While the usage of useState may seem straightforward, it's not uncommon for developers, including experienced ones, to make mistakes when working with it. This article aims to address these common pitfalls and provide clear and practical examples for avoiding them. So let's learn how to avoid these 5 React useState blunders.
Table of contents
1. Getting Previous Value Incorrectly
When handling state with useState, understanding how to access and manage the previous state correctly is crucial to prevent errors. Let's understand first React useState blunder.
When working with setState, it's important to realize that you have the ability to access the previous state as an argument within the callback function. Neglecting to do so can lead to unexpected state updates. Let's dissect this common oversight using a classic Counter example.
import { useCallback, useState } from "react"export default function CounterComponent() {const [counter, setCounter] = useState(0)const handleInstantIncrement = useCallback(() => {setCounter(counter + 1);}, [counter]);const handleDelayedIncrement = useCallback(() => {// counter + 1 is the problem,// because the counter can be already different, when callback invokessetTimeout(() => setCounter(counter + 1), 1000)}, [counter]);return (<div><h1>{`Counter is ${counter}`}</h1>{/* This handler works just fine */}<button onClick={handleInstantIncrement}>Instant increment</button>{/* Multi-clicking that handler causes unexpected states updates */}<button onClick={handleDelayedIncrement}>Delayed increment</button></div>);}
Now, let's try using callbacks when setting state. Pay attention because it can also help us eliminate unnecessary dependencies in our useCallback. Make sure to remember this solution, as it's a question that often comes up in interviews.
Let's see the solution to avoid first React useState blunder.
import { useCallback, useState } from "react"export default function CounterComponent() {const [counter, setCounter] = useState(0)const handleInstantIncrement = useCallback(() => {setCounter((prevCounter) => prevCounter + 1)// Dependency removed}, []);const handleDelayedIncrement = useCallback(() => {// Using prev state helps us to avoid unexpected behavioursetTimeout(() => setCounter((prevCounter) => prevCounter + 1), 1000)// Dependency removed}, []);return (<div><h1>{`Counter is ${counter}`}</h1>{/* This handler works just fine */}<button onClick={handleInstantIncrement}>Instant increment</button>{/* Multi-clicking that handler causes unexpected states updates */}<button onClick={handleDelayedIncrement}>Delayed increment</button></div>);}
2. Storing Global State in useState
Learn why using useState for global state management is not the best approach and discover alternative solutions for more robust global state management. Let's understand second React useState blunder.
Think of useState as a tool best suited for storing data that's specific to a single component, such as input values or toggles within that component. On the other hand, global state pertains to the entire application and is not limited to just one component. If your data needs to be shared across multiple pages or widgets, it's a good idea to consider using a global state management solution like React Context, Redux, MobX, and others.
Let's illustrate this with an example. Although our current example is simple, imagine that we're building a much larger and more complex app. With a deep component hierarchy and the need to use user state throughout the app, it becomes practical to separate our state into a global scope. This way, we can easily access it from any part of the app without the hassle of passing props through multiple levels.
import React, { useState } from "react"// Passing propsfunction FirstName(user) {return user.firstName;}// Passing propsfunction LastName(user) {return user.lastName;}export default function UserComponent() {// User state will be used all over the app. We should replace useStateconst [user] = useState({ firstName: "Nagi", lastName: "Gajjela" });return (<><FirstName user={user} /><LastName user={user} /></>)}
Rather than using local state in this context, it would be more appropriate to opt for a global state approach. Let's modify the example by implementing React Context to achieve this.
Let's see the solution to avoid second React useState blunder.
import React, { createContext, useContext, useMemo, useState } from "react"// Created contextconst UserContext = createContext()// That component separates user context from app, so we don't pollute itfunction UserContextProvider({ children }) {const [firstName, setFirstName] = useState("Nagi");const [lastName, setLastName] = useState("Gajjela");// We want to remember value reference, otherwise we will have unnecessary rerendersconst value = useMemo(() => {return {firstName,lastName,setFirstName,setLastName};}, [name, surname]);return (<UserContext.Provider value={value}>{children}</UserContext.Provider>)}function FirstName() {const { firstName } = useContext(UserContext);return firstName}function LastName() {const { lastName } = useContext(UserContext);return lastName}export default function UserComponent() {return (<UserContextProvider><FirstName /><LastName /></UserContextProvider>);}
Now, we have the ability to effortlessly access our global state from any section of our application. This approach proves to be far more convenient and transparent compared to solely relying on pure useState.
3. Forgetting to Initialize the State
Initializing state is a fundamental step in React applications. We'll discuss why it's vital and how to do it correctly to avoid unexpected issues. Let's understand third React useState blunder.
This one is likely to result in errors when your code runs. You might have encountered this type of error before; it's often referred to as the "Can't read properties of undefined" error.
import React, { useEffect, useState } from "react"// Fetch users func. I don't handle error here, but you should always do it!async function fetchUsers() {const usersResponse = await fetch(`https://jsonplaceholder.typicode.com/users`);const users = await usersResponse.json()return users}export default function UserComponent() {// No initial state here, so users === undefined, until setUsersconst [users, setUsers] = useState()useEffect(() => {fetchUsers().then(setUsers)}, []);return (<div>{/* Error, can't read properties of undefined */}{users.map(({ id, name, username, email }) => (<div key={id}><h4>{name}</h4><h4>{username}</h4><h6>{email}</h6></div>))}</div>)}
Fixing this issue is as simple as making the mistake! We can resolve it by setting our state as an empty array. If you're unsure of an appropriate initial state, you can even use 'null' and handle it accordingly.
Let's see the solution to avoid thrid React useState blunder.
import React, { useEffect, useState } from "react"// Fetch users func. I don't handle error here, but you should always do it!async function fetchUsers() {const usersResponse = await fetch(`https://jsonplaceholder.typicode.com/users`);const users = await usersResponse.json()return users}export default function UserComponent() {// If it doesn't cause errors in your case, it's still a good tone to always initialize it (even with null)const [users, setUsers] = useState([])useEffect(() => {fetchUsers().then(setUsers)}, []);// You can also add that check// if (users.length === 0) return <Loading />return (<div>{users.map(({ id, name, username, email }) => (<div key={id}><h4>{name}</h4><h4>{username}</h4><h6>{email}</h6></div>))}</div>)}
4. Mutating State Instead of Returning a New One
One common pitfall is directly modifying state when you should create a new state object. We'll delve into this issue and how to work with immutable state. Let's understand fourth React useState blunder.
You mustn’t mutate React state ever in your life! React performs a series of intelligent and critical operations when state changes, and it relies on shallow comparisons (comparing references, not values) to do so.
import { useCallback, useState } from "react"export default function userComponent() {// Initialize Stateconst [userInfo, setUserInfo] = useState({firstName: "Nagi",lastName: "Gajjela"})// field is either firstName or lastNameconst handleChangeInfo = useCallback((field) => {// e is input onChange eventreturn (e) => {setUserInfo((prev) => {// Here we are mutating prev state.// That simply won't work as React doesn't recognise the changeprev[field] = e.target.value;return prev;});};}, [])return (<div><h2>{`First Name = ${userInfo.firstName}`}</h2><h2>{`Last Name = ${userInfo.lastName}`}</h2><input value={userInfo.firstName} onChange={handleChangeInfo("firstName")} /><input value={userInfo.lastName} onChange={handleChangeInfo("lastName")} /></div>)}
The solution is pretty straightforward. We should avoid mutating state and simply return a new state. Let's see the solution to avoid fourth React useState blunder.
import { useCallback, useState } from "react"export default function userComponent() {// Initialize Stateconst [userInfo, setUserInfo] = useState({firstName: "Nagi",lastName: "Gajjela"})// field is either firstName or lastNameconst handleChangeInfo = useCallback((field) => {// e is input onChange eventreturn (e) => {// Now it works!setUserInfo((prev) => ({// So when we update firstName, lastName stays in state and vice versa...prev, [field]: e.target.value}))}}, [])return (<div><h2>{`First Name = ${userInfo.firstName}`}</h2><h2>{`Last Name = ${userInfo.lastName}`}</h2><input value={userInfo.firstName} onChange={handleChangeInfo("firstName")} /><input value={userInfo.lastName} onChange={handleChangeInfo("lastName")} /></div>)}
5. Hooks Logic Copy-Paste, Forgetting to Compose Them
As your React application grows, it's easy to fall into the trap of copy-pasting hooks logic. We'll explore why this is problematic and how to properly compose hooks for maintainability. Let's understand last React useState blunder.
All React hooks can be mixed and matched to package specific functionalities. This flexibility enables you to create custom hooks tailored to your needs, which you can then easily employ across your application.
Consider the example below. Doesn't it seem a bit repetitive for such a straightforward piece of logic?
import React, { useCallback, useState } from "react"export default function UserComponent() {const [firstName, setFirstName] = useState("")const [lastName, setLastName] = useState("")const handleFirstNameChange = useCallback((e) => {setFirstName(e.target.value);}, [])const handleLastNameChange = useCallback((e) => {setLastName(e.target.value);}, [])return (<div><input value={firstName} onChange={handleFirstNameChange} /><input value={lastName} onChange={handleLastNameChange} /></div>)}
How can we make our code simpler? Essentially, we're doing two similar things here: defining local state and managing the onChange event. These actions can be neatly separated into a custom hook, which we'll name 'useInput'!
Let's see the solution to avoid last React useState blunder.
import React, { useCallback, useState } from "react";function useInput(defaultValue = "") {// We declare this state only once!const [value, setValue] = useState(defaultValue);// We write this handler only once!const handleChange = useCallback((e) => {setValue(e.target.value);}, []);// Cases when we need setValue are also possiblereturn [value, handleChange, setValue];}export default function App() {const [firstName, onChangeFirstName] = useInput("Nagi");const [lastName, onChangeLastName] = useInput("Gajjela");return (<div><input value={firstName} onChange={onChangeFirstName} /><input value={lastName} onChange={onChangeLastName} /></div>);}
We've isolated the input logic within a dedicated hook, making it much easier to work with. React hooks are a tremendously potent tool, so make sure you take full advantage of them!
6. Resources
Here, you'll find a collection of valuable resources and references to further enhance your understanding of using React hooks effectively.
0 Comments