Custom complex React Context and TypeScript
Tags: react, typescript, javascript, tutorial
Published: 2021-04-28 18:10:36

One of the fundamental aspects when developing a _website_, an _application_ or simply a program, is the use of components that are as reusable as possible, as the **DRY** (*Don't repeat yourself!*) rule explains.

When developing a web app, especially if it is very *complex*, it is very important to follow this approach, in order to be able to maintain all the components and functions in a much simpler way.
In this article, we're going to see how React Context can help us in sharing values in all the children of the context and how to create custom and more complex ones (with **hooks**, **reducers**, **memoization**). In addition, we will also add strong **TypesScript** support.


##Summary
* [Create the project] (#create-the-project)
* [Add types] (#add-types)
* [Create the custom provider] (#create-the-custom-provider)
* [Create the custom hook] (#create-the-custom-hook)
* [Implement the provider] (#implement-the-provider)
* [Handle the logic] (#handle-the-logic)
* [Dispatch the values] (#dispatch-the-values)
* [Epilogue] (#epilogue)


###Create the project <a name="create-the-project"></a>

First, let's create the project, through CRA:

`npx create-react-app example --template typescript`

And then in /src/contexts (create if doesn't exists) we create `userContext.tsx`:


```
import React, { useContext, createContext, useMemo, useReducer } from "react";

const UserContext = createContext();

export default UserContext;
```

### Add types

Next, we add the types of both the context and the reducers:

```
interface ContextInterface {
  id?: string;
}

interface ActionInterface {
  type: setUser
  payload: ContextInterface;
}

type DispatchInterface = (action: ActionInterface) => void;
```

And then we add these interfaces to UserContext:

```
const UserContext = createContext<
  | {
      state: ContextInterface;
      dispatch: DispatchInterface;
    }
  | undefined
>(undefined);
```

We give it an initial value of `undefined`, so that later, when we create the provider, we'll pass the reducer to it.


### Create the custom provider <a name="create-the-custom-provider"></a>


But first, we're going to create the reducer:

```
const reducerUser = (
  state: ContextInterface,
  action: ActionInterface
): ContextInterface => {
  switch (action.type) {
    case "setUser":
      return { ...state, id: action.payload.id };
    default:
      throw new Error("Invalid action type in context.");
  }
};
```

Let's now create the custom provider of the `userContext` and also declare the reducer, which we will pass as a *value* to the provider:

```
const UserProvider: React.FC = ({ children }) => {
  const [state, dispatch] = useReducer(reducerUser, {});

  const memoizedUser = useMemo(() => ({ state, dispatch }), [state, dispatch]);

  return (
    <UserContext.Provider value={memoizedUser}>{children}</UserContext.Provider>.
  );
};
```

In case our context is very complex and the value needs to be updated often, I suggest to use **useMemo**, so React won't do any *re-rendering* in case the value is equal to the previous one.
In case the context is very simple (like in this case), it's not essential to do this, on the contrary, **using useMemo when you don't need it, leads to lower performance.** It is shown here as an example only.

### Create the custom hook <a name="create-the-custom-hook"></a>

Now, let's create our custom hook that will allow us to fetch the id of the user from the children of the context.

```
const useUser = () => {
  const user = useContext(UserContext);

  return user;
};
```

So, user, will contain **state** and **dispatch**, with which we're going to *display* and *update* the user id.

And finally, we export everything:

```
export { UserProvider, useUser };
```

###Implement the provider <a name="implement-the-provider"></a>

Let's move to `App.tsx` and implement what we just created. Let's wrap everything inside our context:

```
import React from react;

import { Dashboard, UserProvider } from "./index.d";

const App: React.FC = () => {
  return (
    <UserProvider>
      <Dashboard />
    </UserProvider>
  );
};

export default App;
```

###Handle the logic <a name="handle-the-logic"></a>

In `Dashboard.tsx`, we will import the `useUser` hook created earlier and with that we will check the id. If it isn't undefined, then, it will show the login.
Otherwise, it will show a simple dashboard that shows the user the user id:

```
import React from react;
import { useUser, Login } from "../index.d";

const Dashboard: React.FC = () => {
  const userContext = useUser();

  if (!userContext!.state.id) return <Login />;

  return (
    <div>
      <h2>Dashboard</h2>>

      <p>
        User logged with <em>id</em>: <strong>{userContext!.state.id}</strong>
      </p>
    </div>
  );
};
```

As soon as we open the page, the id will obviously be `undefined`, because no one logged in.
So, we'll be shown the login page (in `Login.tsx`):

![Styled login page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v7x4w15uiw8q5s7kqjs6.png)

```
import React, { useState } from react;
import { useUser } from "../index.d";

const Login: React.FC = () => {
  const [username, setUsername] = useState<string>("");
  const [password, setPassword] = useState<string>("");

  const handleLogin = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    loginTheUser().then((id) => {});
  };

  return (
    <div>
      <div>
        <h1>Login</h1>.
        <form onSubmit={handleLogin}>
          <div>
            <input
              id="user"
              type="text"
              value={username}
              placeholder="Username"
              onChange={(e) => setUsername(e.target.value)}
            />
          </div>
          <div>
            <input
              type="password"
              id="password"
              value={password}
              placeholder="Password"
              onChange={(e) => setPassword(e.target.value)}
            />
          </div>
          <button type="submit">sign in</button>
        </form>
      </div>
    </div>
  );
};

export default Login;
```

### Dispatch the values <a name="dispatch-the-values"></a>

To make the context work, however, you must import the custom hook:

```
const handleUserContext = useUser();
```

And finally, we add the dispatch call that updates our state:

```
const handleLogin = () => {
    loginTheUser().then((id) =>
      handleUserContext!.dispatch({ type: "setUser", payload: { id: id } })
    );
  };
```

Ok, now, after logging in, the message we wrote will appear. 
It seems to be working, perfect! But what if you want to pass it between *multiple components*? Do you have to pass it as a prop in the children?

No, otherwise the point of Context would be lost. To display or update the id, just call the hook from a UserContext child and use the state and dispatch variables to update it.
Simple, isn't it?

### Epilogue <a name="epilogue"></a>


Now, before we wrap it up, we can install *styled-components* and add some simple (and ugly) *CSS* to our project and, to see it finished, I refer you to the [repo on Github](https://github.com/demic-dev/React-Custom-Context).

This here is just a basic example, but it can come in very handy when developing complex web apps, where there are some data that need to be passed in all children (such as authentication, or global settings, like dark mode).


Thanks for reading this article! If you encountered any errors or if you want to add something, leave a comment!

[Github repo.](https://github.com/demic-dev/React-Custom-Context)