Telerik blogs

Improper use of React Hooks can cause problems with performance and bugs. Learn from some common mistakes developers make with React Hooks and solutions for how best to avoid them.

React Hooks have revolutionized the React ecosystem since their inception in React 16.8. They offer the ability to leverage state and lifecycle features inside functional components, which was previously only possible in class components.

Earlier on, we’ve written some deep dives into some of the core React Hooks like the useEffect, useContext and useReducer hooks.

Despite the utility and usefulness React Hooks provide us, improper use can lead to performance degradation and elusive bugs. In this article, we’ll outline some common mistakes developers make when using React Hooks and solutions on how best to avoid them.

1. Using Hooks Inside Conditional Statements

It’s a common error to use hooks inside loops, conditional statements or nested functions. This can lead to inconsistent hook calls between renders, resulting in erratic behaviors and hard-to-find bugs.

Incorrect Usage

function Component() {
  if (condition) {
    const [state, setState] = useState(initialState);
  }
  // ...
}

Hooks must always be invoked in the same order during every component render. This helps promotes more predictable and stable component behavior.

Correct Usage

function Component() {
  const [state, setState] = useState(initialState);
  if (condition) {
    // manipulate or use the state here.
  }
  // ...
}

2. Overusing State

Over-reliance on state is another frequent pitfall. Not every variable in a component needs to be part of component state, especially if it doesn’t trigger a re-render. By distinguishing between stateful and non-stateful data, we can optimize our component’s performance by reducing unnecessary re-renders.

// unneccessary if we don't need a re-render when value changes
const [value, setValue] = useState("");

// instead, we can just assign a variable
let value = "";

3. Mutating State Directly

React state is inherently immutable, and direct mutation is an error sometimes made by developers.

Incorrect Usage

const [state, setState] = useState([]);
state.push("new item"); // Incorrect!

Direct state mutation doesn’t immediately trigger a re-render, causing a discrepancy between the displayed state and the actual state of the component.

Instead, we should always use the state update function available to use from the useState hook to update state. When state changes this way, it triggers a necessary re-render which ensures consistency between the component’s actual state and its rendered output.

const [state, setState] = useState([]);
setState((prevState) => [...prevState, "new item"]); // Correct!

4. Using Stale State Data

A common pitfall when using React Hooks is the misuse of stale state data. This can occur when we directly reference the state variable in consecutive state updates. Because state updates may be asynchronous, the state variable might not reflect the latest value when it’s referenced in successive calls.

Incorrect Usage

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

setCount(count + 1);
setCount(count + 1); // Incorrect! `count` could be stale.

In the incorrect usage above, count is referenced directly within each setCount() call. If these state updates are batched (as they often are in event handlers and lifecycle methods), then both calls to setCount() will use the same initial value of count, leading to an incorrect final state.

Instead, we can use the updater function form of setCount(), which ensures that each update is based on the latest state. The updater function takes the previous state as an argument and returns the new state, so each consecutive update will have the correct value, preventing stale state data issues.

Correct Usage

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

setCount((prevCount) => prevCount + 1);
setCount((prevCount) => prevCount + 1); // Correct! Using the updater function.

5. Running an Effect for Every Update

The useEffect hook is often misused to run on every component update. Although this is necessary in some scenarios, often we only need to run an effect once on mount, or when specific dependencies change.

Incorrect Usage

useEffect(() => {
  fetch("https://example.com/api").then(/* ... */);
}); // Will run every render

In the example above, running the effect on every render will result in numerous API calls which could lead to performance degradation and inconsistent state.

By supplying an empty dependency array, we can ensure the effect runs only once on mount, similar to the traditional componentDidMount lifecycle method in class components.

Correct Usage

useEffect(() => {
  fetch("https://example.com/api").then(/* ... */);
}, []); // Runs once on mount

5. Not Clearing Side Effects

Side effects are operations that interact with the outside of a function scope, affecting the external environment. This could involve data fetching, subscriptions, manual DOM manipulations, and timers like setTimeout and setInterval.

Failing to clear side effects in the useEffect hook is another common error that can occur that can lead to unexpected behavior and/or memory leaks.

Incorrect Usage

useEffect(() => {
  const timer = setTimeout(() => {
    // do something
  }, 1000);
  // Missing cleanup!
});

By returning a cleanup function from our effect, we can remove side effects before the component unmounts or before the effect runs again, preventing memory leaks.

Correct Usage

useEffect(() => {
  const timer = setTimeout(() => {
    // do something
  }, 1000);

  // cleanup function to remove the side effect
  return () => {
    clearTimeout(timer);
  };
});

6. Omitting Dependencies in the Dependency Array

A common mistake is neglecting to include dependencies in the dependency array of useEffect, useCallback or useMemo hooks. If a variable from the component scope is used within these hooks’ callbacks, it should often be included within the dependency array.

Incorrect Usage

function Component({ prop }) {
  useEffect(() => {
    console.log(prop);
  }, []); // Missing dependency: 'prop'
  // ...
}

Omitting dependencies can lead to the capture of stale variables that may have changed since the effect was last run, which can lead to unpredictable behavior.

Correct Usage

function Component({ prop }) {
  useEffect(() => {
    console.log(prop);
  }, [prop]); // Correct dependency array
  // ...
}

By correctly declaring all dependencies, we ensure that the hook updates whenever a dependency changes. This results in consistent and expected behavior.

7. Forgetting to Memoize Expensive Function Calls

Memoization is an optimization technique that primarily speeds up applications by storing the results of expensive function calls and reusing the cached result when the same inputs occur again. This technique is incredibly useful for functions that are computationally intensive and are frequently called with the same arguments.

React provides two hooks, useMemo and useCallback, that implement memoization. useMemo is used for memoizing the return value of a function, while useCallback is used for memoizing the instance of the function itself.

If expensive functions are not correctly memoized, they can provoke unnecessary re-renders. In the following example, if the parent component re-renders for any reason, expensiveFunction() will be recreated. This will cause unnecessary re-renders in the child component, as it will receive a new prop each time, regardless of whether the actual computation result has changed.

Incorrect Usage

function Component({ prop }) {
  const expensiveFunction = () => {
    // Expensive computation
  };

  return <ChildComponent func={expensiveFunction} />;
}

Correct Usage

We can optimize this by using the useCallback hook:

function Component({ prop }) {
  const expensiveFunction = useCallback(
    () => {
      // Expensive computation
    },
    [
      /* dependencies */
    ]
  );

  return <OtherComponent func={expensiveFunction} />;
}

In this correct usage, the useCallback hook ensures that expensiveFunction() is only recreated when its dependencies change. This means that the child component will only re-render when the computed value changes, preventing unnecessary re-renders and enhancing the performance of the React application.

Wrap-up

React Hooks have significantly transformed the React landscape by allowing the use of state and lifecycle features in functional components. However, they can also present unique challenges.

Errors such as using hooks within conditional statements, overusing state, mutating state directly, misusing stale state data, etc. can lead to inefficient code and difficult-to-trace bugs. By avoiding these common mistakes and following best practices, developers can fully harness the potential of React Hooks, leading to more performant, stable and maintainable React applications.


About the Author

Hassan Djirdeh

Hassan is currently a senior frontend engineer at Doordash. Prior to Doordash, Hassan worked at Instacart and Shopify, where he helped build large production applications at-scale. Hassan is also a published author and course instructor and has helped thousands of students learn in-depth fronted engineering tools like React, Vue, TypeScript and GraphQL. Hassan’s non-work interests range widely and, when not in front of a computer screen, you can find him at the gym, going for walks or running through the six.

Related Posts

Comments

Comments are disabled in preview mode.