DEV Community

Cover image for Demystifying React Hooks: Usage, Examples, and Common Mistakes
Kishan Sheth
Kishan Sheth

Posted on

Demystifying React Hooks: Usage, Examples, and Common Mistakes

React is a JavaScript library used for building user interfaces. In React, hooks are functions that allow you to use state and other React features in functional components.

Here are all the hooks currently available in React:

  1. useState()
  2. useEffect()
  3. useContext()
  4. useReducer()
  5. useCallback()
  6. useMemo()
  7. useRef()
  8. useImperativeHandle()
  9. useLayoutEffect()
  10. useDebugValue()

Introducing Kinsta:

Kinsta is a leading cloud hosting provider that specializes in offering high-performance hosting solutions for applications and databases. With a global network of data centers, Kinsta provides reliable and scalable hosting services to businesses of all sizes, from startups to enterprises.

In addition to their hosting services, Kinsta provides top-notch customer support with a team of WordPress and hosting experts available 24/7 to assist customers with any questions or issues they may encounter.

With its high-performance hosting solutions, robust security measures, and exceptional customer support, Kinsta is a trusted choice for businesses looking for reliable and scalable hosting solutions for their applications and databases.

Use the links below to get $20 worth of credits to get started.
Kinsta Application Hosting
Kinsta Database Hosting

useState

The useState hook is a built-in React hook that allows you to add state to functional components. It is a function that takes an initial value as an argument and returns an array with two values: the current state value and a function to update it. The useState hook is used to manage state in functional components, which do not have a state of their own like class components.

Syntax of the useState Hook

const [state, setState] = useState(initialState);
Enter fullscreen mode Exit fullscreen mode

The useState hook takes an initial state value as its argument and returns an array with two values:

The current state value: This is the current value of the state.

The setState function: This is a function that is used to update the state value. When you call this function, React will re-render the component with the new state value.

Example 1: Simple Counter

Let's start with a simple example of a counter that increments every time a button is clicked. Here's the code:

import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

In this example, we start by importing the useState hook from the React library. We then define a functional component called Counter, which initializes a state variable called count with an initial value of 0 using the useState hook.

Inside the component, we render the current value of the count state variable inside an h1 tag. We also render a button that calls the setCount function whenever it is clicked. The setCount function updates the count state variable by incrementing it by 1.

Example 2: Input Field

In this example, we will create an input field that updates the state variable as the user types. Here's the code:

import React, { useState } from 'react';

