DEV Community

Sergey Todyshev
Sergey Todyshev

Posted on

2 2

createState function using React Context and ImmerJS

In this post I am going to show a function for global state management in React applications
using React Context and ImmerJS.
It is heavily inspired by this post.

Enough words, please read the final code :)

import React, { useContext, useEffect, useReducer } from "react";
import { useRouter } from "next/router";
import produce from "immer";
import mapValues from "lodash/mapValues";

type Children = { children?: any };
type ProviderFC = React.FC<Children>;

type MutationFn<T> = (state: Partial<T>) => void;
type Mutations<T> = {
  [name: string]: (state: T, ...args: any[]) => void;
};

type Update<T, M extends Mutations<T>> = { setState: MutationFn<T> } & M;
type UseStateResult<T, M extends Mutations<T>> = [T, MutationFn<T>, M];
type UseStateFn<T, M extends Mutations<T>> = () => UseStateResult<T, M>;

export default function createState<T, M extends Mutations<T>>(
  {
    initialState,
    loadState = () => Promise.resolve(initialState),
  }: {
    initialState: T;
    loadState?: () => Promise<T>;
  },
  mutations?: M
): [ProviderFC, UseStateFn<T, M>] {
  const StateContext = React.createContext<T>(initialState);
  const UpdateContext = React.createContext<Update<T, M>>(null as any);

  function Provider({ children }) {
    const router = useRouter();
    const [state, update] = useReducer(produce, initialState);
    const mutate = update as any;

    const setState = (newState: Partial<T>) => {
      mutate((target: any) => {
        Object.assign(target, newState);
      });
    };

    const actions = mapValues(mutations, (fn) => (...params: any[]) => {
      mutate((target: any) => {
        fn(target, ...params);
      });
    });

    const loadAsync = async () => {
      const newState = await loadState();
      mutate((target: any) => {
        Object.assign(target, newState);
      });
    };

    // TODO avoid this effect if loadAsync is not defined
    useEffect(() => {
      loadAsync();
    }, [router.pathname]);

    return (
      <UpdateContext.Provider value={{ setState, ...actions } as any}>
        <StateContext.Provider value={state as any}>
          {children}
        </StateContext.Provider>
      </UpdateContext.Provider>
    );
  }

  function useState(): UseStateResult<T, M> {
    const { setState, ...mutations } = useContext(UpdateContext);
    return [useContext(StateContext), setState, mutations as any];
  }

  return [Provider, useState];
}
Enter fullscreen mode Exit fullscreen mode

The createState function can be used as follow:

// auth state
const initialState = {
  id: "",
  name: "",
  email: "",
  is_owner: false,
  is_admin: false,
  is_authenticated: !!getApiToken(),
};

type State = typeof initialState;

const [UserProvider, useUser] = createState({
  initialState,
  loadState: fetchAuthState,
});

export { UserProvider, useUser };

async function fetchAuthState(): Promise<State> {
  if (!getApiToken()) {
    return initialState;
  }
  try {
    const resp = await api.me();
    return {
      ...initialState,
      ...resp.data,
      is_authenticated: true,
    };
  } catch (err) {
    return initialState;
  }
}

// example login page
function LoginPage() {
   const [_, setUser] = useUser();
   const handleSubmit = async () => {
       const resp = await api.login(form);
       setUser({
            ...omit(resp.data, ['token', "token_expired_at"]),
          is_authenticated: true,
       });
   };
   return <div> Login Form </div>
}
Enter fullscreen mode Exit fullscreen mode

The example is truncated a bit to save your time.

This function allows specifying custom typed mutations as an object with functions accepting current state to be mutated. This can be optionally used to decompose a reducer function with big switch into smaller mutation functions. And you don't need to define actions, just define functions with parameters.

Enjoy! EOF 😄

Link to original post

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay