DEV Community

loading...
Cover image for React useReducer for dummies

React useReducer for dummies

Samson Andrew
I'm an ardent JavaScript lover with 5+ years of professional experience in full-stack development.
・5 min read

So you finally start your React journey or you're just converting your apps to use functional components and you come across the useReducer hook but can't really wrap your head around it? I'll show you how in few minutes. Read on...

The useReducer hook is best used when your component has multiple states, to avoid multiple calls to useState. It is also useful when you want to avoid managing multiple callbacks. I'll show example of each use case.

Example with useState

Consider the following component:

//ES6 and JSX
import React, {useState} from 'react';

function MyBeautifulForm() {
    const [username, setUsername] = useState('');
    const [password, setpassword] = useState('');
    const [email, setEmail] = useState('');
    const [age, setAge] = useState(0);
    const [height, setHeight] = useState(0);
    const [acceptTerms, setAcceptTerms] = useState(false);

    function handleFormSubmit(event) {
        event.preventDefault();
        // processForm();
    }

    return (
        <form onSubmit={handleFormSubmit}>
            <input type="text" name="username" value={username} onChange={(e) => setUsername(e.target.value)} />
            <input type="password" name="password" value={password} onChange={(e) => setPassword(e.target.value)} />
            <input type="email" name="email" value={email} onChange={(e) => setEmail(e.target.value)} />
            <input type="number" name="age" value={age} onChange={(e) => setAge(e.target.value)} />
            <input type="number" name="height" value={height} onChange={(e) => setHeight(e.target.value)} />
            <input type="checkbox" name="terms" checked={acceptTerms ? 'checked' : ''} onChange={(e) => setAcceptTerms(e.target.checked)} />
            <input type="submit" name="submit" value="Submit" />
        </form>
    )
}
Enter fullscreen mode Exit fullscreen mode

That's a whole lot of useStates to maintain. If our form continues to grow by adding more fields, we would have even more states to maintain and this would someday grow into a big mess. So how do we take care of this?

Your initial thought might be to turn all the initial states to a single state object and pass it to a single useState call. While this is going to work, we should remember that calling setState in function components replaces the state rather than merging as in class components. States can be lost or overridden unintentionally if updated this way most especially if we're coming from class components.

This is where the useReducer hook shines. Let's see how we can use it to simplify the previous code:

Example with useReducer

//ES6 and JSX
import React, {/*useState,*/ useReducer} from 'react';

function reducer(state, action) {
    switch(action.type) {
        case 'USERNAME':
            return {...state, username: action.payload};
        case 'PASSWORD':
            return {...state, password: action.payload};
        ...
        ...
        default:
            return state;
    }
}

function MyBeautifulForm() {
    const initialState = {
        username: '',
        password: '',
        email: '',
        age: 0,
        height: 0,
        acceptTerms: false
    }  
    // const [formState, setFormState] = useState(initialState); // we will come bct to this later

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

    function handleFormSubmit(event) {
        event.preventDefault();
        // processForm();
    }

    return (
        <form onSubmit={handleFormSubmit}>
            <input type="text" name="username" value={state.username} onChange={(e) => dispatch({type: 'USERNAME', payload: e.target.value})} />
            <input type="password" name="password" value={state.password} onChange={(e) => dispatch({type: 'PASSWORD', payload: e.target.value})} />
            <input type="email" name="email" value={state.email} onChange={(e) => dispatch({type: 'EMAIL', payload: e.target.value})} />
            <input type="number" name="age" value={state.age} onChange={(e) => dispatch({type: 'AGE', payload: e.target.value})} />
            <input type="number" name="height" value={state.height} onChange={(e) => dispatch({type: 'HEIGHT', payload: e.target.value})} />
            <input type="checkbox" name="terms" checked={state.acceptTerms ? 'checked' : ''} onChange={(e) => dispatch({type: 'TERMS', payload: e.target.checked})} />
            <input type="submit" name="submit" value="Submit" />
        </form>
    )
}
Enter fullscreen mode Exit fullscreen mode

And now, we have a single state object (initialState) and a single state updater function (reducer).

Let's discuss what just happened:

