useReducer instead of useState while calling APIs!

Yogini Bende - Apr 27 '21 - - Dev Community

Hello folks!

It’s been a while since React has introduced Hooks and we all fell in love with it’s patterns and ease of use. Though this is the case, many of us do not leverage all the features, hooks provide and useReducer is one of them! Because useState is the hook which we learn first, we do not make much use of useReducer hook. So in this article, I will be focussing on useReducer and will walk you through the best use-cases to implement it.

So, let’s dive in!

What is useReducer?

useReducer is another hook used for the modern state management in React. This concept was introduced in Redux first and then it is adapted by React as well. Typically, reducer is a function which accepts two arguments - state and action. Based on the action provided, reducer will perform some operations on a state and returns a new updated state. In context of React, useReducer also performs similar state management. You can read more about useReducer in detail in the react documentation

How to use it for API calls?

You must have got the basic idea of useReducer hook till now. Let’s just dive straight into the code and understand how using useReducer will make our code more efficient over useState.

Let’s first start with an API call using simple useState. It will look something like this -

// user component using useState 
const User = () => {
    const [userDetails, setUserdetails] = useState();
    const [loading, setLoading] = useState(false);
    const [error, setError] = useState();

    useEffect(() => {
        setLoading(true);
        const getUsers = async () => {
            let response = await axios.get('/users');
            if (response.status == 200) {
                setUserdetails(response.data);
                setError(false);
                return;
            }
            setError(response.error);
        };

        getUsers();
        setLoading(false);
    });

    return (
        <div>
            {loading ? (
                <p>loading...</p>
            ) : error ? (
                <p>{error}</p>
            ) : (
                <ul>
                    {userDetails.map((user) => (
                        <li key={user.id}>
                            <h1>{user.name}</h1>
                            <p>{user.location}</p>
                        </li>
                    ))}
                </ul>
            )}
        </div>
    );
};

export default User;
Enter fullscreen mode Exit fullscreen mode

This is a very basic API call. In real life scenarios, we have to manage more states than this. But for starters, let’s assume we have 3 states to manage and those are dependent on each other. When our application gets more complex, at times, we end up defining more than 7-8 states. In such scenarios, if we are using only useState, then it becomes very tedious to keep track of all the states and to update them synchronously.

To solve all these problems, a better approach is using useReducer. Let’s see the same API call using useReducer.

// user component using useReducer
const ACTIONS = {
    CALL_API: 'call-api',
    SUCCESS: 'success',
    ERROR: 'error',
};

const userDetailsReducer = (state, action) => {
    switch (action.type) {
        case ACTIONS.CALL_API: {
            return {
                ...state,
                loading: true,
            };
        }
        case ACTIONS.SUCCESS: {
            return {
                ...state,
                loading: false,
                userDetails: action.data,
            };
        }
        case ACTIONS.ERROR: {
            return {
                ...state,
                loading: false,
                error: action.error,
            };
        }
    }
};

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

const User = () => {
    const [state, dispatch] = useReducer(userDetailsReducer, initialState);
    const { userDetails, loading, error } = state;

    useEffect(() => {
        dispatch({ type: ACTIONS.CALL_API });
        const getUsers = async () => {
            let response = await axios.get('/users');
            if (response.status == 200) {
                dispatch({ type: ACTIONS.SUCCESS, data: response.data });
                return;
            }
            dispatch({ type: ACTIONS.ERROR, error: response.error });
        };

        getUsers();
    });

    return (
        <div>
            {loading ? (
                <p>loading...</p>
            ) : error ? (
                <p>{error}</p>
            ) : (
                <ul>
                    {userDetails.map((user) => (
                        <li key={user.id}>
                            <h1>{user.name}</h1>
                            <p>{user.location}</p>
                        </li>
                    ))}
                </ul>
            )}
        </div>
    );
};

export default User;
Enter fullscreen mode Exit fullscreen mode

Here, we are using a dispatch function to call our reducer. Inside the reducer, the switch case is defined to handle the actions provided by the dispatch function. The actions object declared above will make sure that every time we pass predefined action to the dispatch function. You can skip that step and use strings directly. Inside each switch case, we are performing operations on the state given and returning a new state.

I know your first reaction seeing the code would be, this looks lengthy! But trust me, it makes more sense. The useReducer hook accepts two parameters, a reducer function and initial state. Reducer function will perform all the state updations on the state provided. But what are the benefits of doing this?

  • State will update in a single function, based on the action and it will be dependent on previous.

    When we pass action to the reducer, we tell it what operation to perform on a previous state. This way, we can make sure all the states are in sync with that operation and there is a very less chance of missing any updates on a state.

  • Easy to manage complex states

    As one function is updating states, it is easier to manage complex states containing arrays and objects. We can useReducer effectively to handle updates on objects and arrays.

  • Easy to test and predictable

    Reducers are pure functions and perform operations based on predefined actions. Hence, they do not have any side effects and will return the same values when given the same arguments. This makes them predictable and easy to test when implemented.

When to choose useReducer over useState?

useReducers are good to choose over useState but not every time. If your use case is simple, they will add unnecessary complexity to your code. I use this couple of rules to choose useReducer over useState -
1. If there are many states dependent on each other.
2. If the state is a complex object.

I hope these rules will help you as well to decide which state management hook to go for. If you have any other factor to choose between these two, let me know in the comments.

Thank you for reading this article! Hope it will help you in some way. You can also connect with me on Twitter or buy me a coffee if you like my articles.

Keep learning 🙌

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . .