A more advanced guide to the useState Hooks 🕕>🦸‍♀️

A more advanced guide to the useState Hooks 🕕>🦸‍♀️

Introduction

In the previous blog, the basics and different use cases of the useState Hook were seen. This blog will present advanced topics, rules to follow, and avoiding mistakes when using the useState Hook.

Inside of useState 🧠

Have you ever wondered what goes on under the hood? What runs so smoothly that it aids in holding a state between re-renders?

useState accepts an initial state and returns an array with ✌ values:

  • One state variable holds the value (the state), and
  • An async function to update the value of the state variable. After the value is updated, React re-renders the component to update the values

Here is a example of how the useState Hooks works internally, and the same is explained below.

let render = -1;  // Keeps track of how many calls made to useState
const states = []; // Keeps track of which value to update

const useState = (value) => {
  const renderId = ++render;

  if (states[renderId])
    // Not on the first render as the state pair already exists.
    // Return it and prepare for the next Hook call.
    return states[renderId];

  const setValue = (newValue) => {
    // On state change put the new value into the pair.
    states[renderId][0] = newValue;
    renderManually();
  };

  const state = [value, setValue]; // On first render, create a state
  states[renderId] = state;   // Store the pair for future renders
  return state;
};

When the default values are passed useState({ paints: 10, crayons: 1 }), the initial values are assigned on the first render, and a state pair is created. On subsequent renders, with the help of the states array, the setValue function knows which value to update.

const renderManually = () => {
  // Reset the var render after each render
  // the component should behave like a new one.
  render = -1;
  const rootElement = document.getElementById("root");
  ReactDOM.render(<App />, rootElement);
};

To see the updated values, a re-rendering must happen. The render variable is reset as the component behaves like a new one.

This is a very basic useState implementation. A lot more goes on behind the scenes, but at the very least, understanding how the function operates internally aids in developing a meaningful mental model.

The Hook does not update immediately 🔔

Printing the value soon after updating the Hook does not display the most recent updated value. Why? 😱

const [color, setColor] = useState("red);

setColor("green");
console.log(color);  // Prints red

The answer is simple -  They are queues

React does not update instantly, even though it appears to do so at first glance. By queuing the states in the order they are called, React keeps track of the states. It queues all upcoming changes, updates them after a component's re-rendering, which is not immediate. It also determines which value matches which state by doing this. Every time the component tries to re-render, it proceeds according to the queue.

A look at (10).png

React useState and setState do not change the state object directly. The reason the updates don't update instantly is because they build queues to enhance performance. Changes to the component state are enqueued via the updater functions. However, React has the potential to stall the changes by updating multiple components at once. The initial update requests are always handled first by React.

Although batching update requests and delaying reconciliation are advantageous, there are some situations when one must wait for updates before acting on updated information.

Thus state updates in React are asynchronous; when an update is requested, there is no guarantee that the updates will be made immediately.

Note: useState guarantees that the state value will always be the most recent after applying updates.

🚫 Mistakes to Avoid 🚫

  • Forcing a React component re-render  => When React fails to update the components automatically, before forcing a re-render, analyze the code.
  • Importance of immutability  =>  State, as described in the React documentation, should be treated as immutable. Using the same value as the current state to update the new state, React won't trigger a re-render. When working with objects, it's easy to make this mistake.
function changeColor() {
   buycolors.paints = 30;
   setBuyColors(buycolors);
 }

The component does not change, so there was no re-rendering trigger. Why? This is because React checks whether the new value for the state reference is the same object. It evaluates based on shallow or reference equality.

In the above example, buyColors object properties were updated, but technically setBuyColors refers to the same object, and thus, React didn't perceive any change in its state.

Solution: Use the spread operator to get the existing properties and then update the Hook. Here is the link for the demo

  • Updating a nested Object incorrectly  =>  The problem when working with multidimensional arrays or nested objects is that Object.assign() and the spread syntax will create a shallow copy instead of a deep copy i.e. only one level deep when copying an array.
const [buyPhone, setBuyPhone] = useState({
    color: "black",
    battery: "4000mAH",
    internet: {
      "4g": true,
       "5g": false
    }
})

setBuyPhone({
   ...buyPhone,   // The spread operator does only a shallow copy
   internet: {
      ...buyPhone.internet,
      "4g": false
   }
});

Here is the link for the demo on how to update a nested Hook properly

  • Using one or many state variables? => To determine which values tend to change together, it is always preferable to divide the state into many state variables. Another advantage of separating independent state variables is extracting some similar logic into a custom Hook is simple. Components tend to be the most readable when balancing these two extremes and combining related states into a small number of independent state variables.

Note: Grouping related data together as state variables work very well as containers for objects and arrays. However, unlike this.setState() in class components, updating a state variable always replaces it instead of merging it.

  • Changing Hooks invocation order  => The Hooks executed conditionally can lead to unexpected and hard-to-debug errors. React Hooks work internally and require components to invoke Hooks in the same order between renderings !

  • Using stale state  =>  When using the current state to calculate the next state, always use a functional way to update the state: setState(prevState => prevState * 10). This can be a common problem when dealing with closures. Here is the link for the demo

  • Passing the functional updater to child components  => Passing the setState function to the child component to set the parent component's state is a bad practice. It is challenging to read and could be confusing, especially when components are mixed in complex use cases. As the application grows, it can have undesired side effects.

  • Better state management would be required to save and apply the logic related to authentication and security, as it is required to hold the user session across different pages that contain different components such as Redux or Context API.

  • Calling the same setter multiple times  => Do not call the same setter Hook multiple times, one after the other. React does not update the Hook immediately.

✍️ Rules to follow

  1. Do not call Hooks from regular JavaScript functions or Class Components of React.
  2. The ESLint plugin enforces the Rules of Hooks. Always advised to install it in the React app.
  3. Call Hooks at the top level of the functional component - do not call it in loops, conditions, or nested functions since React relies on the order in which useState functions are called to get the correct value for a particular state variable.
if (condition) { 
  const [color, setColor] = useState( 'red' );
  setMessage( 'pink' );  
}
// May or may not be executed, changing the order of the useState calls.

That's why it is essential to maintain the Hook calls in the same order. Otherwise, a value belonging to another state variable could be returned.

Conclusion

Hooks require some getting used to, but it is simple and fun once appropriate practice is observed.

Every state update causes the component to be updated to re-render. React improves efficiency by preventing needless re-renders by batching state updates.

Hooks are nothing more than a group of specialized JavaScript functions designed to solve the issues when working with class components. Hooks have changed how React components are created for the better—and are here to stay!

Thank you for reading this article 😊 In the following article, an introduction to useEffect will be presented. Stay tuned, and cheers!

Please feel free to ask questions in the comments below.