DEV Community

Cover image for Top 15 React useState Mistakes Every Developer Should Know
Yogesh Chavan
Yogesh Chavan

Posted on โ€ข Edited on

16 1 1 1 2

Top 15 React useState Mistakes Every Developer Should Know

The useState hook is one of the most commonly used hook in React, but there are a few subtle mistakes that developers often make.

In this article, you will learn how to avoid them and write better state logic.

Limited Time Offer: Get Lifetime access to ALL My Current + Future Courses & Ebooks At Just $๐Ÿญ๐Ÿต / โ‚น๐Ÿญ๐Ÿฑ๐Ÿฎ๐Ÿฌ (instead of $๐Ÿญ๐Ÿณ๐Ÿฒ / โ‚น๐Ÿญ๐Ÿฐ๐Ÿฌ๐Ÿด๐Ÿฌ)


Mistake 1: Assuming setState Updates State Immediately

Reactโ€™s setState function is asynchronous, meaning the state doesnโ€™t update right away. If you try to use the updated state immediately after calling setState, youโ€™ll still get the old value.

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

function increment() {
  setCount(count + 1);
  setCount(count + 1); // count is still 0 and not incremented to 1
}
Enter fullscreen mode Exit fullscreen mode

Solution: Use the Functional Update Form

When the new state depends on the previous state, use the functional form of setState.

function increment() {
  setCount(prevCount => prevCount + 1);   
  setCount(prevCount => prevCount + 1); // Always works reliably
}
Enter fullscreen mode Exit fullscreen mode

Mistake 2: Using Objects Without Merging State

When working with objects in useState, React doesnโ€™t automatically merge the new state with the old state like this.setState does in class components. In functional components, you need to manually merge the object.

const [user, setUser] = useState({ name: 'John', age: 30 });

function updateAge() {
  setUser({ age: 31 }); // Overwrites the entire user state, removing `name`
}
Enter fullscreen mode Exit fullscreen mode

Solution: Spread the Previous State

Always spread the previous state to preserve existing properties.

function updateAge() {
  setUser(prevUser => ({ ...prevUser, age: 31 })); // Keeps `name` intact
}
Enter fullscreen mode Exit fullscreen mode

Mistake 3: Using Stale State in Event Handlers

If your event handler relies on the current state (e.g., inside a loop or async function), you might accidentally use stale state values.

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

function handleClick() {
  setTimeout(() => {
    setCount(count + 1); // Uses the stale `count` value
  }, 1000);
}
Enter fullscreen mode Exit fullscreen mode

Solution: Use the Functional Update Form

To ensure you always have the latest state, use the functional update form.

function handleClick() {
  setTimeout(() => {
    setCount(prevCount => prevCount + 1); // Always gets the latest value
  }, 1000);
}
Enter fullscreen mode Exit fullscreen mode

Mistake 4: Using Derived State In Component

Instead of storing derived values (like computed properties) in state, calculate them on the fly. This reduces unnecessary re-renders and keeps your state logic simple.

const [firstName, setFirstName] = useState('John');
const [lastName, setLastName] = useState('Doe');
const [fullName, setFullName] = useState(`${firstName} ${lastName}`); // Derived state

function updateFirstName(name) {
  setFirstName(name);
  setFullName(`${name} ${lastName}`); // Extra work to keep `fullName` in sync
}
Enter fullscreen mode Exit fullscreen mode

Solution: Compute Derived Values Directly

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

// Calculate fullName dynamically
const fullName = `${firstName} ${lastName}`;
Enter fullscreen mode Exit fullscreen mode

Mistake 5: Not Using Lazy Initialization for Expensive State Calculations

If your initial state involves an expensive computation (e.g., fetching data, parsing JSON, or calculating values), you can use a function to initialize the state lazily. This ensures the computation only happens once, improving performance.

const [data, setData] = useState(expensiveComputation()); // Runs every re-render of component
Enter fullscreen mode Exit fullscreen mode

Solution: Use Lazy Initialization

const [data, setData] = useState(() => expensiveComputation()); // Runs only once first time component is rendered
Enter fullscreen mode Exit fullscreen mode

This is especially useful when dealing with large datasets or complex logic during initialization.


Mistake 6: Manually Resetting Each State For Full Reset

When toggling between components or resetting forms, you might want to reset the state to its initial value.

Instead of manually resetting each piece of state, you can use a key prop to force React to unmount and remount the component.

const [inputValue, setInputValue] = useState('');

function resetForm() {
  setInputValue(''); // Manually reset each piece of state
}
Enter fullscreen mode Exit fullscreen mode

Solution: Automatic Reset with key

By changing the key prop, React will unmount and remount the component, resetting all state automatically.

Whenever you pass a different value for key prop for a component, the component will get re-created.

