DEV Community

Cover image for Master React State: When to Use useState vs useReducer (No More Confusion)
Werliton Silva
Werliton Silva

Posted on

Master React State: When to Use useState vs useReducer (No More Confusion)

Tchaca is a junior dev who just entered the React world.

On a normal workday, she faced a question:

“I need to manage some state in my component… but should I use useState or useReducer?”

This is a super common question - even experienced devs get confused sometimes.

Let’s follow Tchaca’s journey and understand when to use each one, with simple day-to-day analogies.


When to use useState

stk

useState is like a sticky note: you write something simple, stick it on the screen, and whenever you need, you can update what’s written there.

It’s perfect for small and straightforward things, like:


🧪 Example 1 (Beginner ✅): Toggle light/dark theme

const [darkMode, setDarkMode] = useState(false);

return (
  <button onClick={() => setDarkMode(prev => !prev)}>
    Mode {darkMode ? 'Dark' : 'Light'}
  </button>
);
Enter fullscreen mode Exit fullscreen mode

👉 Why useState?

It’s just a sticky note with one word: Light or Dark. Easy to swap.


🧪 Example 2 (Beginner ✅): Store username

const [username, setUsername] = useState('');

<input
  type="text"
  value={username}
  onChange={e => setUsername(e.target.value)}
/>
Enter fullscreen mode Exit fullscreen mode

👉 Why useState?

It’s just a text box holding one word. Nothing complex.


🧪 Example 3 (Intermediate 🚀): Simple form

const [form, setForm] = useState({ name: '', email: '' });

const handleChange = (e) => {
  setForm({ ...form, [e.target.name]: e.target.value });
};
Enter fullscreen mode Exit fullscreen mode

👉 Why useState still works?

The form is small. You can still manage it with a sticky note without a mess.


🧩 When to use useReducer

st

Now imagine that instead of a sticky note, you need a manual of instructions to organize many changes in the same state.

That’s where useReducer comes in: it helps centralize and organize logic.

It shines when state is complex: big forms, shopping carts, multiple state transitions.


🧪 Example 4 (Intermediate 🚀): Shopping cart

const initialState = [];

function cartReducer(state, action) {
  switch (action.type) {
    case 'add':
      return [...state, action.item];
    case 'remove':
      return state.filter(item => item.id !== action.id);
    default:
      return state;
  }
}

const [cart, dispatch] = useReducer(cartReducer, initialState);
Enter fullscreen mode Exit fullscreen mode

👉 Why useReducer?

If it were a sticky note, it would get messy. With a manual, you know exactly what to do when someone adds or removes an item.


🧪 Example 5: Complex form

const initialState = {
  step: 1,
  values: { name: '', email: '' },
  errors: {}
};

function formReducer(state, action) {
  switch (action.type) {
    case 'next':
      return { ...state, step: state.step + 1 };
    case 'setField':
      return {
        ...state,
        values: { ...state.values, [action.field]: action.value }
      };
    case 'setError':
      return {
        ...state,
        errors: { ...state.errors, [action.field]: action.error }
      };
    default:
      return state;
  }
}

const [formState, dispatch] = useReducer(formReducer, initialState);
Enter fullscreen mode Exit fullscreen mode

👉 Why useReducer?

It’s like building a puzzle: multiple pieces need to fit together. The reducer organizes everything in one place.


🧪 Example 6: Multiple state transitions

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

function viewReducer(state, action) {
  switch (action.type) {
    case 'edit':
      return { ...state, mode: 'edit' };
    case 'save':
      return { ...state, loading: true };
    case 'success':
      return { ...state, loading: false, data: action.data, mode: 'view' };
    case 'error':
      return { ...state, loading: false, error: action.error };
    default:
      return state;
  }
}
Enter fullscreen mode Exit fullscreen mode

👉 Why useReducer?

Here the sticky note won’t cut it. It’s like managing multiple browser tabs: you need a manual to know what to do in each situation.


🎯 The complexity scale

Think of it like this:

scale

  • useState → Sticky note (simple, quick).
  • ⚖️ Middle ground → You can still use useState, but useReducer also works.
  • 💡 useReducer → Manual of instructions (many steps, multiple options).

🚧 Things to watch out for

Hook Pros Cons
useState Simple, direct, less boilerplate Hard to scale
useReducer Organizes more complex logic More verbose, more code

🔄 From useState to useReducer(Side by Side)

Tchaca first tried to solve her problem using useState.
It felt natural: she just created one state for each thing she needed to track.

Using useState

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

const handleEdit = () => setMode("edit");
const handleSave = () => setLoading(true);
const handleSuccess = (newData) => {
  setLoading(false);
  setData(newData);
  setMode("view");
};
const handleError = (err) => {
  setLoading(false);
  setError(err);
};
Enter fullscreen mode Exit fullscreen mode

👉 At first, this looked clean and simple. Each variable had its own setter, easy to follow.


But as her app grew, Tchaca noticed the state transitions were scattered everywhere.
It was getting harder to see the “big picture” of how state was changing.

That’s when she learned about useReducer.

Using useReducer

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

function viewReducer(state, action) {
  switch (action.type) {
    case 'edit':
      return { ...state, mode: 'edit' };
    case 'save':
      return { ...state, loading: true };
    case 'success':
      return { ...state, loading: false, data: action.data, mode: 'view' };
    case 'error':
      return { ...state, loading: false, error: action.error };
    default:
      return state;
  }
}

const [state, dispatch] = useReducer(viewReducer, initialState);

Enter fullscreen mode Exit fullscreen mode

👉 With this approach, all possible state changes lived in one single function.
It was easier for Tchaca to reason about the flow of her component and maintain it as the logic expanded.

Diff

bys


🧠 Key takeaway

  • useState → Great when things are simple and independent.
  • useReducer → Better when logic gets more complex, since everything is centralized.

🧠 Conclusion

In the end, Tchaca learned there’s no absolute rule.

  • If the state is simple, use a sticky noteuseState.
  • If the state starts to get complex, use a manual of instructionsuseReducer.

👉 The secret is to keep your code easy to read, maintain, and scale.


📌 Challenge for you:

Build a simple to-do list with useState. Then refactor it to use useReducer and compare which version feels clearer to you.

Top comments (4)

Collapse
 
masterdevsabith profile image
Muhammed Sabith

Ok challenge accepted ! This was a very useful post, this is the first time I'm hearing about this thing, all these years, I've been showering in useState, I'll definitely try a change. Keep making posts like these, these are also easy to understand ❤️‍🔥📈

Collapse
 
werliton profile image
Werliton Silva

I'm glad hear this. thanku

Collapse
 
dev_king profile image
Dev King

Muito util

Collapse
 
werliton profile image
Werliton Silva

Thank for this