The advanced guide to React Context with hooks.

Practical guide

Hi all,

It's been almost 4 years when React team has been released hooks, the addition to React function component to use state and control the mounting of the apps via Effects lifecycle method.

In this guide you'll have a practical guide how to use hooks with Context api (the alternative of redux for small projects [my opinion]).

I'll use Typescript if you're not comfortable with it simply just remove the types 😬.

Our project is for authentication process, you have to think about it like separate package that you can use for your app and it will handle everything.

Let's get started....

#1. Create React app:

yarn create react-app my-app --template typescript

then create a directory: src/auth

#2.

we have to ask ourselves what are the things that needed for authentication for now

  • Wrapper to wrap our app(Provider).
  • Component the will inject the props that we need in components tree(Consumer).
  • Events can be listened from any component tree to auth module.
  • Couple hooks that makes our life easier 🙂.

Provider:

We start by creating context that will expose a higher order component called AuthProvider.

create file under src/auth/AuthContext.ts and fill it with:

src/auth/AuthContext.ts

import { createContext } from "react";

export interface IAuthContext {
  register: (email: string, password: string) => void;
  login: (email: string, password: string) => void;
  isAuthenticated: boolean;
}

export const AuthContext = createContext<IAuthContext | undefined>(undefined);

You can see in the interface that I have defined login, register and isAuthenticated which is the value that we will rely on our app.

And then create a file you can call it AuthProvider with this content.

src/auth/AuthProvider.tsx

import React, { ReactElement } from "react";
import { IAuthContext, AuthContext } from "./AuthContext";

interface IAuthProviderProps {
  children: ReactElement<any> | ReactElement<any>[];
}
export function AuthProvider({ children }: IAuthProviderProps) {
  return (
    <AuthContext.Provider
      value={{
        login: () => {},
        register: () => {}
        isAuthenticated: false,
      }}
    >
      {children}
    </AuthContext.Provider>
  );
}

This a higher order component that can wrap our app which is the children that you pass down and it will check whenever the value is changed and it will render the children again( the normal behaviour of react).

Now in our app we could wrap like this:

App.tsx

import React from "react";
import { render } from "react-dom";
import { AuthProvider } from "./auth/AuthProvider";
import MyComponent from "./MyComponent";

const App = () => (
  <AuthProvider>
    <MyComponent />
  </AuthProvider>
);

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

MyComponent will have the interaction with UI when button is pressed for register or login and depends to to the value of isAuthenticated our app will render.

Let's see MyComponent:

import React from "react";

interface Props {}

export default function MyComponent(props: Props) {
  const onSubmit = (e: any) => {};

  return (
    <div>
      <h1>Login </h1>
      <form onSubmit={onSubmit}>
        <input type="text" onChange={() => {}} name="email" />
        <input type="password" onChange={() => {}} name="password" />
      </form>
    </div>
  );
}

To handle input value with two inputs, we will create a custom hooks that handle it, and also handle onSubmit

import React, { useState } from "react";

interface Props {}

export default function MyComponent(props: Props) {
  const email = useInputValue(); //** added */
  const password = useInputValue(); //** added */

  const onSubmit = (e: any) => {
    e.preventDefault();
    const { value: emailValue } = email;
    const { value: passValue } = password;
    if (
      emailValue &&
      emailValue.trim() !== "" &&
      passValue &&
      passValue.trim() !== ""
    ) {
      //login
    } else {
      return;
    }
  };

  return (
    <div>
      <h1>Login </h1>
      <form onSubmit={onSubmit}>
        <input type="text" name="email" {...email} />
        <input type="password" name="password" {...password} />
      </form>
    </div>
  );
}
//** added */
const useInputValue = (defaultValue: string = "") => {
  const [val, setVal] = useState(defaultValue);
  const handleChange = (e: any) => setVal(e.target.value);

  return {
    value: val,
    onChange: handleChange
  };
};

In order to access login function we need to have Consumer to access the values login, register

Consumer hook:

create a file in auth/useAuthentication.ts with content:

src/auth/useAuthentication.ts

import React, { useContext } from "react";
import { AuthContext, IAuthContext } from "./AuthContext";

export default function useAuthentication(): IAuthContext | undefined {
  return useContext(AuthContext);
}

It will only expose the context to access the values in the Provider.

Now, we will use it in MyComponent like this:

src/components/MyComponent.tsx

import React, { useState } from "react";
import useAuthentication from "./auth/useAuthentication"; //** added */

