If you look at the following gif when I tap on the likes button, you could see a jank where a No likes found
screen is displayed, and then immediately likes
are displayed.
I have seen this similar type of UX glitch in my project. The problem was with this piece of code.
function LikesScreen() {
const [isLoading, setIsLoading] = useState(true);
const [likes, setLikes] = useState([]);
useEffect(() => {
setIsLoading(true);
fetch("https://jsonplaceholder.typicode.com/todos/1").then((likes) => {
setIsLoading(false);
setLikes(likes);
});
}, []);
if (isLoading) return <Loading />;
if (likes.length === 0) {
return <EmptyLikes />;
}
return <Likes likes={likes} />;
}
In the above code initially, the isLoading
state is true.
- Loading screen is rendered.
- Then the effect is triggered
- A network request is made and the promise resolves with the
likes
data.
This is where the interesting thing happens.
React would not batch state updates triggered asynchronously i.e, in Promises, timeouts etc.
So setIsLoading(false)
would trigger a re-render and React would render <EmptyLikes />
Then setLikes(likes)
would trigger another re-render and React would render <Likes />
.
So the setIsLoading ->Render -> setLikes-> Render is the root cause of the issue.
How can we fix this?
We can fix this by merging isLoading
and likes
states into a single state so that state updates are atomic.
function LikesScreen() {
const [{ isLoading, likes }, setState] = useState({
isLoading: true,
likes: []
});
useEffect(() => {
setState((state) => {
return { ...state, isLoading: true };
});
fetch("https://jsonplaceholder.typicode.com/todos/1").then((likes) => {
setState({ likes, isLoading: false });
});
}, []);
if (isLoading) return <Loading />;
if (likes.length === 0) {
return <EmptyLikes />;
}
return <Likes likes={likes} />;
}
So when there are states which have the information about the same context then they could be merged so that the state updates become atomic.
This is a simple case but for complex cases, the state update logics might be more complex and would have been spread throughout the component.
In those cases, useReducer would be really helpful by colocating all of the state update logic.
Next thing
Even after all of this, there could still be a problem.
Consider the state contains 5 boolean fields. Then the total possible states would be 2 pow 5 = 32.
The component should handle all these possible 32 cases. Creating these type of components could be hard
So the solution is to make Illegal states impossible to represent about which I will be writing in the next article🤗
Top comments (3)
I expect the next article to be as good as this one!
Teaser: combinatorial explosion in a component is a Design Pattern smell.
Consider which pattern should we be using here, and how we are breaking it.
Fortunately, the React ecosystem provides a solution :)
a simple yet powerful concept, we easily miss. Thanks for the insights AC.
a minor correction in the code for the audience.
Looking forward to the next one.