DEV Community

Cover image for useReducer Hook+ Context API = A Powerful React Tool
Abhishek Singh
Abhishek Singh

Posted on

useReducer Hook+ Context API = A Powerful React Tool

We are immensely familiar with hooks like useState, useEffect and useRef a lot which allows us to use class-based components features now in functional components. But React hooks have one more weapon in its arsenal which can be an effective tool to optimise a react application: the useReducer hook.

useReducer - a redux wannabe

The best description and example of the useReducer hook can be found in the Official React docs. But if I have to explain it in a concise manner:

useReducer allows your react component to have a redux-like state

You just need to provide a reducer function and an initial state value. Your component will get a state and a dispatch function which can be used to update that state.

It seems similar to useState, and React specifies some deciding factor that can indicate when useReducer will be better alternative:

  1. Your component state is complex that involves multiple sub-values, and/or
  2. The next state value depends upon the current state value.

So a best example of useReducer can be like this:

const initialTodos = [
    {
        id: 1,
        task: 'Sample Done task #1',
        done: true
    },
    {
        id: 2,
        task: 'Sample todo task #2',
        done: false
    }
]

function reducer (state, action) {
    switch(action.type) {
        case 'new_todo':
            return [
                ...state,
                {
                    id: state[state.length],
                    task: action.payload.task,
                    done: false
                }
            ]
        case 'edit_todo_task':
            const todoIdx = state.find( todo => todo.id===action.payload.id)
            return  [
                ...state.slice(0, todoIdx),
                {
                    ...state[todoIdx],
                    task: action.payload.task
                },
                ...state.slice(todoIdx+1)
            ]
        case 'toggle_todo_state': 
            const todoIdx = state.find( todo => todo.id===action.payload.id)
            return  [
                ...state.slice(0, todoIdx),
                {
                    ...state[todoIdx],
                    done: !state[todoIdx].state
                },
                ...state.slice(todoIdx+1)
            ]
    }
}

function TodoApp () {

    const [todos, dispatch] = useReducer(initialTodos, reducer)

    const handleStatusChange = (todoId) => {
        dispatch({
            type: 'toggle_todo_state',
            payload: { id: todoId}
        })
    }

    const handleTaskUpdate = (todoId, newTaskText) => {
        dispatch({
            type: 'edit_todo_task',
            payload: {
                id: todoId,
                task: newTaskText
            }
        })
    }
    const createNewTodo= (newTodoTask) => {
        dispatch({
            type: 'new_todo',
            payload: { task: newTodoTask }
        })
    }

    return (
        <TodoList
            todos={todos}
            onTodoCreate={createNewTodo}
            onStatusChange={handleStatusChange}
            onTaskEdit={handleTaskUpdate}
        />
    )
}
Enter fullscreen mode Exit fullscreen mode

A common and irritating use case in React Application

When using a complex component state like useReducer, we are likely to run into a scenario where we have to pass down the state updating function or a callback function (wrapping the state updating function) to the children components. If you have a large application, then it may happen that you have to pass those callback functions through intermediate children components until it reaches the actual descendant component which uses them. This can become unmanageable & suboptimal.

The Solution?

Combine the useReducer state & dispatch with the Context API.

The Context API
Context API have been a key feature of React. If you feel you need to be familiar with it, you can go through the docs

Both the state and the dispatch function produced by the useReducer can be fed to separate Context Providers in a parent component. Then any child component, no matter how deep, under the parent, can access them as needed with the use of useContext or Context Consumer.

Example:

const TodosDispatch = React.createContext(null);
const Todos = React.createContext(null)

function TodoApp() {
  const [todos, dispatch] = useReducer(reducer, initialTodos);

  return (
    <TodosDispatch.Provider value={dispatch}>
        <Todos.Provider value={todos} >
            <TodoList />
        </Todos.Provider>
    </TodosDispatch.Provider>
  );
}



function TodoList() {
    const {todos} = useContext(Todos)

    return (
        <ul>
        {
            todos.map(todo => <TodoItem key={todo.id} task={task} isDone={todo.done} />)
        }
        </ul>
    )
}

function AddTodoButton() {
  const dispatch = useContext(TodosDispatch);

  function handleClick() {
    dispatch({
        type: 'new_todo', payload: { task: 'hello' }});
  }

  return (
    <button onClick={handleClick}>Add todo</button>
  );
}
Enter fullscreen mode Exit fullscreen mode

This combination helps to avoid passing down states or update functions through intermediate components.

Only the components who actually needs the state or the dispatch function can get what they need.

The intermediate components get to handle lesser props as well and can better handle faster component re-rendering decision when memoized.

Benefits

  • This useReducer and useContext combination actually simulates Redux's state management, and is definitely a better light weight alternative to the PubSub library.
  • If your application is already using an application state, and you require another application state(for whole or part of the application), the combination can be used as a 2nd application state

Caveat

This is not a perfect Redux alternative.

  • Redux allows use of custom middlewares for better state management, but this feature is lacking in React's useRecuder.
  • Async tasks can not be used with useReducer.
  • Just like in Redux, there will be huge boilerplate code in the reducer function, and there are no APIs like Redux Tookkit to use for avoiding this.

Discussion (0)