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;
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])
}
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])
}
This article is based on this youtube video. https://www.youtube.com/watch?v=GGo3MVBFr1A
Top comments (18)
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.
yes, I added code snippet.
Thanks for the post.
You can also remove the "import useRef" from that snippet.
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.
it seems like you forgot to add the abort signal to the request in the last example :-?
I fixed it, thanks.
This is really help me, thanks man
bookmarked
Nice code snippets
A good read, will save this for later
Thanks for this very Informative write-up, Man!!.
Solid article!
This is spectacular information, especially the first tip