DEV Community

Cover image for Custom complex React Context and TypeScript
Michele
Michele

Posted on

Custom complex React Context and TypeScript

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

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

And then we add these interfaces to UserContext:

const UserContext = createContext<
  | {
      state: ContextInterface;
      dispatch: DispatchInterface;
    }
  | undefined
>(undefined);
Enter fullscreen mode Exit fullscreen mode

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

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.");
  }
};
Enter fullscreen mode Exit fullscreen mode

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>.
  );
};
Enter fullscreen mode Exit fullscreen mode

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

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;
};
Enter fullscreen mode Exit fullscreen mode

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 };
Enter fullscreen mode Exit fullscreen mode

Implement the provider

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;
Enter fullscreen mode Exit fullscreen mode

Handle the logic

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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

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;
Enter fullscreen mode Exit fullscreen mode

Dispatch the values

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

const handleUserContext = useUser();
Enter fullscreen mode Exit fullscreen mode

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

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

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

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.

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.

Top comments (1)

Collapse
 
jdbertron profile image
J.D. Bertron

This was awesome, thank you for doing this.