React hooks - useReducer, useTransition, useDeferredValue


Introduction

In my last blog, I explained about the useMemo, useCallback, React.memo. Now, we will see the use of useReducer, useTransition, and useDeferredValue. Before useReducer came along, the only way to manage complex state transition was to use the Redux library. With useReducer, that is no longer the case. useTransition was released with React 18, and it is amazing, and we will see what it does along with the useDeferredValue which is similar to useTransition with slight differences.

useReducer

The useReducer does exactly what useState allows us to do except it helps us manage complex state transition in a simpler way. The useReducer hook takes in two arguments - a reducer function, and the intial state that which we want to update. It returns an array containing the state, as its first element, and a dispatch function using which we will call the reducer function which helps us in updating the state depending upon the type of action passed into the dispatch function as argument.

Let's look at an example to understand how it is implemented.

function reduce(state, action) {
    switch(action.type) {
        case 'increment': 
            return { ...state, count: state.count + 1 }
        case 'decrement': 
            return { ...state, count: state.count - 1 }
        default: 
            return { ...state } 
    }
}

export default function App() {
    const [ state, dispatch ] = useReducer(reduce, { count: 0 });

    return (
        <>
            <button onClick={() => dispatch({ type: 'increment' })} + </button>
            <div> { state.count } </div>
            <button onClick={() => dispatch({ type: 'decrement' })}> - </button> 
        </>
    );
}

I have implemented a simple counter. The buttons upon clicking call the dispatch function which has a specific "type" passed to it as an argument that triggers an action in updating the state. The reduce function contains all the logic of state updation related to specific types of action. The benefit of using useReducer is that one can update and manage states globally.

useTransition

The useTransition hook is really interesting. In order to understand it let us look at an example.

import { useState } from "react";

export default function App() {
    const [ name, setName ] = useState('');
    const [ list, setList ] = useState([]);

    function handleChange(e) {
        setName(e.target.value);
        let l = [];
        for(let i = 0; i < 20000; i++) {
          l.push(e.target.value);
        }       

        setList(l);
    }

    return (
        <>
          <input type="text" value={name} onChange={handleChange} />
          {
            list.map(item => {
              return <div>{item}</div>
            })
          }
        </>
    );
}

In the above example, we have two states - name, and list. Upon typing something in the input field, the handleChange function is called where the name and list states get updated. Now, we know that whenever states change, the component gets re-rendered. In this case, if we think logically, both states are being updated, which means the component should get rendered twice. This is all good except React is smart enough to understand that both the state updates are occurring simultaneously so instead of rendering the component twice, it only renders the component once after the updation of both states. This is good except in some cases, like the example that I have provided above, it could affect the performance of an application where the state updation takes a lot of time creating a bad user experience issue.

To avoid such scenarios, useTransition comes to rescue. What it actually does is that it considers some state updates as less important and thus it allows a component to render with the less important state updates occurring parallelly. The useTransition hook returns an array containing two elements - the first being a boolean value, and the second being a callback function that wraps around the state updating function.

It returns an array with the first element being a boolean value which represents if the state updation process is completed or not. The second element is a callback function which is used to wrap the less important state-updating function.

Let us modify the above example to understand how to implement the useTransition hook.

import { useState } from "react";

export default function App() {
    const [ name, setName ] = useState('');
    const [ list, setList ] = useState([]);
    const [ isPending, startTransition ] = useTransition();

    function handleChange(e) {
        setName(e.target.value);
        let l = [];
        for(let i = 0; i < 20000; i++) {
          l.push(e.target.value);
        }       

        startTransition(() => {setList(l)});
    }

    return (
        <>
          <input type="text" value={name} onChange={handleChange} />
          {
            isPending ? 
            <div>Loading....</div>
            :
            list.map(item => {
              return <div>{item}</div>
            })
          }
        </>
    );
}

"isPending" is "true" if the state update is happening and the value changes to "false" if the state update has finished.

useDeferredValue

useDeferredValue lets us defer updating a part of the UI. It takes in an input and returns a deferred value that doesn't update immediately. It implements a delay before some information is calculated to fix slow rendering issues in the UI.

Let us look at an example to understand how it works

import { useState } from "react";

export default function App() {
    const [ input, setInput ] = useState('');
    const handleChange = (e) => {
        setInput(e);
    };

    return (
        <>
          <input type="text" value={input} onChange={handleChange} />
          <List input={input} />
        </>                  
    );
}
import { useDeferredValue } from "react";

function calculate(deferredInput, LIST_SIZE) {
    let l = [];
    for(let i = 0; i < LIST_SIZE; i++) {
        l.push(deferredInput);
    }

    return l;
}

export default function List({ input }) {
    const deferredInput = useDeferredValue(input);
    const LIST_SIZE = 20000;

    const list = useMemo(() => {
        return calculate(deferredInput);
    }, [deferredInput] );

    return list; 
}

In the above example, what we are actually doing is that in the input field, as the user types in anything, it causes a change to the "input" state variable. However, in the "List" component, the useDeferredValue takes the input as an argument and returns a deferred value, such that the deferred value gets only updated after some delay which fixes the issue of slow rendering. During the initial render, the deferred value is the same as the initial value. It is only in the second rendering of the component, the deferred value is updated with the new value, which in the example above, causes the calculate function to return a new value with the deferred value.

In a way, useDefferedValue works similarly useTransition except it is used when we don't have access to the state updating function and so we make use of the state variable which gets affected as input to the useDeferredValue hook to return a deferred value.

Final Conclusion

That's all about the useReducer, useTransition, and useDeferredValue hook. In my next blog, I will be explaining about the useState, useEffect, and useContext hooks. Till then be happy, and code furiously!

Here's a fun fact before you go:

I always implement the useTransition hook in my life. Do you wanna how? I always delegate the "less important" tasks to my colleagues and work on the more important tasks myself, which produces more value. Did you get it?????

You can follow me on Github, LinkedIn.

Thank you for reading.