interface Props {}

export default function MyComponent(props: Props) {
  const email = useInputValue();
  const password = useInputValue();
  const context = useAuthentication();//** added */

  const onSubmit = (e: any) => {
    e.preventDefault();
    const { value: emailValue } = email;
    const { value: passValue } = password;
    if (
      emailValue &&
      emailValue.trim() !== "" &&
      passValue &&
      passValue.trim() !== ""
    ) {
      //** added */
      context.login(emailValue, passValue);
    } else {
      return;
    }
  };

  return (
    <div>
      <h1>Login </h1>
      <form onSubmit={onSubmit}>
        <input type="text" name="email" {...email} />
        <input type="password" name="password" {...password} />
      </form>
    </div>
  );
}

const useInputValue = (defaultValue: string = "") => {
  const [val, setVal] = useState(defaultValue);
  const handleChange = (e: any) => setVal(e.target.value);

  return {
    value: val,
    onChange: handleChange
  };
};

And because now you have the context values, we rely on isAuthenticated to show the login form or authenticated page.

<div>
      {context.isAuthenticated ? (
        <div>
          <h1>You have been logged on ${email.value}</h1>
        </div>
      ) : (
        <div>
          <h1>Login </h1>
          <form onSubmit={onSubmit}>
            <input type="text" name="email" {...email} />
            <input type="password" name="password" {...password} />
          </form>
        </div>
      )}
    </div>

With this we have covered almost the implementation of auth module but don't we forget something, that's right! the value of isAuthenticated is always false since we didn't implement yet login function.

Implementation login

For this we can simply create a custom hook that handles it:

src/auth/AuthProvider.tsx

import React, { ReactElement, useState } from "react";
import { AuthContext } from "./AuthContext";

interface IAuthProviderProps {
  children: ReactElement<any> | ReactElement<any>[];
}
export function AuthProvider({ children }: IAuthProviderProps) {
  const contextValue = useContextChange(); //** added */

  return (
    //** Added */
    <AuthContext.Provider value={contextValue}>{children}</AuthContext.Provider>
  );
}
//** Added */
const useContextChange = () => {
  const [isAuthenticated, setIsAuthenticated] = useState(false);

  const login = (email: string, password: string) => {
    // some api call.
    fetch("http://localhost/5000", {
      method: "post",
      body: JSON.stringify({
        email,
        password // don't forget to hash the password
      })
    })
      .then(res => setIsAuthenticated(true))
      .catch(error => {
        setIsAuthenticated(false);
        throw new Error("[Authenticaion] " + JSON.stringify(error));
      });
  };

  const register = (email: string, password: string) => {
    // same for register
  };

  return {
    isAuthenticated,
    login,
    register
  };
};

With that our authentication is done, is it? normally yes. But what if one of our component down in the tree needs to access login, register of `isAuthenticated``

in the case we will create another higher order component that can easily wrap any component and access this value:

src/auth/withAuthentication.tsx

import React, { ComponentType } from "react";
import { AuthContext, IAuthContext } from "./AuthContext";

export default function withAuthentication<T>(
  Component: ComponentType<T & IAuthContext>
) {
  return (props: T) => (
    <AuthContext.Consumer>
      {context => <Component {...props} {...context} />}
    </AuthContext.Consumer>
  );
}

And we can use like this in any component under Provider:

AnyComponent.tsx

import React from "react";
import { IAuthContext } from "./auth/AuthContext";
import withAuthentication from "./auth/withAuthentication";

interface Props {}

function AnyComponent(props: Props & IAuthContext) {
  return (
    <div>
      <h2>Yes, you can access this value {props.isAuthenticated}</h2>
    </div>
  );
}

export default withAuthentication(AnyComponent);

It's done for this time :)

Thank you for reading

I hope you learned something here

Please share & like

Comments (2)

Tran Hiep's photo

Hi there, Thank you for your content, I've learned a lot from your post. However, I see you make some mistakes during using auth context. First, you created auth context in src/auth/AuthContext.ts but you imported the context from ./auth/Context instead of ./auth/AuthContext. The second problem I saw in your code that AuthContext may be undefined, we should use optional chaining to avoid Object is possibly 'undefined' error.

CodeReviewIo's photo

Thank you, I have updated it. For context value: this actually should be defaultValue not undefined but in this case we have to provide full initial data consider it initialState. So, the solution that we provide undefined or make all the contextValues optional and then pass defaultState I would say in this case it depends to your project.👍 Happy that helps.