function InputField() {
  const [text, setText] = useState('');

  function handleChange(event) {
    setText(event.target.value);
  }

  return (
    <div>
      <input type="text" value={text} onChange={handleChange} />
      <p>You typed: {text}</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

In this example, we define a functional component called InputField that initializes a state variable called text with an empty string using the useState hook.

Inside the component, we render an input field that is bound to the text state variable using the value attribute. We also define a handleChange function that updates the text state variable whenever the user types in the input field.

Finally, we render a paragraph tag that displays the current value of the text state variable.

Example 3: Object State

In this example, we will use the useState hook to manage an object state variable. Here's the code:

import React, { useState } from "react";

function ObjectState() {
  const [person, setPerson] = useState({ name: "", age: 0 });

  function handleChange(event) {
    setPerson({ ...person, [event.target.name]: event.target.value });
  }

  return (
    <div>
      <label>
        Name:
        <input
          type="text"
          name="name"
          value={person.name}
          onChange={handleChange}
        />
      </label>
      <br />
      <label>
        Age:
        <input
          type="number"
          name="age"
          value={person.age}
          onChHookHookHookange={handleChange}
        />
      </label>
      <br />
      <p>Name: {person.name}</p>
      <p>Age: {person.age}</p>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

In this example, we define a functional component called ObjectState that initializes a state variable called person with an object containing name and age properties using the useState hook.

Inside the component, we render two input fields, one for the person's name and one for their age. We also define a handleChange function that updates the person state variable using the spread operator to preserve the existing properties and overwrite the property that matches the name attribute of the input field that triggered the event.

Finally, we render two paragraph tags that display the current values of the name and age properties of the person state variable.

Common Errors with useState and how to solve them :

1 Forgetting to pass an initial state value to useState

One common mistake is forgetting to pass an initial state value to the useState hook. This can lead to unexpected behavior and errors. Always make sure to pass an initial state value to the useState hook.

// Incorrect
const [count, setCount] = useState(); // no initial state value passed

// Correct
const [count, setCount] = useState(0); // initial state value of 0 passed
Enter fullscreen mode Exit fullscreen mode

2 Incorrectly updating the state

Another common mistake is incorrectly updating the state. The setState function returned by useState should be used to update the state, but some people try to update it directly. This can lead to bugs and unpredictable behavior. Always use the setState function to update the state.

// Incorrect
setCount(count + 1); // trying to update state directly

// Correct
setCount(prevCount => prevCount + 1); // using functional form of setState to update state
Enter fullscreen mode Exit fullscreen mode

3 Changing state directly

You should never modify the state directly. Doing so can lead to bugs and unpredictable behavior. Instead, you should always use the setState function to update the state.

// Incorrect
count++;

// Correct
setCount(prevCount => prevCount + 1);
Enter fullscreen mode Exit fullscreen mode

4 Passing a function to setState that relies on the current state

Sometimes people pass a function to setState that relies on the current state. This can lead to bugs and unexpected behavior. When you need to update the state based on the current state, use the functional form of setState that takes a callback function as an argument.

// Incorrect
setCount(count + 1); // assuming count is the current state value

// Correct
setCount(prevCount => prevCount + 1); // using functional form of setState that takes the previous state value as an argument
Enter fullscreen mode Exit fullscreen mode

5 Creating a new state on every render

Another common mistake is creating a new state on every render. This can lead to performance issues and unexpected behavior. Instead, use the useMemo or useCallback hooks to memoize the state.

// Incorrect
const [items, setItems] = useState(getItems()); // getItems() called on every render

// Correct
const [items, setItems] = useState(() => getItems()); // getItems() called only on initial render, thanks to the useCallback hook
Enter fullscreen mode Exit fullscreen mode

6 Not using the destructuring syntax

The useState hook returns an array of two values: the current state and the setState function. It is important to use the destructuring syntax to extract these values from the array. Not doing so can lead to bugs and unexpected behavior.

// Incorrect
const state = useState(0);
state[0]++; // updating state directly

// Correct
const [count, setCount] = useState(0);
setCount(count + 1); // using destructuring syntax to extract state values from array and update state
Enter fullscreen mode Exit fullscreen mode

useEffect

The useEffect hook is a built-in React hook that enables developers to handle side effects in their components. Side effects can include anything that affects the outside world, such as API requests, DOM manipulation, or subscription to events. The useEffect hook accepts two parameters: a callback function that represents the side effect and an optional array of dependencies. The callback function executes after the component mounts, and after every subsequent re-render if any of the dependencies change.

Examples of useEffect Hook

Fetching Data from an API

Let's assume that we want to fetch data from an API and display it in our component. We can use the useEffect hook to fetch data and update the component state.

import React, { useState, useEffect } from 'react';

function App() {
  const [data, setData] = useState([]);

  useEffect(() => {
    async function fetchData() {
      const response = await fetch('https://jsonplaceholder.typicode.com/posts');
      const data = await response.json();
      setData(data);
    }
    fetchData();
  }, []);

  return (
    <div>
      {data.map((post) => (
        <div key={post.id}>
          <h3>{post.title}</h3>
          <p>{post.body}</p>
        </div>
      ))}
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

In the example above, we use the useState hook to store the fetched data and the useEffect hook to fetch data from the API. We pass an empty array [] as the second parameter to ensure that the effect runs only once when the component mounts.

Subscribing to an Event

In some cases, we may want to subscribe to an event, such as a window resize or a mouse movement. We can use the useEffect hook to subscribe to the event and clean up after ourselves when the component unmounts.

import React, { useState, useEffect } from 'react';

function App() {
  const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  useEffect(() => {
    function handleResize() {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    }
    window.addEventListener('resize', handleResize);
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);

  return (
    <div>
      <p>Window width: {windowSize.width}</p>
      <p>Window height: {windowSize.height}</p>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

In the example above, we use the useState hook to store the window size and the useEffect hook to subscribe to the window resize event. We pass an empty array [] as the second parameter to ensure that the effect runs only once when the component mounts. We also return a cleanup function to remove the event listener when the component unmounts.

Common Errors with useEffect Hook and How to Solve Them

1 Infinite Loop

One common mistake with the useEffect hook is creating an infinite loop by updating the state inside the effect without providing any dependencies. In such cases, the effect runs on every re-render, causing the state to update, which triggers another re-render, and so on, resulting in an infinite loop. To solve this error, we need to add the state as a dependency to the useEffect hook or move the state update outside the effect.

// Infinite Loop Example
useEffect(() => {
  setData(data + 1); // updates data state
});

// Solution
useEffect(() => {
  setData((prevData) => prevData + 1); // uses prevState to update data state
}, [data]);
Enter fullscreen mode Exit fullscreen mode

2 Missing Dependencies

Another common mistake is omitting dependencies from the useEffect hook, which can result in unexpected behavior. For example, if we have a state value that the effect relies on, but we forget to add it as a dependency, the effect may not run when the state changes. To solve this error, we need to add all dependencies to the dependency array.

// Missing Dependencies Example
useEffect(() => {
  // effect relies on count state value
  console.log(`Count is ${count}`);
}, []);

// Solution
useEffect(() => {
  console.log(`Count is ${count}`);
}, [count]); // add count state to dependency array

Enter fullscreen mode Exit fullscreen mode

3 Cleanup Function

It's important to remember to include a cleanup function in the useEffect hook to clean up any resources that were created in the effect. For example, if we subscribe to an event or set up a timer, we need to remove the subscription or clear the timer when the component unmounts. To solve this error, we need to return a cleanup function from the effect.

// Cleanup Function Example
useEffect(() => {
  const interval = setInterval(() => {
    console.log('Tick');
  }, 1000);
  return () => {
    clearInterval(interval); // removes interval on unmount
  };
}, []);
Enter fullscreen mode Exit fullscreen mode

useContext()

The useContext hook is a hook in React that allows you to consume context within a component. Context is a way to share data between components without having to pass props down the component tree. Context provides a way to pass data through the component tree without having to pass props down manually at every level.

The useContext hook allows you to consume context within a functional component. It takes a context object as its argument and returns the current context value.

Example 1: Simple Context

Let's start with a simple example to illustrate how the useContext hook works. In this example, we will create a context to store the user's name and use the useContext hook to access the context value in a child component.

// Create the context
const NameContext = React.createContext();

// Create a provider component to provide the context value
function NameProvider({ children }) {
  const [name, setName] = useState("");

  return (
    <NameContext.Provider value={{ name, setName }}>
      {children}
    </NameContext.Provider>
  );
}

// Create a child component that consumes the context value
function Greeting() {
  const { name } = useContext(NameContext);

  return <p>Hello, {name}!</p>;
}

// Render the provider and child components
function App() {
  return (
    <NameProvider>
      <Greeting />
    </NameProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

In this example, we first create a context using the createContext function. We then create a provider component called NameProvider that provides the context value to its children. The provider component uses the useState hook to store the user's name and passes the name and setName functions as the context value.

Next, we create a child component called Greeting that consumes the context value using the useContext hook. The Greeting component extracts the name value from the context object and renders a greeting message.

Finally, we render the NameProvider component as the top-level component and the Greeting component as a child component.

Example 2: Nested Context

In this example, we will create a nested context to store the user's name and age and use the useContext hook to access the context value in a child component.

// Create the contexts
const NameContext = React.createContext();
const AgeContext = React.createContext();

// Create a provider component to provide the context values
function UserProvider({ children }) {
  const [name, setName] = useState("");
  const [age, setAge] = useState(0);

  return (
    <NameContext.Provider value={{ name, setName }}>
      <AgeContext.Provider value={{ age, setAge }}>
        {children}
      </AgeContext.Provider>
    </NameContext.Provider>
  );
}

// Create a child component that consumes the context values
function Profile() {
  const { name } = useContext(NameContext);
  const { age } = useContext(AgeContext);

  return (
    <div>
      <p>Name: {name}</p>
      <p>Age: {age}</p>
    </div>
  );
}

// Render the provider and child components
function App() {
  return (
    <UserProvider>
      <Profile />
    </UserProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

In this example, we create two contexts:
NameContext and AgeContext. We then create a provider component called UserProvider that provides both context values to its children. The provider component uses the useState hook to store the user's name and age and passes the name and setName functions as the value of the NameContext and the age and setAge functions as the value of the AgeContext.

Next, we create a child component called Profile that consumes both context values using the useContext hook. The Profile component extracts the name and age values from their respective context objects and renders them in the UI.

Finally, we render the UserProvider component as the top-level component and the Profile component as a child component.

Common Mistakes with useContext Hook

1 Not using the Provider Component:

The useContext hook can only consume context that has been provided by a Provider component. If you try to use the useContext hook without a Provider component, you will get an error. Make sure to wrap the components that use the useContext hook with a Provider component.

// Incorrect example: no Provider component
import React, { useContext } from 'react';
import { UserContext } from './UserContext';

function Profile() {
  const user = useContext(UserContext);

  return (
    <div>
      <h1>Hello, {user.name}!</h1>
      <p>You are {user.age} years old.</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode
// Correct example: with Provider component
import React, { useContext } from 'react';
import { UserContext, UserProvider } from './UserContext';

function Profile() {
  const user = useContext(UserContext);

  return (
    <div>
      <h1>Hello, {user.name}!</h1>
      <p>You are {user.age} years old.</p>
    </div>
  );
}

function App() {
  return (
    <UserProvider>
      <Profile />
    </UserProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

2 Creating Too Many Contexts:

Context is a powerful feature in React, but creating too many contexts can lead to complex and hard-to-maintain code. Before creating a new context, ask yourself if you can reuse an existing context or pass the necessary data down as props.

// Incorrect example: creating too many contexts
import React, { createContext } from 'react';

const NameContext = createContext('');
const AgeContext = createContext(0);
const EmailContext = createContext('');

function Profile() {
  const name = useContext(NameContext);
  const age = useContext(AgeContext);
  const email = useContext(EmailContext);

  return (
    <div>
      <h1>Hello, {name}!</h1>
      <p>You are {age} years old.</p>
      <p>Your email address is {email}.</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode
// Correct example: using a single context
import React, { createContext } from 'react';

const UserContext = createContext({ name: '', age: 0, email: '' });

function Profile() {
  const { name, age } = useContext(UserContext);

  return (
    <div>
      <h1>Hello, {name}!</h1>
      <p>You are {age} years old.</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

3 Passing Down Too Much Data:

While it can be tempting to pass down all your data through context, it can quickly become unmanageable. Only pass down the data that is necessary for the child components to function. If a child component only needs a small subset of the data, consider passing that data down as props instead of using context.

// Incorrect example: passing down too much data through context
import React, { useState, createContext } from 'react';

const UserContext = createContext();

function UserProvider({ children }) {
  const [name, setName] = useState('');
  const [age, setAge] = useState(0);
  const [email, setEmail] = useState('');

  const user = { name, age, email, setName, setAge, setEmail };

  return <UserContext.Provider value={user}>{children}</UserContext.Provider>;
}

function Profile() {
  const user = useContext(UserContext);

  // Even though this component only uses the `name` property of the user object,
  // it receives the entire object through context.
  return (
    <div>
      <h1>Hello, {user.name}!</h1>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode
// Correct example: passing down only necessary data through context
import React, { useState, createContext } from 'react';

const NameContext = createContext();
const AgeContext = createContext();

function UserProvider({ children }) {
  const [name, setName] = useState('');
  const [age, setAge] = useState(0);

  const user = { name, age };

  return (
    <NameContext.Provider value={name}>
      <AgeContext.Provider value={age}>{children}</AgeContext.Provider>
    </NameContext.Provider>
  );
}

function Profile()  {
  const name = useContext(NameContext);
  const age = useContext(AgeContext);

  return (
    <div>
      <h1>Hello, {name}!</h1>
      <p>You are {age} years old.</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

4 Mutating Context Directly:

Context should be treated as immutable data. Mutating context directly can cause unexpected behavior and lead to bugs. Instead, use setState or the equivalent function to update the context value.

// Incorrect example: modifying context data directly
import React, { useState, createContext } from 'react';

const UserContext = createContext();

function UserProvider({ children }) {
  const [name, setName] = useState('');
  const [age, setAge] = useState(0);

  const user = { name, age, setName, setAge };

  return <UserContext.Provider value={user}>{children}</UserContext.Provider>;
}

function Profile() {
  const user = useContext(UserContext);

  function handleAgeChange(event) {
    const newAge = parseInt(event.target.value);
    user.age = newAge; // Modifying context data directly, which is not allowed
  }

  return (
    <div>
      <h1>Hello, {user.name}!</h1>
      <p>You are {user.age} years old.</p>
      <input type="number" value={user.age} onChange={handleAgeChange} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode
// Correct example: updating context data through functions
import React, { useState, createContext } from 'react';

const UserContext = createContext();

function UserProvider({ children }) {
  const [name, setName] = useState('');
  const [age, setAge] = useState(0);

  const user = { name, age, setName, setAge };

  return <UserContext.Provider value={user}>{children}</UserContext.Provider>;
}

function Profile() {
  const user = useContext(UserContext);

  function handleAgeChange(event) {
    const newAge = parseInt(event.target.value);
    user.setAge(newAge); // Updating context data through functions
  }

  return (
    <div>
      <h1>Hello, {user.name}!</h1>
      <p>You are {user.age} years old.</p>
      <input type="number" value={user.age} onChange={handleAgeChange} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

useReducer

useReducer is a hook that follows the reducer pattern, where you have a state and an action that changes the state. The useReducer hook takes two arguments: a reducer function and an initial state.

The reducer function takes two arguments: the current state and an action object, and returns the new state based on the action type. The action object typically contains a type property that defines the type of action being performed and may contain additional data.

Examples of useReducer hook:

1 Basic Example:

import React, { useReducer } from 'react';

function reducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    default:
      return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });

  function handleIncrement() {
    dispatch({ type: 'INCREMENT' });
  }

  function handleDecrement() {
    dispatch({ type: 'DECREMENT' });
  }

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={handleIncrement}>+</button>
      <button onClick={handleDecrement}>-</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

In this example, we define a reducer function that takes the current state and an action object, and returns a new state based on the action type. The Counter component uses useReducer hook to create a state object with a count property, and a dispatch function that is used to trigger actions that modify the state. The handleIncrement and handleDecrement functions call dispatch function with an action object that has the type property set to 'INCREMENT' and 'DECREMENT', respectively.

2 Shopping Cart:

import React, { useReducer } from 'react';

function reducer(cart, action) {
  switch (action.type) {
    case 'ADD':
      return [...cart, action.payload];
    case 'REMOVE':
      return cart.filter(item => item.id !== action.payload.id);
    case 'INCREMENT':
      return cart.map(item => (item.id === action.payload.id ? { ...item, quantity: item.quantity + 1 } : item));
    case 'DECREMENT':
      return cart.map(item =>
        item.id === action.payload.id ? { ...item, quantity: item.quantity - 1 } : item
      );
    default:
      return cart;
  }
}

function ShoppingCart() {
  const [cart, dispatch] = useReducer(reducer, []);

  function handleAdd(item) {
    dispatch({ type: 'ADD', payload: { ...item, quantity: 1 } });
  }

  function handleRemove(item) {
    dispatch({ type: 'REMOVE', payload: item });
  }

  function handleIncrement(item) {
    dispatch({ type: 'INCREMENT', payload: item });
  }

  function handleDecrement(item) {
    dispatch({ type: 'DECREMENT', payload: item });
  }

  const totalPrice = cart.reduce((total, item) => total + item.price * item.quantity, 0);

  return (
    <div>
      <ul>
        {cart.map(item => (
          <li key={item.id}>
            <span>{item.name}</span>
            <span>{item.price}</span>
            <button onClick={() => handleDecrement(item)}>-</button>
            <span>{item.quantity}</span>
            <button onClick={() => handleIncrement(item)}>+</button>
            <button onClick={() => handleRemove(item)}>Remove</button>
          </li>
        ))}
      </ul>
      <p>Total: {totalPrice}</p>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

In this example, we define a reducer function that takes the current cart state and an action object, and returns a new state based on the action type. The ShoppingCart component uses useReducer hook to create a state object with an empty array as the initial state, and a dispatch function that is used to trigger actions that modify the cart state. The handleAdd, handleRemove, handleIncrement, and handleDecrement functions call dispatch function with an action object that has the corresponding action type and payload.

Common Mistakes with useReducer Hook

1 Incorrectly Updating the State

One of the common mistakes people make with useReducer hook is updating the state incorrectly. In useReducer, the state is immutable, and any changes to the state should result in a new state object being returned from the reducer function. If you mutate the state directly, React may not detect the changes, and your component may not re-render correctly.

Here is an example of how not to update the state:

function reducer(state, action) {
  switch (action.type) {
    case 'ADD':
      state.push(action.payload); // incorrect!
      return state;
    default:
      return state;
  }
}
Enter fullscreen mode Exit fullscreen mode

Instead, you should create a new state object with the changes:

function reducer(state, action) {
  switch (action.type) {
    case 'ADD':
      return [...state, action.payload]; // correct!
    default:
      return state;
  }
}
Enter fullscreen mode Exit fullscreen mode

2 Not Providing an Initial State:

Another common mistake people make with useReducer hook is not providing an initial state. If you don't provide an initial state, React will assume that the initial state is undefined, and your component may not work as expected. To avoid this mistake, always provide an initial state when using useReducer hook.

Here is an example of how to provide an initial state:

function reducer(state, action) {
  switch (action.type) {
    case 'ADD':
      return [...state, action.payload];
    default:
      return state;
  }
}

function MyComponent() {
  const [myState, dispatch] = useReducer(reducer, { counter: 0 });
  // ...
}
Enter fullscreen mode Exit fullscreen mode

In this example, we provide an initial state object with a counter property set to 0.

3 Not Handling All Action Types:

Another common mistake people make with useReducer hook is not handling all action types in the reducer function. If you don't handle all action types, your component may not work as expected, and you may get errors in the console.

Here is an example of how to handle all action types:

function reducer(state, action) {
  switch (action.type) {
    case 'ADD':
      return [...state, action.payload];
    case 'REMOVE':
      return state.filter(item => item.id !== action.payload.id);
    case 'INCREMENT':
      return state.map(item => (item.id === action.payload.id ? { ...item, quantity: item.quantity + 1 } : item));
    case 'DECREMENT':
      return state.map(item =>
        item.id === action.payload.id ? { ...item, quantity: item.quantity - 1 } : item
      );
    default:
      throw new Error(`Unhandled action type: ${action.type}`);
  }
}
Enter fullscreen mode Exit fullscreen mode

In this example, we add a default case to the switch statement that throws an error if an unknown action type is provided.

useCallback

useCallback is a hook that helps you optimize the performance of your React components by memoizing the functions. It returns a memoized version of the callback function that only changes if one of the dependencies has changed. If none of the dependencies have changed, useCallback returns the previously memoized function, which avoids unnecessary re-renders.

How to Use useCallback

The syntax for useCallback is as follows:

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b]
);
Enter fullscreen mode Exit fullscreen mode

The first argument to useCallback is the callback function that you want to memoize. The second argument is an array of dependencies that should trigger the re-creation of the memoized function. If any of the dependencies in the array have changed, useCallback will re-create the memoized function. Otherwise, it will return the previously memoized function.

Example of how to use useCallback in a component:

import React, { useState, useCallback } from 'react';

function MyComponent() {
  const [count, setCount] = useState(0);

  const increment = useCallback(() => {
    setCount(count + 1);
  }, [count]);

  return (
    <div>
      <h1>{count}</h1>
      <button onClick={increment}>Increment</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

In this example, we're using useCallback to memoize the increment function, which is passed to the onClick handler of the button element. We're also adding count as a dependency to the useCallback hook so that the memoized increment function is re-created only when count changes.

Common Mistakes with useCallback() hook:

1 Not Adding Dependencies to useCallback

One common mistake people make with useCallback is not adding the dependencies to the second argument of the hook. This can cause unexpected behavior in your component, such as stale closures or infinite loops.

Here's an example of how to avoid this mistake:

import React, { useState, useCallback } from 'react';

function MyComponent() {
  const [count, setCount] = useState(0);

  const increment = useCallback(() => {
    setCount(count + 1); // This is a stale closure!
  }, []); // Don't forget to add dependencies to the array!

  return (
    <div>
      <h1>{count}</h1>
      <button onClick={increment}>Increment</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

In this example, we forgot to add count as a dependency to the useCallback hook. This creates a stale closure, and the count variable always has the initial value of 0, even if it changes in the component state. To fix this, we add count to the dependencies array.

2 Using useCallback with Non-primitive Dependencies

useCallback only works with primitive types such as strings, numbers, and booleans. If you pass an object or an array as a dependency, useCallback will always return a new function, even if the object or array hasn't changed.

Here's an example of how to avoid this mistake:

import React, { useState, useCallback } from 'react';

function MyComponent() {
  const [data, setData] = useState([]);

  function fetchData() {
    // Fetch data from an API and update the state
  }

  const fetchDataCallback = useCallback(fetchData, [data]); // This won't work!

  return (
    <div>
      <button onClick={fetchDataCallback}>Fetch Data</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

In this example, we're trying to memoize the fetchData function using useCallback. However, we're passing data as a dependency, which is an array. This won't work because useCallback only works with primitive types. Instead, we should use the useMemo hook to memoize the data and pass it as a dependency to the fetchData function.

import React, { useState, useCallback, useMemo } from 'react';

function MyComponent() {
  const [data, setData] = useState([]);

  function fetchData() {
    // Fetch data from an API and update the state
  }

  const memoizedData = useMemo(() => data, [data]);
  const fetchDataCallback = useCallback(() => fetchData(memoizedData), [memoizedData]);

  return (
    <div>
      <button onClick={fetchDataCallback}>Fetch Data</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

In this example, we're using the useMemo hook to memoize the data array and pass it as a dependency to the fetchData function. Then we're using useCallback to memoize the fetchData function with memoizedData as a dependency.

useMemo

useMemo is a hook that allows you to memoize the results of a function call and return the cached value if the dependencies haven't changed. It takes two arguments: a function that returns a value, and an array of dependencies that are used to determine if the value should be recomputed.

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
Enter fullscreen mode Exit fullscreen mode

In this example, computeExpensiveValue is a function that takes two arguments a and b and returns a value. We're using useMemo to memoize the value of computeExpensiveValue(a, b) and only recompute it if a or b change.

Example of how to useMemo()

The most common use case for useMemo is to optimize the performance of a component that renders a large list or a complex UI. Instead of recomputing the same values on every render, you can use useMemo to memoize the values and return the cached results if the dependencies haven't changed.

Here's an example of how to use useMemo to memoize a list of items:

import React, { useMemo } from 'react';

function MyComponent({ items }) {
  const sortedItems = useMemo(() => items.sort(), [items]);

  return (
    <div>
      <h1>Sorted Items</h1>
      <ul>
        {sortedItems.map((item) => (
          <li key={item}>{item}</li>
        ))}
      </ul>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

In this example, we're using useMemo to memoize the sorted list of items and only re-sort the list if the items prop changes.

Common Mistakes with useMemo

Like all React hooks, useMemo has some common mistakes that can lead to unexpected behavior and performance issues. Let's take a look at some of them:

1 Not Adding All Dependencies

One common mistake with useMemo is not adding all the dependencies to the array of dependencies. If you forget to add a dependency, you can end up with stale or incorrect data.

import React, { useMemo } from 'react';

function MyComponent({ items }) {
  const sortedItems = useMemo(() => items.sort(), []); // Oops! Missing dependency

  return (
    <div>
      <h1>Sorted Items</h1>
      <ul>
        {sortedItems.map((item) => (
          <li key={item}>{item}</li>
        ))}
      </ul>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

In this example, we forgot to add the items dependency to the array of dependencies. This means that the sorted list won't update if the items prop changes.

2 Memoizing Non-primitive Values

Like useCallback, useMemo only works with primitive values such as strings, numbers, and booleans. If you pass an object or an array as a dependency, useMemo will always return a new value, even if the object or array hasn't changed.

import React, { useMemo } from 'react';

function MyComponent({ data }) {
  const computedData = useMemo(() => {
    return data.map(item => {
      return item.name.toUpperCase();
    });
  }, [data]);

  return (
    <div>
      <h1>Computed Data</h1>
      <ul>
        {computedData.map(item => (
          <li key={item}>{item}</li>
        ))}
      </ul>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

In this example, we're memoizing an array that's computed from the data prop. However, since arrays are non-primitive values, the computedData will always be a new array, even if data hasn't changed. To fix this, we need to make sure that data is a primitive value or use a different hook such as useReducer or useState.

3 Overusing useMemo

While useMemo is a powerful optimization tool, it's important not to overuse it. Memoizing small values or values that don't change often can actually hurt performance by adding unnecessary overhead.

import React, { useMemo } from 'react';

function MyComponent({ value }) {
  const computedValue = useMemo(() => value * 2, [value]);

  return (
    <div>
      <h1>Computed Value</h1>
      <p>{computedValue}</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

In this example, we're memoizing a value that's just multiplied by 2. Since this is a simple calculation that doesn't require memoization, we're actually adding unnecessary overhead and hurting performance. Instead, we can just calculate the value directly in the return function.

useRef

The useRef hook is used to create a mutable reference that can be attached to a DOM element or any other value in a functional component. Unlike state variables, changes to the value of a ref do not trigger a re-render of the component. This makes useRef ideal for managing DOM refs or storing other mutable values that need to persist across re-renders.

The syntax for using useRef is simple:

import React, { useRef } from 'react';

const MyComponent = () => {
  const myRef = useRef();

  // ...rest of the component code

  return (
    // JSX
  );
};
Enter fullscreen mode Exit fullscreen mode

You can also initialize the ref with an initial value by passing it as an argument to useRef:

const myRef = useRef(initialValue);
Enter fullscreen mode Exit fullscreen mode

Examples of useRef

1 Accessing DOM elements

One of the most common uses of useRef is to access DOM elements. Here's an example:

import React, { useRef } from 'react';

function MyComponent() {
  const inputRef = useRef();

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <div>
      <input type="text" ref={inputRef} />
      <button onClick={handleClick}>Focus Input</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

In this example, we're using useRef to get a reference to the input element. We then use the focus method on the inputRef.current to give it focus when the button is clicked.

2. Storing previous state or props

Another common use case for the useRef hook is to store previous state or props, which can be useful for comparing changes between renders. This is often used in conjunction with the useEffect hook to trigger side effects based on changes to state or props.

For example, let's say we have a component that receives a prop called value, and we want to trigger a side effect whenever the value changes:

import { useEffect } from 'react';

function MyComponent({ value }) {
  useEffect(() => {
    // Trigger side effect whenever value changes
  }, [value]);

  return (
    <div>
      <p>{value}</p>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

However, what if we want to compare the new value to the previous value in our side effect? We can use the useRef hook to store the previous value and compare it to the new value:

import { useRef, useEffect } from 'react';

function MyComponent({ value }) {
  const prevValueRef = useRef(value);

  useEffect(() => {
    // Compare new value to previous value
    if (value !== prevValueRef.current) {
      // Trigger side effect
    }

    // Update previous value
    prevValueRef.current = value;
  }, [value]);

  return (
    <div>
      <p>{value}</p>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

In this example, we create a ref called prevValueRef and initialize it with the current value prop. Then, in our useEffect hook, we compare the new value prop to the previous value stored in the ref. If the new value is different, we trigger our side effect. Finally, we update the ref with the new value so it will be available for the next render.

This pattern can also be used to store previous state values in a similar way. For example, let's say we have a component with a counter that increments when a button is clicked:

import { useState } from 'react';

function MyComponent() {
  const [count, setCount] = useState(0);

  const incrementCount = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={incrementCount}>Increment</button>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

If we want to compare the new count value to the previous count value, we can use the useRef hook to store the previous value:

import { useRef, useState } from 'react';

function MyComponent() {
  const [count, setCount] = useState(0);
  const prevCountRef = useRef(count);

  const incrementCount = () => {
    setCount(count + 1);
  };

  useEffect(() => {
    // Compare new count to previous count
    if (count !== prevCountRef.current) {
      // Trigger side effect
    }

    // Update previous count
    prevCountRef.current = count;
  }, [count]);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={incrementCount}>Increment</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

In this example, we create a ref called prevCountRef and initialize it with the current count state. Then, in our useEffect hook, we compare the new count state to the previous count stored in the ref. If the new count is different, we trigger our side effect. Finally, we update the ref with the new count so it will be available for the next render.

Common Mistakes with useRef

1. Modifying ref values directly

One common mistake when using useRef is modifying the ref value directly. Since the ref value is mutable, you might be tempted to modify it directly, but this will not trigger a re-render. For example:

const myRef = useRef(0);

const handleClick = () => {
  myRef.current += 1; // This will not trigger a re-render
};
Enter fullscreen mode Exit fullscreen mode

Instead, you should use the useRef hook in conjunction with the useState hook to manage mutable values that need to trigger a re-render. Here's an example:

import React, { useRef, useState } from 'react';

const MyComponent = () => {
  const [count, setCount] = useState(0);
  const myRef = useRef(0);

  const handleClick = () => {
    myRef.current += 1;
    setCount(myRef.current); // Trigger a re-render
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment</button>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

In this example, we use the useRef hook to store the mutable value "myRef", but we also use the useState hook to manage the state "count". When the button is clicked, we modify the "myRef" value directly, but we also update the state "count" with the new value to trigger a re-render.

2. Not using the current property

Another common mistake is not using the ".current" property when accessing the value of the ref. The ".current" property holds the current value of the ref, and you should always access it to get the latest value. For example:

const myRef = useRef(0);

const handleClick = () => {
  console.log(myRef); // Incorrect
  console.log(myRef.current); // Correct
};
Enter fullscreen mode Exit fullscreen mode

Always remember to use the ".current" property to access the value of the ref.

3. Not using useEffect to update refs

If you want to update a ref based on a prop or state change, you should use the useEffect hook. One mistake is directly updating the ref value inside the component body, which may cause issues with stale values. For example:

const MyComponent = ({ value }) => {
  const myRef = useRef();

  myRef.current = value; // Incorrect

  // ... rest of the component code ...
};

Enter fullscreen mode Exit fullscreen mode

Instead, you should use the useEffect hook to update the ref value based on changes in props or state. Here's an example:

const MyComponent = ({ value }) => {
  const myRef = useRef();

  useEffect(() => {
    myRef.current = value; // Update ref value
  }, [value]); // Trigger on prop change

  // ... rest of the component code ...
};
Enter fullscreen mode Exit fullscreen mode

In this example, we use the useEffect hook to update the ref value "myRef.current" whenever the prop "value" changes.

useImperativeHandle

useImperativeHandle is a custom React hook that allows you to customize the instance value that is exposed by a parent component when it references a child component. It's often used in cases where a parent component needs to interact directly with a child component, such as manipulating DOM elements or triggering animations.

The basic syntax for useImperativeHandle is as follows:

useImperativeHandle(ref, createHandle, deps);
Enter fullscreen mode Exit fullscreen mode
  • ref: A ref object that is passed to the child component.
  • createHandle: A function that returns an object containing the imperative methods or values that you want to expose to the parent component.
  • deps: An array of dependencies that triggers a re-creation of the handle when the dependencies change. It is an optional parameter.

Example 1: Exposing a DOM Element

One common use case for useImperativeHandle is to expose a DOM element of a child component to the parent component. Here's an example:

import React, { useRef, useImperativeHandle } from 'react';

const ChildComponent = React.forwardRef((props, ref) => {
  const inputRef = useRef();

  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    },
    getValue: () => {
      return inputRef.current.value;
    },
  }));

  return <input ref={inputRef} />;
});

const ParentComponent = () => {
  const childRef = useRef();

  const handleFocusClick = () => {
    childRef.current.focus();
  };

  const handleGetValueClick = () => {
    alert(childRef.current.getValue());
  };

  return (
    <div>
      <ChildComponent ref={childRef} />
      <button onClick={handleFocusClick}>Focus Input</button>
      <button onClick={handleGetValueClick}>Get Value</button>
    </div>
  );
};

export default ParentComponent;

Enter fullscreen mode Exit fullscreen mode

In this example, the ChildComponent exposes a focus method and a getValue method using useImperativeHandle. The parent component, ParentComponent, can then access these methods through the childRef ref object and call them as needed.

Example 2: Exposing Custom Methods

You can also use useImperativeHandle to expose custom methods or values from a child component to its parent component. Here's an example:

import React, { useRef, useImperativeHandle } from "react";

const ChildComponent = React.forwardRef((props, ref) => {
  const countRef = useRef(0);

  const increment = () => {
    countRef.current += 1;
  };

  useImperativeHandle(ref, () => ({
    increment: increment,
    getCount: () => {
      return countRef.current;
    },
  }));

  return <div>Count: {countRef.current}</div>;
});

const ParentComponent = () => {
  const childRef = useRef();

  const handleIncrementClick = () => {
    childRef.current.increment();
  };

  const handleGetCountClick = () => {
    alert(childRef.current.getCount());
  };

  return (
    <div>
      <ChildComponent ref={childRef} />
      <button onClick={handleIncrementClick}>Increment</button>
      <button onClick={handleGetCountClick}>Get Count</button>
    </div>
  );
};

export default ParentComponent;

Enter fullscreen mode Exit fullscreen mode

In this example, the ChildComponent exposes an increment method and a getCount method using useImperativeHandle. The parent component, ParentComponent, can then access these methods through the childRef ref object and call them as needed.

Common Mistakes with useImperativeHandle

While useImperativeHandle can be a powerful tool for exposing imperative methods from child to parent components, there are some common mistakes that developers might make. Let's take a look at them with code examples.

Mistake 1: Not using React.forwardRef

useImperativeHandle requires the use of React.forwardRef in the child component. If you forget to use it, the ref object passed to the child component will not have the methods or values exposed by useImperativeHandle. Here's an example:

import React, { useRef, useImperativeHandle } from 'react';

// Incorrect usage of useImperativeHandle without React.forwardRef
const ChildComponent = (props, ref) => {
  const inputRef = useRef();

  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    },
  }));

  return <input ref={inputRef} />;
};
Enter fullscreen mode Exit fullscreen mode

To fix this, you need to use React.forwardRef to wrap the child component function:

import React, { useRef, useImperativeHandle } from 'react';

// Correct usage of useImperativeHandle with React.forwardRef
const ChildComponent = React.forwardRef((props, ref) => {
  const inputRef = useRef();

  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    },
  }));

  return <input ref={inputRef} />;
});
Enter fullscreen mode Exit fullscreen mode

Mistake 2: Not Providing the Dependency Array

useImperativeHandle accepts an optional dependency array (deps) as its third parameter. If you don't provide this array, the handle will be recreated on every render of the parent component, which may cause unnecessary performance overhead. Here's an example:

import React, { useRef, useImperativeHandle } from 'react';

const ChildComponent = React.forwardRef((props, ref) => {
  const inputRef = useRef();

  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    },
  }));

  return <input ref={inputRef} />;
});

const ParentComponent = () => {
  const childRef = useRef();

  return (
    <div>
      <ChildComponent ref={childRef} />
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

To fix this, you should provide a dependency array that specifies when the handle should be recreated. In this example, we can use an empty array [] to indicate that the handle should only be created once, during the initial render of the parent component:

import React, { useRef, useImperativeHandle } from "react";

const ChildComponent = React.forwardRef((props, ref) => {
  const inputRef = useRef();

  useImperativeHandle(
    ref,
    () => ({
      focus: () => {
        inputRef.current.focus();
      },
    }),
    []
  ); // Empty dependency array to indicate handle should only be created once

  return <input ref={inputRef} />;
});

const ParentComponent = () => {
  const childRef = useRef();

  return (
    <div>
      <ChildComponent ref={childRef} />
    </div>
  );
};

Enter fullscreen mode Exit fullscreen mode

Mistake 3: Overusing useImperativeHandle

Another mistake that developers may make is overusing useImperativeHandle to expose too many methods or values from the child component to the parent component. This can lead to tightly coupled and hard-to-maintain code. It's important to carefully consider what methods or values should be exposed and avoid exposing unnecessary implementation details. Here's an example:

import React, { useRef, useImperativeHandle } from 'react';

const ChildComponent = React.forwardRef((props, ref) => {
  const inputRef = useRef();
  const count = useRef(0);

  useImperativeHandle(ref, () => ({
    increment: () => {
      count.current++;
    },
    decrement: () => {
      count.current--;
    },
    getCount: () => {
      return count.current;
    },
  }));

  return <input ref={inputRef} />;
});

const ParentComponent = () => {
  const childRef = useRef();

  const handleIncrementClick = () => {
    childRef.current.increment();
  };

  const handleDecrementClick = () => {
    childRef.current.decrement();
  };

  const handleGetCountClick = () => {
    alert(childRef.current.getCount());
  };

  return (
    <div>
      <ChildComponent ref={childRef} />
      <button onClick={handleIncrementClick}>Increment</button>
      <button onClick={handleDecrementClick}>Decrement</button>
      <button onClick={handleGetCountClick}>Get Count</button>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

In this example, the child component exposes three methods increment, decrement, and getCount to the parent component. However, it might be better to encapsulate the logic for incrementing and decrementing the count within the child component itself, and only expose a getCount method to the parent component. This can help to keep the code more modular and maintainable.

useLayoutEffect

useLayoutEffect is a React hook that allows you to perform side effects that depend on the layout of the component, such as measuring DOM elements or performing DOM manipulations, before the browser paints the screen. It is similar to useEffect in terms of syntax and usage, but it has a crucial difference: useLayoutEffect runs synchronously, blocking the browser from painting until the effect is complete. This makes it suitable for situations where you need to ensure that the DOM is updated before the user sees the changes.

The syntax for useLayoutEffect is as follows:

useLayoutEffect(() => {
  // Effect logic
}, deps);

Enter fullscreen mode Exit fullscreen mode

Just like useEffect, useLayoutEffect takes two arguments: a function that contains the effect logic, and an optional dependency array (deps) that specifies which values the effect depends on. The effect function will be executed every time the component renders, unless the dependencies array is provided and none of the values in the array change. If you pass an empty array ([]) as the dependencies, the effect will only run once, similar to componentDidMount in class components.

Examples of useLayoutEffect

Example 1: Measuring DOM Elements

A common use case for useLayoutEffect is measuring DOM elements, such as getting the size or position of an element, and using that information to perform further calculations or update the UI. Here's an example:

import React, { useLayoutEffect, useRef, useState } from 'react';

const ElementSize = () => {
  const [width, setWidth] = useState(0);
  const elementRef = useRef();

  useLayoutEffect(() => {
    const element = elementRef.current;
    const elementWidth = element.offsetWidth;
    setWidth(elementWidth);
  }, []);

  return (
    <div>
      <div ref={elementRef}>Element</div>
      <p>Element width: {width}px</p>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

In this example, we use useLayoutEffect to measure the width of a DOM element using the offsetWidth property, and then update the state with the measured value. Since we pass an empty dependency array [] to useLayoutEffect, the effect will only run once after the component mounts, ensuring that the width is measured and updated before the browser paints.

Example 2: Performing DOM Manipulations

Another use case for useLayoutEffect is performing DOM manipulations, such as adding or removing elements from the DOM, updating attributes, or changing styles. Here's an example:

import React, { useLayoutEffect, useRef } from "react";

const DynamicElement = () => {
  const elementRef = useRef();

  useLayoutEffect(() => {
    const element = elementRef.current;

    // Add a class to the element
    element.classList.add("dynamic-element");

    // Update the text content of the element
    element.textContent = "Dynamic Element";

    return () => {
      // Clean up the DOM manipulation when the component unmounts
      element.classList.remove("dynamic-element");
    };
  }, []);

  return (
    <div>
      <div ref={elementRef}>Element</div>
    </div>
  );
};

Enter fullscreen mode Exit fullscreen mode

In this example, we use useLayoutEffect to add a class and update the text content of a DOM element when the component mounts. We also clean up the DOM manipulation by removing the class when the component unmounts. The synchronous nature of useLayoutEffect ensures that the DOM manipulations are applied before the browser paints, avoiding any flickering or visual inconsistencies.

Common Mistakes with useLayoutEffect

While useLayoutEffect can be a powerful tool for managing side effects that depend on the layout of the component, there are some common mistakes that people may make while using it. Let's explore these mistakes with code examples:

Mistake 1: Blocking the Main Thread

Since useLayoutEffect runs synchronously and blocks the main thread until the effect is complete, it can potentially cause performance issues if the effect logic is computationally expensive or takes a long time to complete. Here's an example:

import React, { useLayoutEffect, useState } from 'react';

const BlockingEffect = () => {
  const [count, setCount] = useState(0);

  useLayoutEffect(() => {
    // Perform heavy computation that blocks the main thread
    for (let i = 0; i < 1000000000; i++) {
      // ...
    }

    setCount(1);
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

In this example, the effect logic performs a heavy computation that blocks the main thread, potentially causing the UI to freeze or become unresponsive. To avoid blocking the main thread, it's important to ensure that the effect logic is optimized and does not have a significant impact on performance.

Mistake 2: Missing Dependencies

useLayoutEffect takes an optional dependencies array (deps) that specifies which values the effect depends on. If you omit the dependencies array, the effect will run on every render, which may cause unnecessary re-renders and performance issues. Here's an example:

import React, { useLayoutEffect, useState } from 'react';

const MissingDependencies = () => {
  const [count, setCount] = useState(0);

  useLayoutEffect(() => {
    // Effect logic that depends on count
    console.log('Effect ran');
  });

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

In this example, the effect logic depends on the count state, but we forgot to include it in the dependencies array. This may result in stale data being used in the effect, leading to unexpected behavior. To fix this, we should include count in the dependencies array, like this: useLayoutEffect(() => { ... }, [count]);.

Mistake 3: Not Cleaning Up Effect

useLayoutEffect can return a cleanup function that will be called when the component unmounts or when the effect dependencies change. If you don't clean up the effect, it may cause memory leaks or unexpected behavior. Here's an example:

import React, { useLayoutEffect, useState } from "react";

const NotCleaningUpEffect = () => {
  const [count, setCount] = useState(0);

  useLayoutEffect(() => {
    // Perform effect logic
    console.log("Effect ran");

    // Forgot to clean up the effect
  }, []);
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

Enter fullscreen mode Exit fullscreen mode

In this example, we forgot to return a cleanup function from the useLayoutEffect hook. This means that the effect will not be cleaned up when the component unmounts or when the count state changes, potentially causing memory leaks or unexpected behavior. To fix this, we should return a cleanup function from the effect, like this:

useLayoutEffect(() => {
  // Perform effect logic
  console.log('Effect ran');

  return () => {
    // Clean up effect
    console.log('Effect cleaned up');
  };
}, []);
Enter fullscreen mode Exit fullscreen mode

Mistake 4: Using useLayoutEffect Unnecessarily

useLayoutEffect should be used sparingly and only when necessary, as it can cause performance issues if used improperly. In most cases, useEffect is sufficient for managing side effects in a React component. Only use useLayoutEffect when you need to perform synchronous updates that affect the layout of the component. Here's an example of using useLayoutEffect unnecessarily:

import React, { useLayoutEffect, useState } from 'react';

const UnnecessaryUseLayoutEffect = () => {
  const [count, setCount] = useState(0);

  useLayoutEffect(() => {
    // Perform effect logic
    console.log('Effect ran');

    // This effect does not require synchronous updates, so useEffect can be used instead
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

In this example, the effect does not require synchronous updates that affect the layout of the component, so useEffect should be used instead of useLayoutEffect to avoid potential performance issues.

useDebugValue

React's useDebugValue is a hook that is used to provide a label for custom hooks or components. This label is then shown in DevTools alongside the component or hook name, making it easier to identify and debug specific instances of a component or hook.

The useDebugValue hook takes two arguments: a value and a formatter function. The value is the data you want to label, and the formatter function is used to format the value for display in DevTools. The formatter function is optional and can be used to customize the way the value is displayed.

Here's the syntax for useDebugValue:

useDebugValue(value, formatter);
Enter fullscreen mode Exit fullscreen mode

Examples of useDebugValue

Example 1: Custom Hook

Let's start with an example of using useDebugValue to label a custom hook. Consider the following custom hook that manages a timer:

import { useState, useEffect, useDebugValue } from 'react';

const useTimer = () => {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const intervalId = setInterval(() => {
      setSeconds((prevSeconds) => prevSeconds + 1);
    }, 1000);

    return () => {
      clearInterval(intervalId);
    };
  }, []);

  useDebugValue(seconds, (value) => `Timer: ${value} seconds`);

  return seconds;
};
Enter fullscreen mode Exit fullscreen mode

In this example, we use useDebugValue to label the seconds state value with a custom string that includes the current value of seconds. This label will be shown in DevTools alongside the name of the custom hook, making it easier to inspect the value of seconds during debugging.

Example 2: Component

useDebugValue can also be used to label components, providing additional information in DevTools for easier debugging. Here's an example of using useDebugValue in a component:

import React, { useDebugValue, useState } from 'react';

const Counter = () => {
  const [count, setCount] = useState(0);

  useDebugValue(count, 'Counter Value');

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

In this example, we use useDebugValue to label the count state value with the string "Counter Value". This label will be shown in DevTools alongside the name of the component, making it easier to identify and inspect the value of count during debugging.

Common Mistakes with useDebugValue

While useDebugValue is a powerful tool for enhancing the debugging experience in React, there are some common mistakes that people may make while using it. Let's take a look at a few of them and provide examples of how to fix them.

Mistake 1: Not Using useDebugValue

One common mistake is simply not using useDebugValue when it could be beneficial. Forgetting to use useDebugValue can result in a lack of useful information in DevTools, making it harder to identify and debug issues with your custom hooks or components.

Here's an example of how to fix this mistake:

import { useDebugValue } from 'react';

const useCustomHook = (value) => {
  // Some logic here

  // Wrong: Not using useDebugValue
  // useDebugValue(value);

  // Correct: Using useDebugValue with a label
  useDebugValue(value, 'Custom Hook Value');

  return value;
};

Enter fullscreen mode Exit fullscreen mode

In this example, the custom hook useCustomHook was missing the use of useDebugValue, which could have provided valuable information in DevTools. By simply adding the useDebugValue hook with a label, we can now see the labeled value in DevTools alongside the name of the custom hook, making it easier to debug.

Mistake 2: Incorrectly Formatting the Value

Another mistake that can be made with useDebugValue is formatting the value incorrectly. The formatter function is optional, but if provided, it should return a string that represents the formatted value. If the formatter function is not provided or if it returns a value that is not a string, DevTools will show an empty label.

Here's an example of how to fix this mistake:

import { useDebugValue } from 'react';

const useCustomHook = (value) => {
  // Some logic here

  // Wrong: Incorrectly formatting the value
  // useDebugValue(value, (val) => val + ' custom');

  // Correct: Formatting the value as a string
  useDebugValue(value, (val) => `${val} custom`);

  return value;
};
Enter fullscreen mode Exit fullscreen mode

In this example, the original formatter function was not returning a string, resulting in an empty label in DevTools. By updating the formatter function to return a string, we can now see the formatted value in DevTools alongside the name of the custom hook.

Closing Notes:

Thank you for taking the time to read this blog post on the hooks in React. I hope you found the content informative and useful. If you're interested in learning more about full stack applications, you can also follow me on my YouTube channel, where I regularly post tutorials on building full stack projects.

Top comments (2)

Collapse
 
kristiyan_velkov profile image
Kristiyan Velkov

Usefull resource for React JS developers -> React 18 Snippets

Very good article! <3

Collapse
 
fruntend profile image
fruntend

Сongratulations 🥳! Your article hit the top posts for the week - dev.to/fruntend/top-10-posts-for-f...
Keep it up 👍