React context API state management with typescript
Tags: react, contextapi, typescript, reactstate
Published: 2021-11-03 07:33:16

## Initial setup
We will use the default `npx create-react-app app_name --template typescript --use-npm ` for anyone with both `npm` and `yarn` installed in the system or `npx create-react-app app_name` for just `npm`  to setup our initial project
I'll call my app `client` for the start

## My directory structure
<pre>
client
|-node_modules
|- public
|- src
|      ├── App.css
|      ├── App.tsx
|      ├── index.tsx
|      ├── react-app-env.d.ts
|      ├── components
│      |    ├── Header.tsx
│      |    └── Home.tsx
|      |
|      |
|      └── state
|      |    ├── ActionTypes.tsx
|      |    ├── AppProvider.tsx
|      |    ├── interfaces.tsx
|      |    └── reducers
|      |        ├── themeReducer.tsx
|      |        └── userReducer.tsx

</pre>

First we'll create a directory in the `src` folder named `state` for keeping all files related to our global state. For `reducer` functions we'll create a folder in the state named reducers.
In the AppProvider we'll import `createContext` from react to create a context instance for holding our global state and sharing the state value across all children bellow it.

In the handling of different states it's good if we keep the reducers to handle only a concerning section of the state for easy maintainance. In my state I have two states i.e `user` and `theme`.
I have defined all types for the AppState already in the interfaces.tsx.

The `combined reducer` function takes in a given state and passess it to the appropriate `reducer` function. We destructure the state in the `combinedReducer` arguments and return the state after any update.

To keep a persistent state in the application we use localstorage to store our data. I have set up an `APP_STATE_NAME` variable to ensure consistency and ease of access to the localstorage varible.
We first check if there is an existing state in the localstorage, if there is no state registered we use the default state value after.

For syncing state in the AppProvider we import the `useReducer` hook from `react` for dispatching events on our state.
We pass the state to the AppContext as value. In addition to ensure we keep the app state in sync we use the `useEffect` hook to watch for changes in the state and refresh the state in case of any change.

### AppProvider.tsx

```ts
/**
 * AppProvider.tsx
 */

import React, { createContext, Dispatch, useEffect, useReducer } from "react";
import { IState, IThemeAction, StateActions, UserActions } from "./interfaces";
import themeReducer from "./reducers/themeReducer";
import userReducer from "./reducers/userReducer";
const APP_STATE_NAME = "testing";

//Check if state already exist and take the instance or set a default value
//in case there is no state in the localstorage
const initialState: IState = JSON.parse(localStorage.getItem(APP_STATE_NAME)!)
  ? JSON.parse(localStorage.getItem(APP_STATE_NAME)!)
  : {
      user: {
        username: "",
        active: false,
      },
      theme: {
        dark: false,
      },
    };

const AppContext = createContext<{
  state: IState;
  dispatch: Dispatch<StateActions>;
}>({ state: initialState, dispatch: () => null });

const combinedReducers = (
  { user, theme }: IState,
  action: UserActions | IThemeAction
) => ({
  user: userReducer(user, action),
  theme: themeReducer(theme, action),
});

const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const [state, dispatch] = useReducer(combinedReducers, initialState);
  // Watches for any changes in the state and keeps the state update in sync
  //Refresh state on any action dispatched
  useEffect(() => {
    //Update the localstorage after detected change
    localStorage.setItem(APP_STATE_NAME, JSON.stringify(state));
  }, [state]);
  return (
    <AppContext.Provider value={{ state, dispatch }}>
      {children}
    </AppContext.Provider>
  );
};

export default AppProvider;
export { AppContext, AppProvider };
```
## interfaces 

Defining my types

```ts
/**
 * interfaces.tsx
 */
import { LOGIN, LOGOUT, THEME } from "./ActionTypes";
export interface IUser {
  username: string;
  active: boolean;
}
export interface ITheme {
  dark: boolean;
}

export interface IState {
  user: IUser;
  theme: ITheme;
}

export interface IUserLogin {
  type: typeof LOGIN;
  payload: IUser;
}

export interface IUserLogout {
  type: typeof LOGOUT;
  payload: {};
}

export interface IThemeAction {
  type: typeof THEME;
  payload: { toggle: boolean };
}

export type UserActions = IUserLogin | IUserLogout;
export type StateActions = UserActions | IThemeAction;
```

