Skip to content

Instantly share code, notes, and snippets.

@microlancer
Last active December 3, 2019 04:40
Show Gist options
  • Save microlancer/31dc0a92ba164352088d0e5f4088140b to your computer and use it in GitHub Desktop.
Save microlancer/31dc0a92ba164352088d0e5f4088140b to your computer and use it in GitHub Desktop.
class User extends Component
{
componentDidMount() {
// Fetch the user info from API using async GET method and knows
// the username to fetch by checking the 'queryName' prop. The
// result will call setState({userState: responseData}) thus updating
// the unistore state for the user.
this.props.apiGetUserAction()
}
render() {
const userState = this.props.userState
return html`<div>Hello ${userState.name}!</div>`
}
}
// Let's suppose that the component gets rendered when the user browses to
// a URL such as http://mysite.com/user/mary and 'mary' is added to props
// as this.props.queryName = 'mary'.
// Now, let's say that the user follows this sequence:
// 1) Browse to http://mysite.com/user/mary
// 2) An xhr request to the API returns the json object for mary
// 3) The setState({ userState: ... }) gets called for mary
// 4) The connected component reacts to the change in state
// and calls render()
// 5) The render() method shows mary's information.
// Everything is working great in this 5-step normal sequence. But, what
// if the API request gets lagged for some reason?
// 1) Browse to http://mysite.com/user/mary
// 2) An xhr request to the API is sent off, but is lagged due to network
// issues...
// 3) Impatiently, the user browses to http://mysite.com/user/john
// 4) For whatever reason, this new xhr request returns quickly
// 5) The setState({ userState: ... }} gets called for john
// 6) The render() shows john's information.
// 7) Finally, the first xhr is completed.
// 8) Now, setState({ userState: ... }} gets called for mary
// 9) Another render() is called, overwriting john's info for mary
// 10) Confusingly, the URL is http://mysite.com/user/john but the
// information on the page is showing mary's data.
// So due to async issues, we get the wrong render!
// We can solve this by doing two things:
// 1) Add a shouldComponentUpdate() which will check that the url
// matches the returning payload, and if not, don't render()
// 2) In the case of a lagged payload coming back to set the state
// while the user isn't even on the user profile page, we may
// end up constructing and mounting the component with the
// incorrect userState to begin with, which means that for a
// brief moment, it may show john's data on mary's profile page
// until the new xhr response comes back. To avoid this, we can
// add more logic to the render() to also verify that the url
// matches the returning payload, and if not, pretend that the
// userState is empty, even though it is populated.
// I'm not sure if it makes sense to add these fixes everywhere? Is
// there a more canonical way people are handling async setState()
// as to not render out-of-date or incorrectly-matched data on their
// UIs?
Answer posted:
import { h } from 'preact';
import htm from 'htm';
import { connect } from 'unistore/preact';
const html = htm.bind(h);
// our fake API call
function getProfile(name) {
return new Promise(resolve => {
setTimeout(() => {
resolve({ name });
}, 1000);
});
}
const actions = store => ({
getUserProfile(state, username) {
if (state.currentUser === username) return;
// use a lock to avoid mixing up callbacks
const lock = {};
const currentUser = username;
store.setState({ currentUser, lock, loading: true });
getProfile(username).then(userState => {
// if getUserProfile has been called elsewhere, discard this callback.
if (store.getState().lock !== lock) return;
// note: update currentUser and userState at the same time!
store.setState({ currentUser, userState, loading: false });
});
}
});
export default connect(
['currentUser', 'userState', 'loading'],
actions
)(({ username, currentUser, userState, loading, getUserProfile }) => {
if (!userState) {
// this happens when we first route. there's no user data ready yet!
userState = {};
}
if (username !== currentUser) {
getUserProfile(username);
}
return html`
<div>
<h2>
Hello ${currentUser}!
<progress-spinner hidden=${!loading} />
</h2>
<p>Profile data for ${userState.name}:</p>
<pre>${JSON.stringify(userState, 0, 2)}</pre>
</div>
`;
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment