9 min read react

Common React Hooks Mistakes

Common React Hooks Mistakes and how to avoid them with examples of bad and good code.

What are React Hooks in short?

React Hooks are a new feature which established in React 16.8.0 that allows you to use state and other React features without writing a class. You can read more about them in the official documentation. Hooks can only be called at the top level of your component. You can’t call them inside a conditional, an if statement, or any other place that is not at the top level.

What are the most common mistakes?

1. Using state when you do not need it

The first mistake is to use the state when you do not need it. For example, if you have a component that only renders a list of items, you do not need to use state. You can just use props to pass the items to the component.

// Bad
const MyComponent = () => {
  const [items, setItems] = useState([]);
  return (
    <ul>
      {items.map(item => <li>{item}</li>)}
    </ul>
  );
}

// Good
const MyComponent = ({ items }) => {
  return (
    <ul>
      {items.map(item => <li>{item}</li>)}
    </ul>
  );
}

Or if you have a form that only should have interaction on submitting the form, you do not need to use the state. You can just use refs to get the values of the inputs.

// Bad
const MyComponent = () => {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [message, setMessage] = useState('');
  const handleSubmit = () => {
    // send the data to the server
  }
  return (
    <form onSubmit={handleSubmit}>
      <input type="text" value={name} onChange={e => setName(e.target.value)} />
      <input type="email" value={email} onChange={e => setEmail(e.target.value)} />
      <textarea value={message} onChange={e => setMessage(e.target.value)} />
      <button type="submit">Submit</button>
    </form>
  );
}

// Good
const MyComponent = () => {
  const nameRef = useRef();
  const emailRef = useRef();
  const messageRef = useRef();
  const handleSubmit = () => {
    // send the data to the server
    // nameRef.current.value
    // emailRef.current.value
    // messageRef.current.value
  }
  return (
    <form onSubmit={handleSubmit}>
      <input type="text" ref={nameRef} />
      <input type="email" ref={emailRef} />
      <textarea ref={messageRef} />
      <button type="submit">Submit</button>
    </form>
  );
}

2. Not paying attention to reference equality in useEffect

When you use useEffect, you should pay attention to reference equality. If you pass an array of dependencies to the useEffect, it will only run if one of the dependencies has changed. If you pass an object as a dependency, it will always be different from the previous render, so the effect will run every time. If you want to compare the object, you can use a custom hook like useDeepCompareEffect or useDeepCompareMemoize.

// Bad
const MyComponent = () => {
  const [state, setState] = useState({ a: 1, b: 2 });

  useEffect(() => {
    // do something
    // it will run every time
  }, [state]);
}; 

// Good
import { useDeepCompareEffect } from 'use-deep-compare-effect';

const MyComponent = () => {
  const [state, setState] = useState({ a: 1, b: 2 });

  useDeepCompareEffect(() => {
    // do something
  }, [state]);
};

// Good
import { useDeepCompareMemoize } from 'use-deep-compare-memoize';
const MyComponent = () => {
  const [state, setState] = useState({ a: 1, b: 2 });

  useEffect(() => {
  // do something
  }, [useDeepCompareMemoize(state)]);
};

// Good
const MyComponent = () => {
  const [state, setState] = useState({ a: 1, b: 2 });

  useEffect(() => {
  // do something
  }, [JSON.stringify(state)]);
};

3. Using the wrong dependency array

The dependency array is used to specify which values should be watched for changes. If you don’t specify a dependency array, the effect will run every time the component renders. If you specify an empty dependency array, the effect will only run once when the component mounts. If you specify a dependency array with values, the effect will run every time the component renders, and one of the values in the dependency array changes. You can read more about it in the official documentation.

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

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

  // Bad
   useEffect(() => {
    console.log('count changed');
  }); // it will run every time the component renders

  // Good
  useEffect(() => {
    console.log('count changed');
  }, [count]);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(prevCount => prevCount + 1)}>
        Click me
      </button>
    </div>
  );
};

4. Not using const for the combination of states values

Storing the combination of state values in a variable is a common practice and it helps when we do not need another state for the same repeated values and it will reduce re-renders. It is also a good practice to use const for this variable. This way, you will be sure that the variable will not be changed by mistake.

// Bad
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState(firstName + ' ' + lastName);

// Good
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const fullName = `${firstName} ${lastName}`;

6. Using the setState to store the combination of other states as an object

