Last active
December 3, 2019 04:40
-
-
Save microlancer/31dc0a92ba164352088d0e5f4088140b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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