Opinionated React: Use Context for Shared State
Tags: react, javascript, redux, codequality
Published: 2020-06-09 23:14:16

# Intro
I’ve been working with React for over four years. During this time, I’ve formed some opinions on how I think applications should be. This is part 5 in the series of such opinionated pieces.

# My Pattern for React Context
My buddy [Nader](https://twitter.com/dabit3) asked how I use React Context in my apps. I promised I'd write about it, so here we are.

{% twitter 1233180102747246593 %}

# Why
There are some instances in your application state is needed by multiple components. I will use context if this shared state requires a lot of prop drilling. In the past, Redux was a popular solution to avoid prop drilling. However, I don't believe Redux is needed anymore. React's context api works great for this.

# Use Cases - Important!
- You should use React context for global state. That being said, there aren't that many pieces of global state. Some good examples of global state are the current user, the current language setting, or a map of feature flags.

- You don't need to use context only for global state. Context can be applied to a specific sub-tree of your application. 

- It's common to have multiple sub-tree specific contexts.

# Inspiration
I originally learned this pattern from [Kent C. Dodd's](https://twitter.com/kentcdodds) excellent post [How to use React Context effectively](https://kentcdodds.com/blog/how-to-use-react-context-effectively), I recommend reading this. Tanner Linsley also covers similar concepts in his talk [Custom Hooks in React: The Ultimate UI Abstraction Layer](https://www.youtube.com/watch?v=J-g9ZJha8FE&feature=emb_title).

# Example
The end goal is to have an api that looks like this.
```tsx
export const App = ({ userId }) => {
  return (
    <UserProvider id={userId}>
      <Dashboard />
    </UserProvider>
  );
};

const Dashboard = () => {
  const { isLoading, user } = useUserState();
  if (isLoading) {
    return <div>Loading...</div>;
  }
  return (
    <div>
      <h1>Dashboard</h1>
      <div>Hello {user.displayName}!</div>
    </div>
  );
};

```
Let's work backwards to get to this solution.

First, let's start with defining the state of our context, as well as the two contexts we will be creating.

```tsx
interface UserState {
  user?: User;
  isLoading: boolean;
}

const UserStateContext = React.createContext<UserState | undefined>(undefined);
const UserDispatchContext = React.createContext<UserDispatch | undefined>(
  undefined
);
```

We are creating two separate contexts because not all components will need access to both `state` and `dispatch`. This way, a component can use only the context it requires. The added benefit is that if a component is only using `dispatch`, it will not re-render on `state` change because it is not using that context.

For state management within the context, we're going to use `useReducer`.
```tsx
// omitted rest of the file

enum UserActionTypes {
  LOADING = "loading",
  SUCCESS = "success"
}
type UserAction =
  | { type: UserActionTypes.LOADING }
  | { type: UserActionTypes.SUCCESS; payload: User };
type UserDispatch = (action: UserAction) => void;

function userReducer(state: UserState, action: UserAction): UserState {
  switch (action.type) {
    case UserActionTypes.LOADING: {
      return { isLoading: true };
    }
    case UserActionTypes.SUCCESS: {
      return { isLoading: false, user: action.payload };
    }
    default: {
      throw new Error("Invalid action type");
    }
  }
}
```
I often write contexts like this. At app startup, we'd like to fetch information about the currently logged in user, and make that data available globally.

The user you want to fetch might be determined by an id, and since Provider components can accept props, we can simply pass in an `id` so when our Context mounts, we fetch the user.

Here's what the provider component looks like.
```tsx
export const UserProvider: React.FC<{ id: string }> = ({ id, children }) => {
  const [state, dispatch] = React.useReducer(userReducer, { isLoading: true });

  React.useEffect(() => {
    const handleGetUser = async id => {
      dispatch({ type: UserActionTypes.LOADING });
      const user = await getUserById(id);
      dispatch({ type: UserActionTypes.SUCCESS, payload: user });
      return;
    };
    handleGetUser(id);
    return;
  }, [id]);

  return (
    <UserStateContext.Provider value={state}>
      <UserDispatchContext.Provider value={dispatch}>
        {children}
      </UserDispatchContext.Provider>
    </UserStateContext.Provider>
  );
};
```
In most of my applications, I'm using hooks, so we will define the hooks here.

```tsx
export const useUserState = () => {
  const userStateContext = React.useContext(UserStateContext);
  if (userStateContext === undefined) {
    throw new Error("useUserState must be used within a UserProvider");
  }
  return userStateContext;
};

export const useUserDispatch = () => {
  const userDispatchContext = React.useContext(UserDispatchContext);
  if (userDispatchContext === undefined) {
    throw new Error("useUserDispatch must be used within a UserProvider");
  }
  return userDispatchContext;
};
```
# Wrapping Up
Here's everything together:
```tsx
import * as React from "react";
import { getUserById } from "../services/user-service";
import { User } from "../types/user";

interface UserState {
  user?: User;
  isLoading: boolean;
}
enum UserActionTypes {
  LOADING = "loading",
  SUCCESS = "success"
}
type UserAction =
  | { type: UserActionTypes.LOADING }
  | { type: UserActionTypes.SUCCESS; payload: User };
type UserDispatch = (action: UserAction) => void;

const UserStateContext = React.createContext<UserState | undefined>(undefined);
const UserDispatchContext = React.createContext<UserDispatch | undefined>(
  undefined
);

function userReducer(state: UserState, action: UserAction): UserState {
  switch (action.type) {
    case UserActionTypes.LOADING: {
      return { isLoading: true };
    }
    case UserActionTypes.SUCCESS: {
      return { isLoading: false, user: action.payload };
    }
    default: {
      throw new Error("Invalid action type");
    }
  }
}

export const UserProvider: React.FC<{ id: string }> = ({ id, children }) => {
  const [state, dispatch] = React.useReducer(userReducer, { isLoading: true });

  React.useEffect(() => {
    const handleGetUser = async id => {
      dispatch({ type: UserActionTypes.LOADING });
      const user = await getUserById(id);
      dispatch({ type: UserActionTypes.SUCCESS, payload: user });
      return;
    };
    handleGetUser(id);
    return;
  }, [id]);

  return (
    <UserStateContext.Provider value={state}>
      <UserDispatchContext.Provider value={dispatch}>
        {children}
      </UserDispatchContext.Provider>
    </UserStateContext.Provider>
  );
};

export const useUserState = () => {
  const userStateContext = React.useContext(UserStateContext);
  if (userStateContext === undefined) {
    throw new Error("useUserState must be used within a UserProvider");
  }
  return userStateContext;
};

export const useUserDispatch = () => {
  const userDispatchContext = React.useContext(UserDispatchContext);
  if (userDispatchContext === undefined) {
    throw new Error("useUserDispatch must be used within a UserProvider");
  }
  return userDispatchContext;
};

```

This is the fifth post in a series of posts I will be doing. If you enjoyed this, please give me some hearts and leave a comment below. What else would you

As always, I'm open to recommendations.

Thanks for reading.