function App() {
  const [formKey, setFormKey] = useState(0);

  function resetForm() {
    setFormKey(prevKey => prevKey + 1); // Forces a reset
  }

  return (
    <div>
      <MyForm key={formKey} />
      <button onClick={resetForm}>Reset Form</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Mistake 7: Not Using Arrays or Objects for Related State

Instead of creating multiple useState hooks for related pieces of state, group them into a single object or array. This makes your code more organized and easier to manage.

const [x, setX] = useState(0);
const [y, setY] = useState(0);

function updateCoordinates(newX, newY) {
  setX(newX);
  setY(newY);
}
Enter fullscreen mode Exit fullscreen mode

Solution: Using Combined State

const [coordinates, setCoordinates] = useState({ x: 0, y: 0 });

function updateCoordinates(newX, newY) {
  setCoordinates({ x: newX, y: newY });
}
Enter fullscreen mode Exit fullscreen mode

Mistake 8: Not Using useReducer Hook For Complex Logic

For complex state transitions (e.g., multiple related states or conditional updates), consider combining useState with useReducer. This hybrid approach keeps your code modular and readable.

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

async function fetchData() {
  setLoading(true);
  try {
    const response = await fetch('https://api.example.com/data');
    const result = await response.json();
    setData(result);
  } catch (err) {
    setError(err.message);
  } finally {
    setLoading(false);
  }
}
Enter fullscreen mode Exit fullscreen mode

Solution: Use useReducer for Complex State

const initialState = { loading: false, error: null, data: null };

function reducer(state, action) {
  switch (action.type) {
    case 'FETCH_START':
      return { ...state, loading: true, error: null };
    case 'FETCH_SUCCESS':
      return { ...state, loading: false, data: action.payload };
    case 'FETCH_ERROR':
      return { ...state, loading: false, error: action.payload };
    default:
      return state;
  }
}

function MyComponent() {
  const [state, dispatch] = useReducer(reducer, initialState);

  async function fetchData() {
    dispatch({ type: 'FETCH_START' });
    try {
      const response = await fetch('https://api.example.com/data');
      const result = await response.json();
      dispatch({ type: 'FETCH_SUCCESS', payload: result });
    } catch (err) {
      dispatch({ type: 'FETCH_ERROR', payload: err.message });
    }
  }

  return (
    <div>
      {state.loading && <p>Loading...</p>}
      {state.error && <p>Error: {state.error}</p>}
      {state.data && <pre>{JSON.stringify(state.data, null, 2)}</pre>}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

This approach centralizes state logic and makes it easier to manage complex transitions.


Mistake 9: Not Using Custom Hook For Interacting With LocalStorage Data

Persist state across page reloads by syncing it with local storage. Use useState in combination with useEffect to achieve this seamlessly in custom hook.

const [theme, setTheme] = useState('light');

function toggleTheme() {
  const newTheme = theme === 'light' ? 'dark' : 'light';
  setTheme(newTheme);
  localStorage.setItem('theme', newTheme);
}
Enter fullscreen mode Exit fullscreen mode

Solution: Automate Local Storage Sync

Automatically sync state with local storage.

const useLocalStorage = (key, initialValue) => {
  const [value, setValue] = useState(() => {
    try {
      const localValue = window.localStorage.getItem(key);
      return localValue ? JSON.parse(localValue) : initialValue;
    } catch (error) {
      return initialValue;
    }
  });

  useEffect(() => {
    window.localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
};

function App() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');

  function toggleTheme() {
    setTheme(theme === 'light' ? 'dark' : 'light');
  }

  return (
    <div>
      <p>Current Theme: {theme}</p>
      <button onClick={toggleTheme}>Toggle Theme</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

This approach ensures your state persists across sessions without manual intervention.

Learn to build Book Management App Using This Approach


Mistake 10: Overwriting State Data Using Controlled Inputs

When managing controlled inputs (e.g., forms), avoid overwriting user input by properly handling the onChange event.

const [formData, setFormData] = useState({ name: '', email: '' });

function handleChange(event) {
  setFormData({ [event.target.name]: event.target.value }); // Overwrites other fields
}
Enter fullscreen mode Exit fullscreen mode

Always spread existing values to preserve other fields.

Solution: Spread Existing Values

function handleChange(event) {
  const { name, value } = event.target;
  setFormData(prevData => ({ ...prevData, [name]: value })); // Preserves other fields
}
Enter fullscreen mode Exit fullscreen mode

This ensures all fields in the form remain intact while updating only the relevant one.


Mistake 11: Not Doing Equality Checks During State Update

React doesnโ€™t automatically check if the new state is different from the old state before triggering a re-render. To prevent unnecessary updates, manually compare the new value with the current state.

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

function updateCount(newCount) {
  setCount(newCount); // Triggers a re-render even if `newCount === count`
}
Enter fullscreen mode Exit fullscreen mode

Solution: Add an Equality Check

function updateCount(newCount) {
  if (newCount !== count) {
    setCount(newCount); // Only updates state if the value changes
  }
}
Enter fullscreen mode Exit fullscreen mode

This prevents redundant re-renders and improves performance.


Mistake 12: Always Using Redux Or Context For Storing Data

For temporary UI states (e.g., showing/hiding modals, toggling dropdowns), use useState instead of managing these states globally (e.g., in Redux or Context). This keeps your global state clean and focused on app-wide data.

// Storing modal visibility in Redux or Context is Not good
dispatch({ type: 'SET_MODAL_VISIBLE', payload: true });
Enter fullscreen mode Exit fullscreen mode

Solution: Use Local State

const [isModalOpen, setIsModalOpen] = useState(false);

function openModal() {
  setIsModalOpen(true);
}

function closeModal() {
  setIsModalOpen(false);
}

return (
  <div>
    <button onClick={openModal}>Open Modal</button>
    {isModalOpen && <Modal onClose={closeModal} />}
  </div>
);
Enter fullscreen mode Exit fullscreen mode

Mistake 13: Not Using Updater Function Syntax For Async Operations

When updating state based on asynchronous operations (e.g., fetching data), always use functional updates to ensure youโ€™re working with the latest state.

const [data, setData] = useState([]);

async function fetchData() {
  const response = await fetch('https://api.example.com/data');
  const newData = await response.json();
  setData([...data, ...newData]); // Uses stale `data`
}
Enter fullscreen mode Exit fullscreen mode

Solution: Use Functional Update Syntax

async function fetchData() {
  const response = await fetch('https://api.example.com/data');
  const newData = await response.json();
  setData(prevData => [...prevData, ...newData]); // Always uses the latest state
}
Enter fullscreen mode Exit fullscreen mode

Mistake 14: Not using useState for Conditional Rendering with State Machines

For complex UI flows (e.g., multi-step forms, wizards), use useState to implement a simple state machine. This keeps your logic organized and predictable.

const [step, setStep] = useState(1);

return (
  <div>
    {step === 1 && <Step1 />}
    {step === 2 && <Step2 />}
    {step === 3 && <Step3 />}
  </div>
);
Enter fullscreen mode Exit fullscreen mode

Solution: Use a State Machine

Map each step to a component using a state machine.

const steps = [<Step1 />, <Step2 />, <Step3 />];
const [currentStep, setCurrentStep] = useState(0);

function nextStep() {
  setCurrentStep(prevStep => Math.min(prevStep + 1, steps.length - 1));
}

function prevStep() {
  setCurrentStep(prevStep => Math.max(prevStep - 1, 0));
}

return (
  <div>
    {steps[currentStep]}
    <button onClick={prevStep} disabled={currentStep === 0}>
      Previous
    </button>
    <button onClick={nextStep} disabled={currentStep === steps.length - 1}>
      Next
    </button>
  </div>
);
Enter fullscreen mode Exit fullscreen mode

Mistake 15: Manually Updating Nested Properties

When managing complex nested state (e.g., deeply nested objects or arrays), use immutability helpers like immer to simplify updates while keeping your state immutable.

const [user, setUser] = useState({ name: 'John', address: { city: 'New York' } });

function updateCity(newCity) {
  setUser(prevUser => ({
    ...prevUser,
    address: { ...prevUser.address, city: newCity }, // Tedious and error-prone
  }));
}
Enter fullscreen mode Exit fullscreen mode

Solution: Use Immer for Simplicity

Install immer library (npm install immer) and simplify nested updates.
import produce from 'immer';

const [user, setUser] = useState({ name: 'John', address: { city: 'New York' } });

function updateCity(newCity) {
  setUser(
    produce(draft => {
      draft.address.city = newCity; // Mutate draft immutably
    })
  );
}
Enter fullscreen mode Exit fullscreen mode

Connect With Me

Image of Checkly

Replace beforeEach/afterEach with Automatic Fixtures in Playwright

  • Avoid repetitive setup/teardown in spec file
  • Use Playwright automatic fixtures for true global hooks
  • Monitor JS exceptions with a custom exceptionLogger fixture
  • Keep your test code clean, DRY, and production-grade

Watch video

Top comments (0)

AWS Security LIVE!

Tune in for AWS Security LIVE!

Join AWS Security LIVE! for expert insights and actionable tips to protect your organization and keep security teams prepared.

Learn More

๐Ÿ‘‹ Kindness is contagious

DEV shines when you're signed in, unlocking a customized experience with features like dark mode!

Okay