DEV Community

Cover image for useState vs useReducer: What are they and when to use them?
m0nm
m0nm

Posted on

useState vs useReducer: What are they and when to use them?

It is common to see useState hook used for state management, However React also have another hook to manage component's state, Which is useReducer hook. In fact, useState is built on useReducer!. So a question arises: What's the difference between the two ? And when should you use either ?

useState hook:

useState hook is a hook used to manipulate and update a functional component. The hook takes one argument which is the initial value of a state and returns a state variable and a function to update it.

const [state, setState] = useState(initialValue)
Enter fullscreen mode Exit fullscreen mode

So a counter app using the useState hook will look like this:

function Counter() {
  const initialCount = 0
  const [count, setCount] = useState(initialCount);
  return (
    <>
      Count: {count}
      <button onClick={() => setCount(initialCount)}>Reset</button>
      <button onClick={() => setCount(count - 1)}>Decrement</button>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

useReducer hook:

this hook is similar to the useState hook. However it's able to handle more complex logic regarding the state updates. It takes two arguments: a reducer function and an initial state. The hook then returns the current state of the component and a dispatch function

const [state, dispatch] = useReducer(reducer, initialState)
Enter fullscreen mode Exit fullscreen mode

the dispatch function is a function that pass an action to the reducer function.

The reducer function generally looks like this:

const reducer = (state, action) => {
    switch(action.type) {
        case "CASE1": 
            return "new state";
        case "CASE2": 
            return "new state";
        default:
            return state
    }
}
Enter fullscreen mode Exit fullscreen mode

The action is usually an object that looks like this:

// action object:
{type: "CASE1", payload: data}
Enter fullscreen mode Exit fullscreen mode

The type property tells the reducer what type of action has happened ( for example: user click on 'Increment' button). The reducer function then will determine how to update the state based on the action.

So a counter app using the useReducer hook will look like this:

const initialCount = 0

const reducer = (state, action) => {
    switch (action.type) {
        case "increment":
            return action.payload;

        case "decrement": 
            return action.payload;

        case "reset": 
            return action.payload;

        default: 
            return state;
    }
}

function Counter() {
    const [count, dispatch] = useReducer(reducer, initialCount)

    return (
    <>
      Count: {count}
      <button onClick={() => dispatch({type: "reset", payload: initialCount}))}>Reset</button>
      <button onClick={() => dispatch({type: "decrement", payload: state - 1})}>Decrement</button>
      <button onClick={() => dispatch({type: "increment", payload: state + 1})}>Increment</button>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

When should i useReducer() ?

As stated above, The useReducer hook handles more complex logic regarding the state updates. So if you're state is a single boolean, number, or string, Then it's obvious to use useState hook. However if your state is an object (example: person's information) or an array (example: array of products ) useReducer will be more appropriate to use.

Let's take an example of fetching data:

If we have a state that represent the data we fetched from an API, The state will either be one of this three 'states': loading, data, or error

When we fetch from an API, Our state will go from loading ( waiting to receive data), to either data or we'll get an error

Let's compare how we handle state with the useState hook and with the useReducer hook

  • With the useState hook:
function Fetcher() {
    const [loading, setLoading] = useState(true)
    const [data, setData] = useState(null)
    const [error, setError] = useState(false)

    useEffect(() => {
        fetch('https://jsonplaceholder.typicode.com/posts/1').then(res => {
            setLoading(false)
            setData(res.data)
            setError(false)
        }).catch((err) => {
            setLoading(false)
            setData(null)
            setError(true)
        })
        ,[])

        return (
        {loading ? <p>Loading...</p> 
         : <div>
            <h1>{data.title}</h1>
            <p>{data.body}</p>
         </div> }
        {error && <p>"An error occured"</p> }
        )

}
Enter fullscreen mode Exit fullscreen mode
  • With the useReducer hook:

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

const reducer = (state, action) => {
    switch (action.type) {
        case "SUCCESS":
            return {
                loading: false,
                data: action.payload,
                error: false
            };

        case "ERROR": 
            return {
                loading: false,
                data: null,
                error: true
            };

        default:
            return state;
    }
}

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

    useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/posts/1').then(res => {
        dispatch({type: "SUCCESS", payload: res.data})
    }).catch(err => {
        dispatch({type: "ERROR"})
    })

    } ,[])

    return (
        {state.loading ? <p>Loading...</p> 
         : <div>
            <h1>{state.data.title}</h1>
            <p>{state.data.body}</p>
         </div> }
        {state.error && <p>"An error occured"</p> }
        )

}
Enter fullscreen mode Exit fullscreen mode

As you can see with the useReducer hook we've grouped the three states together and we also updated them together. useReducer hook is extremely useful when you have states that are related to each other, Trying to handle them all with the useState hook may introduce difficulties depending on the complexity and the bussiness logic of it.

Conclusion

To put it simply: if you have a single state either of a boolean, number, or string use the useState hook. And if you're state is an object or an array, Use the useReducer hook. Especially if it contains states related to each other.

I hope this post was helpful, Happy coding!

Top comments (3)

Collapse
 
ramjak profile image
Rama Jakaria

The problem with reducer is it's required a lot of boilerplate. I usually won't use it unless the state is really complicated and require additional testing.

Collapse
 
saritchaethudis profile image
Sarit Chaet Hudis

Thank you for clearing that out!! I was starting to grow an unexplained phobia from useReducer because it looked to mysterious..

Collapse
 
benshaw profile image
Ben Shaw

In the counter example, ut feels like the advantage of useReducer is that you could move logic like state-1 and state+1 into the reducer itself and them simply pass the action type?