### Action types

My action types

```ts
/**
 * ActionTypes.tsx
 */

const LOGIN = "LOGIN";
const LOGOUT = "LOGOUT";
const THEME = "THEME";
// const LOGIN = "LOGIN"
// const LOGIN = "LOGIN"

export default Object.freeze({ LOGIN, LOGOUT, THEME });
export { LOGIN, LOGOUT, THEME };
```
### themeReducer.tsx
A reducer function that only handles state concerning the state theme
```ts
import { THEME } from "../ActionTypes";
import { ITheme, StateActions } from "../interfaces";

const themeReducer = (theme: ITheme, action: StateActions) => {
  switch (action.type) {
    case THEME:
      return { ...theme, ...action.payload };
    default:
      return theme;
  }
};

export default themeReducer;
```

### userReducer.tsx
A reducer function that only handles state concerning the state user
```ts
import { LOGIN, LOGOUT } from "../ActionTypes";
import { IUser, StateActions } from "../interfaces";

const userReducer = (user: IUser, action: StateActions) => {
  const { type, payload } = action;
  switch (type) {
    case LOGIN:
      return { ...user, ...payload };
    case LOGOUT:
      return { ...user, username: "", active: false };
    default:
      return user;
  }
};
export default userReducer;
```
### index.tsx
For us to get access to the global state we have to wrap out app with the `AppProvider` 
```ts
/**
 * index.tsx
 */
import ReactDOM from "react-dom";
import App from "./App";
import AppProvider from "./state/AppProvider";

ReactDOM.render(
  <AppProvider>
    <App />
  </AppProvider>,
  document.getElementById("root")
);
```

## header.tsx

In our header we can aacess the state of the app via a `useContext`  hook to get access to the state and pass our `AppContext` instance to get current state of the application

```ts
/**
 * Header.tsx
 */

import { useContext } from "react";
import { AppContext } from "../state/AppProvider";

const Header = () => {
  const { state } = useContext(AppContext);
  return (
    <header>
      <div className="left">LOGO</div>
      <div className="right">
        <ul>
          <li>
            <a href="/">My pages</a>
          </li>
          <li>
            <a href="/">{state.user.active ? state.user.username : "Login"}</a>
          </li>
        </ul>
      </div>
    </header>
  );
};

export default Header;
```
## Home.tsx
In the `Home.tsx` using the `useContext` hook we can destructure the context value object to get access to the state and the dispatch method for invoking our reducers
```ts
/**
 * Home.tsx
 */

import { useContext } from "react";
import { LOGIN, LOGOUT } from "../state/ActionTypes";
import { AppContext } from "../state/AppProvider";

const Home = () => {
  const { state, dispatch } = useContext(AppContext);
  const { user } = state;
  const hendleLogin = () => {
    dispatch({
      type: LOGIN,
      payload: { active: true, username: "Mike" },
    });
    console.log(state);
  };
  const hendleLogout = () => {
    dispatch({
      type: LOGOUT,
      payload: { username: "", active: false },
    });
  };
  return (
    <div className="home-container">
      <p>{user.active ? user.username : "No user"}</p>
      <div>
        <button
          className="login"
          {...(user.active ? { disabled: true } : { disabled: false })}
          onClick={hendleLogin}
        >
          Login
        </button>
        <button
          className="logout"
          {...(!user.active ? { disabled: true } : { disabled: false })}
          onClick={hendleLogout}
        >
          Logout
        </button>
      </div>
    </div>
  );
};

export default Home;
```

## App.tsx
```ts
/**
 * App.tsx
 */
import "./App.css";
import Header from "./components/Header";
import Home from "./components/Home";
const App = () => {
  return (
    <div>
      <Header />
      <Home />
    </div>
  );
};

export default App;
```

Thank you so much for reading and hope you learn from this. Here is a link to the code on github [Code sample](https://github.com/forinda/dev-blog-react-context-state.git)
For any queries just give in the comments below