• Home
  • Top 6 React Hook Mistakes Beginners Make

The hardest part about learning react is not actually learning how to use react but instead learning how to write good clean react code.

In this article, I will talk about 6 mistakes that I see almost everyone making with the useState and useEffect hook.

Mistake 1, Using state when you don’t need it

The very first mistake that I want to talk about is using state when you don’t actually need any state.

Let’s take a look at this example.

import {useState} from "react";

const App = () => {

  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')

  function onSubmit(e) {
    e.preventDefault();
    console.log({ email, password });
  }

  return (
      <form onSubmit={onSubmit}>
        <label htmlFor="email">Email</label>
        <input
            value={email}
            onChange={e => setEmail(e.target.value)}
            type="email"
            id="email"
        />
        <label htmlFor="password">Password</label>
        <input
            value={password}
            onChange={e => setPassword(e.target.value)}
            type="password"
            id="password"
        />
        <button type="submit">Submit</button>
      </form>
  )

};

export default App;

We use email and password state here, but the problem is we only care the email and password value when submit the form, we don’t need re-render when email and password updated, so instead of tracking the state, and re-render every time I typed a character, I am going to store these inside of a ref.

import { useRef } from "react";

const App = () => {

  const emailRef = useRef();
  const passwordRef = useRef();

  function onSubmit(e) {
    e.preventDefault();
    console.log({
      email: emailRef.current.value,
      password: passwordRef.current.value
    });
  }

  return (
      <form onSubmit={onSubmit}>
        <label htmlFor="email">Email</label>
        <input
            ref={emailRef}
            type="email"
            id="email"
        />
        <label htmlFor="password">Password</label>
        <input
            ref={passwordRef}
            type="password"
            id="password"
        />
        <button type="submit">Submit</button>
      </form>
  )

};

export default App;

If you click the submit, and look at the console again, it will print the values. You don’t need any state at all for this.

So the first tip for you is thinking do you really need to use states, and re-render the components every time state changes, or can you use a ref if you don’t need re-render the components.

Also you can access the data in the form directly, so you don’t need refs too.

import { useRef } from "react";

const App = () => {

    function onSubmit(event) {
        event.preventDefault();
        const data = new FormData(event.target);
        console.log(data.get('email'));
        console.log(data.get('password'));
        fetch('/api/form-submit-url', {
            method: 'POST',
            body: data,
        });
    }

    return (
        <form onSubmit={onSubmit}>
            <label htmlFor="email">Email</label>
            <input
                type="email"
                name="email"
                id="email"
            />
            <label htmlFor="password">Password</label>
            <input
                type="password"
                name="password"
                id="password"
            />
            <button type="submit">Submit</button>
        </form>
    )

};

export default App;

Mistake 2, Not using the function version of useState

Let’s take a look at this example.

import { useState } from "react";

export function Counter() {

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

    function adjustCount(amount) {
        setCount(count + amount);
        setCount(count + amount);
    }

    return (
        <>
            <button onClick={ () => adjustCount(-1) }> - </button>
            <span> {count} </span>
            <button onClick={ () => adjustCount(1) }> + </button>
        </>
    )
}

If you click the button, the count will update only once, even you setCount twice. This is because when the second setCount trigger, the first setCount not finished, the count value is not updated.

You should use function version to fix this problem.

https://legacy.reactjs.org/docs/hooks-reference.html#functional-updates
import { useState } from "react";

export function Counter() {

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

    function adjustCount(amount) {
        setCount(prevCount => prevCount + amount);
        setCount(prevCount => prevCount + amount);
    }

    return (
        <>
            <button onClick={ () => adjustCount(-1) }> - </button>
            <span> {count} </span>
            <button onClick={ () => adjustCount(1) }> + </button>
        </>
    )

}

Mistake 3, State dose not update immediately

Let’s take a look at this example.

import { useState } from "react";

export function Counter() {

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

    function adjustCount(amount) {
        setCount(prevCount => prevCount + amount);
        // count is the value before setCount
        console.log(count);
    }

    return (
        <>
            <button onClick={ () => adjustCount(-1) }> - </button>
            <span> {count} </span>
            <button onClick={ () => adjustCount(1) }> + </button>
        </>
    )
}

When you update your state variable, it actually doesn’t change right away, it doesn’t change until next render, so instead of putting code after your state setter, you should use useEffect.

import {useEffect, useState} from "react";

export function Counter() {

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

    useEffect(() => {
        console.log(count);
    }, [count])

    function adjustCount(amount) {
        setCount(prevCount => prevCount + amount);
    }

    return (
        <>
            <button onClick={ () => adjustCount(-1) }> - </button>
            <span> {count} </span>
            <button onClick={ () => adjustCount(1) }> + </button>
        </>
    )

}