useState
You may have been wondering what we mean when we write:
const [something, setSomething] = useState(somethingElse);
The above line of code used something known as javascript array destructuring. The two constants (something and setSomething) is set to the first two values of the array returned by the call to useState.

Consider the following codes:

// const [fruit, setFruit] = useState('Apple');
let result = useState('Apple');//apple is the initial state
// you can pass null to the function
// if your component does not have an initial state
// console.log(result) // ['Apple', functionToSetAnotherName];

// later
let state = result[0];
let updater = result[1];

// let fruit = result[0];
// let setFruit = result[1];

//Well, this is not worth it...
//array destructuring is short and quite simple to use.
Enter fullscreen mode Exit fullscreen mode

You may declare a state as follows:

const [car, name] = useState('Toyota');

if you want to create unnecessary headache for yourself and other team members.

Using [something, setSomething] is the adopted way of doing this, so you have to follow the pattern.

Whenever your state changes and you want to update it, the only way to do that is by using the second item returned by the setState function. I call it the state updater. This way, you are sure that your state will always maintain the correct value. Always use the function to update your state and avoid changing the value directly (mutation).

useReducer
The process of setting or updating a state with the useReducer may not be as straightforward as with useState but it is more elegant. The steps are as follows:

  1. Declare your intialState object
  2. Declare your reducer function
  3. Declare your state with the const [state, dispatch] = useReducer(reducer, initialState) syntax
  4. Dispatch your actions to the reducer function with the dispatch function (this would have been the state updater function in useState, but since we do not keep a separate state for each component, we have to send the update request to the reducer)
  5. Update your state within the reducer function using the type and the payload information provided by the dispatcher.
  6. The return value after the update is the current state which ensures that our state is up to date

What you gain from this is simplicity as you do not need to pass different callbacks around - you only pass a single dispatch which returns only one value (currentState)

Finally

Let's demonstrate how to use useState with a single state object.

Avoid multiple calls to useState

//ES6 and JSX
import React, {useState} from 'react';

function MyBeautifulForm() {
    const initialState = {
        username: '',
        password: '',
        email: '',
        age: 0,
        height: 0,
        acceptTerms: false
    }  
    const [formState, setFormState] = useState(initialState);

    function updateState(state) {
        setFormState(formState => {...formState, ...state});
    }

    function handleFormSubmit(event) {
        event.preventDefault();
        // processForm();
    }

    return (
        <form onSubmit={handleFormSubmit}>
            <input type="text" name="username" value={state.username} onChange={(e) => updateState({username: e.target.value})} />
            <input type="password" name="password" value={state.password} onChange={(e) => updateState({password: e.target.value})} />
            <input type="email" name="email" value={state.email} onChange={(e) => updateState({email: e.target.value})} />
            <input type="number" name="age" value={state.age} onChange={(e) => updateState({age: e.target.value})} />
            <input type="number" name="height" value={state.height} onChange={(e) => updateState({height: e.target.value})} />
            <input type="checkbox" name="terms" checked={state.acceptTerms ? 'checked' : ''} onChange={(e) => updateState({acceptTerms: e.target.checked})} />
            <input type="submit" name="submit" value="Submit" />
        </form>
    )
}
Enter fullscreen mode Exit fullscreen mode

In a simple way, the code above can be used to achieve the same result as the previous example with useReducer. Although, for some use cases, you might find one to be more elegant than the other. The aim of this article is to explain the basic usage of the useReducer hook and not to promote one at the expense of the other.

Conclusion

We have seen how the useReducer hook can be used to combine multiple states into a single state object and updated with the reducer function using the information provided by the dispatch function. I will answer any question at the comment section.

Thanks ;)

If you're still reading, you can check out my clone of dev.to with React.js at Dev to Blog. The source code is available at the public repo Github Link

Discussion (4)

Collapse
heinkozin profile image
HeinKoZin

Thank you! It was difficult for me to understand before your explanation.

Collapse
maswerdna profile image
Samson Andrew Author

You're welcome Hein,
I'm glad you found the article useful.

Collapse
fadliselaz profile image
fadliselaz

Thanks bro,

Collapse
maswerdna profile image
Samson Andrew Author

You're welcome.
Glad you found it useful 👍