Creating your own mini-Redux - with React Hooks, Context API and TypeScript
Tags: react, typescript, redux, javascript
Published: 2020-07-27 20:31:34

Ever since the introduction of hooks, using React's Context API has become really simple. If you were relatively new to react when hooks came out (like myself), and were curious what this meant for Redux, you must have come across multiple articles on the internet that more than justified why Redux was still better for medium to large sized applications. 

But what if you're working on a relatively smaller application that does not need all the bells-and-whistles that you get with Redux? Could you create your own Redux-like state management solution with just Context API and Hooks? Sure.

I recently came across one such project that was the perfect opportunity to test this out. It was originally written in React 16.3, used the concept of controller and controlled components for state management, and made occasional use of Context API to avoid prop-drilling. The ask was to move it to the latest version of React and re-write the components to use hooks instead. Oh, and it had to use TypeScript!

The goal of this article is to showcase the TypeScript-friendly code structure we used in the project, so that anyone in our situation could build on top of this, instead of re-inventing the wheel. Now I know, there have been multiple articles like this but I had to show our take on it!

I'll be using a really simple example to demonstrate the project structure, that does the following:
1. Takes in a Player's Name and Age
2. Adds his details to a list. 
3. Click on the list item to delete the entry

The form for taking in the player's details and the list will be two different components that will communicate using Context API. This is a really trivial example, but should be enough to explain this approach.

This is going to be a relatively lengthy read, so, in case you want to figure it out yourself, here's the code sandbox link to the final solution:
{% codesandbox dawn-haze-lbsmq %}

Let's begin!

Assuming you have a basic react + typescript environment set up, create the following folders inside src directory:

1. **models** - Place data model here. In our case, the type-definition of Player.
2. **reducers** - Place your reducers and action generators inside this folder
3. **contexts** - Place your context providers in here.
4. **components** - Place your components here

Then, create our Player type-definition like so *(see the comment on top of the code snippets for the exact path)*:
```typescript
// scr/model/Player.ts
export type Player = {
  name: string;
  age: number;
};
```

We'll be keeping our data model as simple as possible. In our example, two fields should be enough.

Once our data model is defined, let's move on to **actions**. Here, we define what actions we allow our application to make on our data:
```typescript
// src/reducers/player/player-actions.ts
import { Player } from "../model/Player";

// Type of Actions allowed
export enum PlayerActionTypes {
  ADD_PLAYER = "ADD_PLAYER",
  REMOVE_PLAYER = "REMOVE_PLAYER"
}

// The data type of the action object.
// Usually, we only send the whole data in Add, and only a unique identifier in other actions
// But we'll ignore that in this example
export type PlayerAction = {
  type: PlayerActionTypes;
  payload?: Player;
};

// Action Generator for ADD
export const addPlayer = (player: Player) => {
  return {
    type: PlayerActionTypes.ADD_PLAYER,
    payload: player
  };
};

// Action Generator for Remove
export const removePlayer = (player: Player) => {
  return {
    type: PlayerActionTypes.REMOVE_PLAYER,
    payload: player
  };
};
```

We'll allow our application to either ADD or REMOVE a user. Comments in the code snippet explain what each line does. 

