diy: from useReducer to useState

published 9 February 2022 · 4 min read

Make your own version of the React useState hook, using TypeScript and the useReducer hook.

#tutorial#typescript#javascript#react#jsx

why did i try this?

This originated as an exercise I saw for a tutorial revolving around the useReducer hook, and upon reading the prompt I thought to myself, "why should I not give this a try?"

getting started

This is how I set up the NextJS project I used to work on this exercise, but you can use Create React App or anything else you want.

Start by typing

yarn create next-app --ts use-custom-state

inside of your terminal window, which will create a new TypeScript NextJS project called use-custom-state. After opening the project in your favourite text editor, create a new folder called hooks, and inside this new folder, create a file called useCustomState.ts (NOT .tsx). This will be where you work on your custom useState hook. At the top of this file, write the following to import the useReducer hook:

import { useReducer } from 'react';

It is assumed that you have good knowledge of the useState hook before reading the rest of this article.

analysing the useState function

The useState function looks like this

const [state, setState] = useState(0);

It takes in a default value, and returns an array of two things. The first thing, state, is the object representing the current state, and the second thing, setState, is a function that can change the current state value. The setState function either takes in the new value or a value that returns the next function as the parameter.

This quick analysis will help us create the type definitions.

creating the types

First of all, we need to type the parameters of the setState function mentioned above using generics:

type CustomStateActionPayload<T> = T | ((prev: T) => T);

We also need to type the setState function, the action that we will feed into the reducer, and the eventual type of the useCustomState hook:

// Type for the reducer's action
type CustomStateAction<T> = {
  type: 'SET';
  payload: CustomStateActionPayload<T>;
};
// Type for the setState function returned by the hook
type SetCustomState<T> = (payload: CustomStateActionPayload<T>) => void;
// Type for the useCustomState hook
type UseCustomState<T> = [T, SetCustomState<T>];

Having these types will allow use to type the hook as strictly as possible.

writing the hook

First of all, write and export the function signature. We will write the reducer within the hook:

const useCustomState = <T>(defaultState: T): UseCustomState<T> => {
  // Reducer goes here
};

export default useCustomState;

Afterwards, we will write the reducer within the hook function signature:

const stateReducer = (state: T, action: CustomStateAction<T>): T => {
  switch (action.type) {
    case 'SET': {
      if (action.payload instanceof Function) {
        const nextValue = action.payload(state);
        return nextValue;
      }

      return action.payload;
    }

    default: {
      return state;
    }
  }
};

Once we are done, we will set up the useReducer hook and create our copy version of the setState function:

const [state, dispatch] = useReducer(stateReducer, defaultState);

// Our custom setState function
const setState = (payload: CustomStateActionPayload<T>) => {
  dispatch({
    type: 'SET',
    payload,
  });
};

return [state, setState];

This is the final code of the entire hook:

import { useReducer } from 'react';

// Types
type CustomStateActionPayload<T> = T | ((prev: T) => T);

type CustomStateAction<T> = {
  type: 'SET';
  payload: CustomStateActionPayload<T>;
};

type SetCustomState<T> = (payload: CustomStateActionPayload<T>) => void;
type UseCustomState<T> = [T, SetCustomState<T>];

// Reducer
const useCustomState = <T>(defaultState: T): UseCustomState<T> => {
  const stateReducer = (state: T, action: CustomStateAction<T>): T => {
    switch (action.type) {
      case 'SET': {
        if (action.payload instanceof Function) {
          const nextValue = action.payload(state);
          return nextValue;
        }

        return action.payload;
      }

      default: {
        return state;
      }
    }
  };

  const [state, dispatch] = useReducer(stateReducer, defaultState);

  const setState = (payload: CustomStateActionPayload<T>) => {
    dispatch({
      type: 'SET',
      payload,
    });
  };

  return [state, setState];
};

export default useCustomState;

test it out

In the styles/globals.css file, replace everything with

.App {
  font-family: sans-serif;
  text-align: center;
}

And finally, in the pages/index.tsx file, replace everything inside with

import type { NextPage } from 'next';
import useCustomState from '../hooks/useCustomState';

const Home: NextPage = () => {
  const [count, setCount] = useCustomState(0);

  return (
    <div className="App">
      <h1>{count}</h1>

      <button onClick={() => setCount((prev) => prev + 1)}>UP</button>
      <button onClick={() => setCount(0)}>RESET</button>
      <button onClick={() => setCount((prev) => prev - 1)}>DOWN</button>
    </div>
  );
};

export default Home;

The final product should be a counter that looks something like this:

the final counter

You can press up to increment the counter by 1, down to decrement it by 1, and reset to set the value of the counter back to 0. Try out the finished app here.

That is all for this post – I hope you found the post to be enjoyable and informative. If you liked this, or if you have something to say, feel free to leave me a message.