DEV Community

Mark
Mark

Posted on • Edited on

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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>
        </>
    )
}
Enter fullscreen mode Exit fullscreen mode

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>
        </>
    )

}
Enter fullscreen mode Exit fullscreen mode

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>
        </>
    )
}
Enter fullscreen mode Exit fullscreen mode

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>
        </>
    )

}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Please note, you can use React Compiler instead of useMemo in React 19 version.

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])

}
Enter fullscreen mode Exit fullscreen mode

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])

}
Enter fullscreen mode Exit fullscreen mode

This article is based on this youtube video. https://www.youtube.com/watch?v=GGo3MVBFr1A

Top comments (18)

Collapse
 
brense profile image
Rense Bakker

You don't need refs for handling form inputs either. You can access the input values directly from the form data in the form onSubmit or onChange event.

Collapse
 
markliu2013 profile image
Mark

yes, I added code snippet.

Collapse
 
avivrosental profile image
Aviv Rosental

Thanks for the post.
You can also remove the "import useRef" from that snippet.

Collapse
 
array_dot_reduce profile image
Bhaskar Ghale

Using refs instead of state when you don't need to re-render is a great one.
But the example could be something else to be clearer imo.

Collapse
 
nhanluong profile image
Luong Nhan • Edited

it seems like you forgot to add the abort signal to the request in the last example :-?

Collapse
 
markliu2013 profile image
Mark

I fixed it, thanks.

Collapse
 
sluxzer profile image
sluxzer

This is really help me, thanks man

Collapse
 
anhngzv profile image
Anh

bookmarked

Collapse
 
mishmanners profile image
Michelle Duke

Nice code snippets

Collapse
 
wintersunset95 profile image
Winter

A good read, will save this for later

Collapse
 
ihssmaheel profile image
Mohamed Ismail

Thanks for this very Informative write-up, Man!!.

Collapse
 
trisogene profile image
Daniel Dallimore Mallaby

Solid article!

Collapse
 
devpaul3000 profile image
Dev Paul

This is spectacular information, especially the first tip