In case you are not aware of **Actions** or **Action Creators**, please check out this [link](https://redux.js.org/basics/actions).

Once our Actions are created, we need to define a **Reducer**. A reducer must be a **pure function** that takes in the current state and an action, and returns a new state. Here is how we define our reducer:

```typescript
// src/reducers/player/player-reducer.ts
import { Player } from "../../model/Player";
import { PlayerAction, PlayerActionTypes } from "./player-actions";

export const playerReducer = (state: Player[], action: PlayerAction) => {
  switch (action.type) {
    case PlayerActionTypes.ADD_PLAYER:
      return state.concat(action.payload);
    case PlayerActionTypes.REMOVE_PLAYER:
      return state.filter((player: Player) => player.name !== action.payload.name)
    default:
      return state;
  }
};
```

As you can see in the above snippet, the Reducer is simply a switch case on the action types. *Always ensure that you do not use methods that directly mutate the state*.

Now that we have our **Actions** and **Reducer** ready, it's time we start creating our context and context providers.

I'll break down Player context module into smaller chunks. We'll start with creating the context first:

```react
export const defaultPlayerListState: Player[] = [];

export type PlayerListContext = {
  playerList: Player[];
  playerDispatch: React.Dispatch<PlayerAction>;
};

export const playerListContext = React.createContext<
  PlayerListContext | undefined
>(undefined);
```
This is what the context would normally look like, but there's an excellent article by **Kent C. Dodds** that explains why splitting up the state and dispatch context is better for performance. Check it out by clicking [here](https://kentcdodds.com/blog/how-to-optimize-your-context-value).

So based on this newfound knowledge, let's change our context to look like this:

```react
export const playerListState = React.createContext<Player[] | undefined>(
  undefined
);

export const playerListDispatch = React.createContext<
  React.Dispatch<PlayerAction> | undefined
>(undefined);
```

Since we've split our state and dispatch into two separate contexts, let's create our custom context provider that will set up its children with both:
```react
export const PlayerListProvider = ({
  children
}: {
  children: React.ReactNode;
}) => {
  const [state, dispatch] = useReducer(playerReducer, []);
  return (
    <playerListState.Provider value={state}>
      <playerListDispatch.Provider value={dispatch}>
        {children}
      </playerListDispatch.Provider>
    </playerListState.Provider>
  );
};
```

Then, let's create a custom hook to let our consumer use our context:
```react
export const usePlayerListState = (): Player[] => {
  const context = React.useContext(playerListState);
  if (undefined === context) {
    throw new Error("Please use within PlayerListStateProvider");
  }
  return context;
};

export const usePlayerListDispatch = (): React.Dispatch<PlayerAction> => {
  const context = React.useContext(playerListDispatch);
  if (undefined === context) {
    throw new Error("Please use within PlayerListDispatchProvider");
  }
  return context;
};

```

We could directly use `React.useContext(playerListDispatch);` in our components, but having a custom hook lets us add additional functionality like error handling in this scenario when you try to use this context in a component that is not within its provider. This is also something I picked up from Kent C. Dodds. Here's a [link](https://kentcdodds.com/blog/how-to-optimize-your-context-value) to his article.

This is what our complete context module must look like:

```react
// src/context/Player.tsx
import React, { useReducer } from "react";
import { Player } from "../model/Player";
import { PlayerAction } from "../reducers/player/player-actions";
import { playerReducer } from "../reducers/player/player-reducer";

export const playerListState = React.createContext<Player[] | undefined>(
  undefined
);

export const playerListDispatch = React.createContext<
  React.Dispatch<PlayerAction> | undefined
>(undefined);

export const PlayerListProvider = ({
  children
}: {
  children: React.ReactNode;
}) => {
  const [state, dispatch] = useReducer(playerReducer, []);
  return (
    <playerListState.Provider value={state}>
      <playerListDispatch.Provider value={dispatch}>
        {children}
      </playerListDispatch.Provider>
    </playerListState.Provider>
  );
};

export const usePlayerListState = (): Player[] => {
  const context = React.useContext(playerListState);
  if (undefined === context) {
    throw new Error("Please use within PlayerListStateProvider");
  }
  return context;
};

export const usePlayerListDispatch = (): React.Dispatch<PlayerAction> => {
  const context = React.useContext(playerListDispatch);
  if (undefined === context) {
    throw new Error("Please use within PlayerListDispatchProvider");
  }
  return context;
};

```

We have our data model, our reducers, our actions and our context providers ready. Now let's begin building our components, starting with the form to accept the player details:

```react
// src/components/AddPlayer
import React from "react";

import { usePlayerListDispatch } from "../context/Player";
import { addPlayer } from "../reducers/player/player-actions";

export const AddPlayer = () => {
  const playerDispatch = usePlayerListDispatch();

  const [playerName, setPlayerName] = React.useState<string>("");
  const [playerAge, setPlayerAge] = React.useState<string>("");

  const onSubmitHandler = event => {
    event.preventDefault();
    if (playerName !== "" && playerAge !== "" && !isNaN(Number(playerAge))) {
      playerDispatch(
        addPlayer({
          name: playerName,
          age: Number(playerAge)
        })
      );
      setPlayerName("");
      setPlayerAge("");
    }
  };

  return (
    <form onSubmit={onSubmitHandler}>
      <label htmlFor="playerName">Player Name: </label>
      <input
        type="text"
        placeholder="Enter Player Name"
        name="playerName"
        id="playerName"
        value={playerName}
        onChange={event => setPlayerName(event.target.value)}
      />
      <br />
      <label htmlFor="playerAge">Player Age: </label>
      <input
        type="number"
        placeholder="Entery Player Age"
        name="playerAge"
        id="playerAge"
        value={playerAge}
        onChange={event => setPlayerAge(event.target.value)}
      />
      <br />
      <input type="submit" className={`btn btn-primary`} name="submit" />
    </form>
  );
};

```
The above component only adds data, never reads it. So we don't need to use usePlayerListState hook. This is where decoupling the state and dispatch is useful.

Then, we build our player list component to display the list of players. We've split it into two components:

```react
// src/components/PlayerList
import React from "react";
import { usePlayerListState } from "../../context/Player";
import { PlayerItem } from "./Player";

export const PlayerList = () => {
  const playerList = usePlayerListState();

  return (
    <>
      {playerList.map(player => {
        return (
          <>
            <PlayerItem player={player} />
            <br />
          </>
        );
      })}
    </>
  );
};

```

```react
// src/components/Player
import React from "react";
import { usePlayerListDispatch } from "../../context/Player";
import { removePlayer } from "../../reducers/player/player-actions";
import { Player } from "../../model/Player";

export const PlayerItem = ({ player }: { player: Player }) => {
  const playerDispatch = usePlayerListDispatch();
  return (
    <span
      onClick={() => {
        playerDispatch(removePlayer(player));
      }}
    >
      {`Player ${player.name} is ${player.age} years old`}
    </span>
  );
};


```

The above examples are fairly simple. The PlayerList components takes a list of players and passes each player's details to the Player component that display individual player information and dispatches a remove action when you click on it.

We have everything ready. It's time to wire things up. Open up App.tsx and place the following lines of code inside:

```react
export default function App() {
  return (
    <PlayerListProvider>
      <AddPlayer />
      <PlayerList />
    </PlayerListProvider>
  );
}

```

And voila! We're done! Play around with the codesandbox I've linked in the beginning and let me know what you think in the comments section! 

Personally, what I like about this approach is that everything Context API related is tucked away inside our custom hooks. Some of my friends I showed this to didn't even realize it was using Context API!