🪝 Hooks in React

Published 18th December 2023

React hooks have revolutionized the way developers manage state and side effects in functional components. In this article, we will delve into two fundamental hooks:

useState

and

useEffect

. By the end of this article, you will not only understand the basics of these hooks but also gain insights into when to use them and how to navigate potential pitfalls.

The objective is that by the end of reading this article, we understand each for

useState

and

useEffect

:

  • What it is
  • When to use it
  • Any Caveats

Understanding

useState

:

What is a

useState

hook? :

At its core, the

useState

hook is a function provided by React that allows functional components to manage state. It enables you to declare state variables in your components without the need for className components.

import React, { useState } from 'react'; const Counter = () => { // Declare a state variable named 'count' with an initial value of 0 const [count, setCount] = useState(0); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ); };

When to use a

useState

hook? :

Use

useState

when you need to introduce and manage local state within functional components. This is particularly useful for handling dynamic data, form inputs, or any scenario where the component's state needs to change over time.

Caveats of

useState

hooks:

Asynchronous Nature of State Updates:
The setState function returned by useState is asynchronous. If you need to perform an action immediately after updating the state, relying on the next state value directly after calling setState may lead to unexpected results. To address this, use the functional form of setState when the new state depends on the previous state.
// Incorrect usage const handleClick = () => { setState(count + 1); console.log(count); // This may not print the updated count immediately }; jsx // Correct usage with functional form const handleClick = () => { setState((prevCount) => prevCount + 1); };
Object State Merging:
When updating a state that is an object, React does not automatically merge the new state with the existing state. Instead, it completely replaces the old state with the new state. If you want to update only a specific property of an object state, you need to manually merge the old and new state.
// Incorrect usage const handleUpdate = () => { setState({ ...state, property: 'new value' }); // This replaces the entire state object }; jsx // Correct usage with state merging const handleUpdate = () => { setState((prevState) => ({ ...prevState, property: 'new value' })); // Merge with the previous state };

Being mindful of these additional caveats will help you write more robust and predictable code when working with the useState hook in React.

Let’s put what we know to the test:

Another use case: Looking to update state and then pass it to the callback.

// Incorrect usage of useState const [selectedIndex, setSelectedIndex] = useState(0); const handleOnTabPress = useCallback( (index: number) => { if (typeof onTabChange === 'function') { setSelectedIndex((selectedIndex) => index); return onTabChange(selectedIndex); } }, [onTabChange], );

Issue: You are referencing an old value of state. However, the setState operators are asynchronous and execute the current block to the end and the update state. So it is passing the old state to the callback and updating the state value.

Knowing what you know now, which do you think is the correct fix?

if (typeof onTabChange === 'function') { setSelectedIndex(index); return onTabChange(index); }
if (typeof onTabChange === 'function') { setSelectedIndex(index); return onTabChange(selectedIndex); }

Mastering

useEffect

:

What is an

useEffect

hook?

The

useEffect

hook is a Swiss Army knife for handling side effects in functional components. It enables you to execute code that goes beyond the typical rendering process, making it ideal for scenarios like data fetching, managing subscriptions, or updating the DOM.

When to use a

useState

hook:

Use the

useEffect

hook when you encounter scenarios that involve side effects in your functional components. Side effects can encompass a wide range of operations, such as fetching data from an API, subscribing to real-time updates, or manipulating the DOM. This hook is particularly useful when you need to perform such operations after the initial render or in response to changes in specific dependencies.

In the provided example, the

useEffect

hook is used to initialize or update a MapWidget based on the zoomLevel dependency. This ensures that the side effect, in this case, the dependency, setting the zoom level on the map, is executed whenever the zoomLevel changes. This pattern allows you to manage and synchronize side effects with the state of your component, resulting in a more dynamic and responsive user interface.

useEffect(() => { if (mapRef.current === null) { mapRef.current = new MapWidget(containerRef.current); } const map = mapRef.current; map.setZoom(zoomLevel); }, [zoomLevel]);

By leveraging the

useEffect

hook carefully, you can enhance the functionality of your React components, making them more adaptable to a variety of scenarios and enabling you to create a more robust user experience.

Caveats of

useEffect

hooks:

If you're not trying to synchronize with some external system, you probably don’t need an Effect:

useEffect

is a powerful tool, but it introduces a level of complexity to your components. If your component doesn't have side effects like data fetching, subscriptions, or interactions with external systems, using

useEffect

might be unnecessary overhead. Overusing effects in situations where they're not needed can lead to less readable and maintainable code. Therefore, consider whether the behaviour you're implementing truly requires the use of `

useEffect

.
Effects only run on the client. They don’t run during server rendering:
React components can be rendered on the server for initial page loads (Server-Side Rendering or SSR) or on the client during subsequent interactions. It's crucial to recognize that effects specified in

useEffect

will only execute on the client side. If your application relies heavily on server-rendered content and you're expecting effects to run during server rendering, you may need to explore other strategies or lifecycle methods suited for server-side operations.
Effects only run on the client. They don’t run during server rendering:
React components can be rendered on the server for initial page loads (Server-Side Rendering or SSR) or on the client during subsequent interactions. It's crucial to recognize that effects specified in

useEffect

will only execute on the client side. If your application relies heavily on server-rendered content and you're expecting effects to run during server rendering, you may need to explore other strategies or lifecycle methods suited for server-side operations.

By understanding these caveats and nuances associated with

useEffect

, you can make informed decisions about when to use it and how to avoid common pitfalls, leading to more robust and maintainable React applications.

Let’s put what we know to the test:

Another use case: We want to log an event metric to log when the page is visited and make an API call when UUID provided

useEffect(() => { logEvent({ type: 'page visited', }); const response = await fetchAPI(uuid); setCountState(() => response?.count); } }, [uuid]);

Issue: This effect is logging an event every time the UUID changes but we only want it to log on the first visit.

Knowing what you know now, which do you think is the correct fix?

useEffect(() => { logEvent({ type: 'page visited', }); }, [uuid]); useEffect(() => { const response = await fetchAPI(uuid); setCountState(() => response?.count); } }, [uuid]);
useEffect(() => { logEvent({ type: 'page visited', }); }, []); useEffect(() => { const response = await fetchAPI(uuid); setCountState(() => response?.count); } }, [uuid]);

Understanding when to use useState and useEffect is crucial for writing robust and maintainable React applications. The provided examples and caveats shed light on common pitfalls and best practices associated with these hooks. By following these guidelines, developers can create more dynamic and responsive user interfaces while avoiding potential issues.

Resources & References

React Docs

React Docs