Typesafe useReducer with React Context
Tags: typescript, react, webdev, javascript
Published: 2022-05-12 08:59:26

In my opinion `useReducer` is an excellent option to use instead the regular `useState` hook for more complex state management. There is also a compelling argument that combining `useReducer` with React Context provides a state management solution that drastically reduce (😂) the need to include other libraries like redux. 

If you're like me and prefer the additional safety that Typescript provides, there is not a lot of information out there on how to use it with `useReducer`.

If you're new to reducers there is a concise summary at the [official documentation](https://reactjs.org/docs/hooks-reference.html#usereducer).

# Example App
In the example we will build the compulsory **Todo App**.
To do this, we need to be able to add, delete and toggle the completed event of `Todo` items defined as:
```ts
// typings/todo.ts
export interface Todo {
  id: string;
  task: string;
  completed: boolean;
}
```

## Actions
We want the actions to contain information on what type of action we want to do and also to carry an additional payload of data needed to perform the action.

To enable that, we can utilise the following interface as a base.
```ts
// typings/action.ts
export interface Action<T, P> {
  type: T;
  payload: P;
}
```
Defining the actions for our Todo app, then becomes easy as
```ts
// context/todo/action.ts
import { Action, Todo } from '~typings';

export type TodoAction =
  | Action<'ADD', Todo>
  | Action<'DELETE', { id: string }>
  | Action<'TOGGLE', { id: string }>;
```
## Reducer function
In the reducer function Typescript will keep track of all all relations between action types and payload, via the union type `TodoAction` that was defined in the previous section.  

```ts
// context/todo/reducer.ts
import { TodoAction } from './action';
import { Todo } from '~typings';

export const todoReducer = (state: Todo[], action: TodoAction): Todo[] => {
  switch (action.type) {
    case 'ADD':
      return [...state, action.payload];
    case 'TOGGLE':
      return state.map((todo) => {
        if (todo.id !== action.payload.id) {
          return todo;
        }
        return { ...todo, completed: !todo.completed };
      });
    case 'DELETE':
      return [...state.filter((todo) => todo.id !== action.payload.id)];
    default:
      return state;
  }
};
```

If you only want to use a plain `useReducer` adding the following two lines to your component should suffice.

```tsx
type reducerFunc = (state: Todo[], action: TodoAction) => Todo[];
const [state, dispatch] = useReducer<reducerFunc>(todoReducer, initialContext.todos);
```
 
## Context
If you instead want use your reducer to manage global state you can wrap it into a Context. This is also what will be used in the examples later on. 
Here we create a context with our list of todo items and our dispatch function.
```tsx
// context/todo/context.tsx
import React, { useContext, useReducer } from 'react';
import { Todo } from '~typings';
import { TodoAction } from './action';
import { todoReducer } from './reducer';

interface TodoContextI {
  todos: Todo[];
  dispatch: (arg: TodoAction) => void;
}

type reducerFunc = (state: Todo[], action: TodoAction) => Todo[];

const initialContext: TodoContextI = {
  todos: [],
  dispatch: () => console.error('Context not initialized')
};

const TodoContext = React.createContext<TodoContextI>(initialContext);

interface Props {
  children?: React.ReactNode;
}

export const TodoProvider: React.FC<Props> = ({ children }) => {
  const [state, dispatch] = useReducer<reducerFunc>(todoReducer, initialContext.todos);
  return <TodoContext.Provider value={{ todos: state, dispatch }}>{children}</TodoContext.Provider>;
};

export const useTodos = (): TodoContextI => useContext(TodoContext);

```
Don't forget to wrap your components within your `Context.Provider`
```tsx
// src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { TodoProvider } from '~context/todo';

const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
  <React.StrictMode>
    <TodoProvider>
      <App />
    </TodoProvider>
  </React.StrictMode>
);
```

## Read State
To read the state, there is no difference on how you would use it with a regular Context.

Here we just call the `useTodo` function which is just a short hand for `useContext(TodoContext)` that we declared with with the context. Then we map over the list and pass each `Todo` into a `TodoCompoent`.

```tsx
export const TodoView: React.FC = () => {
  const { todos } = useTodos();
  return (
    <Container>
      {todos.map((todo) => (
        <TodoComponent {...todo} />
      ))}
      <TodoForm />
    </Container>
  );
};
```
## Use Dispatch
To use the `dispatch` function, we just deconstruct it from the context in similar manner as before. We can then trigger state changes completely typesafe. In the snippet below we trigger `TOGGLE` and `DELETE` actions to mutate the state.
```tsx
// components/TodoComponent.tsx

export const TodoComponent: React.FC<Todo> = ({ task, completed, id }) => {
  const { dispatch } = useTodos();

  const handleCheckBoxClicked = (): void => {
    dispatch({ type: 'TOGGLE', payload: { id } });
  };

  const handleDeleteClicked = (): void => {
    dispatch({ type: 'DELETE', payload: { id } });
  };

  return (
    <TodoContainer done={completed}>
      <p>{task}</p>
      <div>
        <button onClick={onDeleteClick}>Delete</button>
        <input type="checkbox" checked={completed} onChange={handleCheckBoxClicked} />
      </div>
    </TodoContainer>
  );
};
```
And in the code below we trigger an action to `ADD` a new todo item to our list.

```tsx
export const TodoForm: React.FC = () => {
  const [state, setState] = useState('');
  const { dispatch } = useTodos();

  const handleInputChange = (e: ChangeEvent<HTMLInputElement>): void => {
    setState(e.target.value);
  };

  const handleSubmit = (): void => {
    dispatch({ type: 'ADD', payload: { task: state, completed: false, id: uuidv4() } });
    setState('');
  };

  return (
    <Container>
      <input type="text" value={state} onChange={handleInputChange} />
      <button disabled={!state} onClick={handleSubmit}>
        Add
      </button>
    </Container>
  );
};
```

I hope that this example was of use to you. I think that `useReducer` is a good option to use when your state becomes large and/or you need to manipulate the state in a lot of different, but it does come with some additional boilerplate. Most of the time i still prefer to keep things simple with and go with `useState`. _"I det enkla bor det vackra" - Ernst Kirchsteiger_

If you want to have a look at the source you can find it [here](https://github.com/sK4rdell/context-reducers-demo).

Take Care 👋