Mistake 4, Unnecessary useEffect

Let’s take a look at this example.

import { useEffect, useState } from "react";

const App = () => {

    const [firstName, setFirstName] = useState('')
    const [lastName, setLastName] = useState('')
    const [fullName, setFullName] = useState('')

    useEffect(() => {
        setFullName(`${firstName} ${lastName}`);
    }, [firstName, lastName])

    return (
        <>
            <input value={firstName} onChange={ e => setFirstName(e.target.value) } />
            <input value={lastName} onChange={ e => setLastName(e.target.value) } />
            {fullName}
        </>
    )

};

export default App;

The problem is when we update firstName or lastName, the state fullName will update and re-render component again, it re-render twice.

How to make it to be optimal.

import { useState } from "react";

const App = () => {

    const [firstName, setFirstName] = useState('');
    const [lastName, setLastName] = useState('');

    const fullName = `${firstName} ${lastName}`;

    return (
        <>
            <input value={firstName} onChange={ e => setFirstName(e.target.value) } />
            <input value={lastName} onChange={ e => setLastName(e.target.value) } />
            {fullName}
        </>
    )

};

export default App;

Mistake 5, Referential equality mistakes

Let’s take a look at this example.

import {useEffect, useState} from "react";

const App = () => {

    const [age, setAge] = useState(0);
    const [name, setName] = useState('');
    const [darkMode, setDarkMode] = useState(false);

    const person = { age, name };

    useEffect(() => {
        console.log(person)
    }, [person])

    return (
        <div style={{ background: darkMode ? "#333" : "#fff" }}>
            Age: {" "}
            <input value={age} type="number" onChange={ e => setAge(e.target.value)} />
            <br/>
            Name: <input value={name} onChange={ e => setName(e.target.value) } />
            <br/>
            Dark Mode: {" "}
            <input type="checkbox" value={darkMode} onChange={e => setDarkMode(e.target.checked)} />
        </div>
    )
};

export default App;

If you change age or name, useEffect function will be triggered, but if you toggle darkMode, the useEffect function triggered too, this is not what we expected.

There are two key point make this happen.

Firstly, Each render has its own props and state, so the variable person will be initialize as a new one. Checkout this article, if you want to study this in detail, https://overreacted.io/a-complete-guide-to-useeffect/

The second point is Referential equality, https://barker.codes/blog/referential-equality-in-javascript/
so the variable person is not equal when darkMode updated.

We can use useMemo to fix this problem.

import {useEffect, useMemo, useState} from "react";

const App = () => {

    const [age, setAge] = useState(0);
    const [name, setName] = useState('');
    const [darkMode, setDarkMode] = useState(false);

    const person = useMemo(() => {
        return { age, name }
    }, [age, name])

    useEffect(() => {
        console.log(person)
    }, [person])

    return (
        <div style={{ background: darkMode ? "#333" : "#fff" }}>
            Age: {" "}
            <input value={age} type="number" onChange={ e => setAge(e.target.value)} />
            <br/>
            Name: <input value={name} onChange={ e => setName(e.target.value) } />
            <br/>
            Dark Mode: {" "}
            <input type="checkbox" value={darkMode} onChange={e => setDarkMode(e.target.checked)} />
        </div>
    )
};

export default App;

Mistake 6, Not aborting fetch requests

Let’s take a look at this example.

import {useEffect, useState} from "react";

export function useFetch(url) {

    const [loading, setLoading] = useState(true);
    const [data, setData] = useState();
    const [error, setError] = useState();

    useEffect(() => {
        setLoading(true)
        fetch(url)
            .then(setData)
            .catch(setError)
            .finally(() => setLoading(false))
    }, [url])

}

The problem in this example is when the component unmounted, or url changed, the previous fetch still working in the background, it will work not as expect in many situation, checkout out this article in detail, https://plainenglish.io/community/how-to-cancel-fetch-and-axios-requests-in-react-useeffect-hook

We can fix this problem using AbortController.

import {useEffect, useState} from "react";

export function useFetch(url) {

    const [loading, setLoading] = useState(true);
    const [data, setData] = useState();
    const [error, setError] = useState();

    useEffect(() => {
        const controller = new AbortController();
        setLoading(true)
        fetch(url, { signal: controller.signal })
            .then(setData)
            .catch(setError)
            .finally(() => setLoading(false))

        return () => {
            controller.abort();
        }

    }, [url])

}

Author: Muhammad Talha Waseem

Leave Comment