DEV Community

a c sreedhar reddy
a c sreedhar reddy

Posted on • Edited on

When could (should) we merge two states?

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} />;
}
Enter fullscreen mode Exit fullscreen mode

In the above code initially, the isLoading state is true.

  1. Loading screen is rendered.
  2. Then the effect is triggered
  3. 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} />;
}
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
bingalls profile image
Bruce Ingalls

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 :)

Collapse
 
prateek13727 profile image
Prateek • Edited

a simple yet powerful concept, we easily miss. Thanks for the insights AC.
a minor correction in the code for the audience.

fetch("https://jsonplaceholder.typicode.com/todos/1")
  .then(response => response.json())
  .then((likes) => {
      setState({ likes, isLoading: false });
    });
Enter fullscreen mode Exit fullscreen mode
Collapse
 
harish321 profile image
Harish Dhulipalla

Looking forward to the next one.