Storing an object in the state is a common practice. However, it is not a good idea to store the combination of other states in the state. For example, if you have a state called user which contains name and age properties, you should not store setNotRelatedKey in the state. Instead, you should store name and age separately in the state. This is because, when you update the state, React will re-render the component. If you store setNotRelatedKey in the state, React will re-render the component even if you only update name or age. This is because React will compare the previous state with the new state. If the previous state is different from the new state, React will re-render the component. If you store name and age separately in the state, React will only re-render the component if you update name or age.

// Bad
const [user, setUser] = useState({
  name: '',
  age: 0,
  notRelatedKey: '',
});

const handleChange = (e) => {
  const { name, value } = e.target;
  setUser((prevState) => ({
    ...prevState,
    [name]: value,
  }));
}; 

// Good
const [name, setName] = useState('');
const [age, setAge] = useState(0);
const [notRelatedKey, setNotRelatedKey] = useState('');

const handleChange = (e) => {
  const { name, value } = e.target;
  switch (name) {
    case 'name':
      setName(value);
      break;
    case 'notRelatedKey':
      setNotRelatedKey(value);
      break;
    case 'age':
      setAge(value);
      break;
    default:
      break;
  }
};

5. Not using useEffect to check state changes

The useEffect hook is used to perform side effects in your component. It is a combination of componentDidMount, componentDidUpdate, and componentWillUnmount. It is a good practice to use useEffect to check state changes. You can read more about it in the official documentation.

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

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

  // Bad
  console.log('count changed to', count);

  // Good 
  useEffect(() => {
    console.log('count changed to', count);
  }, [count]);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
};

6. Not using useCallback to memoize functions

The useCallback hook is used to memoize functions. It is a good practice to use useCallback to memoize functions. You can read more about it in the official documentation.

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

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

  // Bad
  const increment = () => {
    setCount(count + 1);
  };

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

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={increment}>
        Click me
      </button>
    </div>
  );
};

Make sure to use useCallback when you pass a function to a child component.

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

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


  // Bad
  const increment = () => {
    setCount(count + 1);
  };

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

  return (
    <div>
      <p>You clicked {count} times</p>
      <ChildComponent increment={increment} />
    </div>
  );
};

7. Not using useMemo to memoize values

The useMemo hook is used to memoize values. It is a good practice to use useMemo to memoize values. You can read more about it in the official documentation. Make sure to use useMemo only when you need to memoize values. It is not a good practice to use useMemo to memoize functions.

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

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

  // Good (memoize values)
  const countValue = useMemo(() => {
    return count + 1;
  }, [count]);

  return (
    <div>
      <p>You clicked {count} times</p>
      <p>Count value is {countValue}</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
};

8. Not using useState with current state

When you want to update the state, you should always use the current state. If you don’t, you might end up in a smelly state.

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

// Bad
setCount(count + 1);

// Good
setCount((prevCount) => prevCount + 1);

When you update the state, React will batch the updates and apply them all at once. This means that if you update the state multiple times in a single render, the state will only be updated once. This is why you should not rely on the previous state when updating the state.

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

// Bad
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);

// Good
setCount((prevCount) => prevCount + 1);
setCount((prevCount) => prevCount + 1);
setCount((prevCount) => prevCount + 1);

9. Using Hooks inside a loop

This is a very common mistake. You want to use a hook inside a loop, so you do something like this:

for (let i = 0; i < 10; i++) {
  const [state, setState] = useState(0);
}

10. Using Hooks inside a callback

This is another frequent mistake. You want to use a hook inside a callback, so you do something like this:

const handleClick = () => {
  const [state, setState] = useState(0);
};

11. Using Hooks inside a conditional

This is another basic common mistake. You want to use a hook inside a conditional, so you do something like this:

if (true) {
  const [state, setState] = useState(0);
}

12. Using Hooks inside a function

This is another common mistake. You want to use a hook inside a function, so you do something like this:

function handleClick() {
  const [state, setState] = useState(0);
}

[Top]

Read Next

Post image for Dropdowns in React: Unveiling the Compound Component Pattern
A deep dive into creating a feature-rich React dropdown menu, enhanced with keyboard navigation and the Compound Component Pattern for improved accessibility and maintainability.
Post image for Advanced Component Composition in React
Explore the art of component composition in React, showcasing how it can elegantly handle complex scenarios like theme management in a project management app's interactive dashboard.