DEV Community

Jose Godinez
Jose Godinez

Posted on

The advanced guide to React Context with hooks.

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]).

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

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

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

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() !== ""
    ) {

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

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

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

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>
          <p>Login </p>
          <form onSubmit={onSubmit}>
            <input type="text" name="email" {...email} />
            <input type="password" name="password" {...password} />
          </form>
        </div>
      )}
    </div>
Enter fullscreen mode Exit fullscreen mode

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

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

Note: I have noticed that context will not be updated in case which means the component will not render regardless In this case please use useAuthContext hook in to get the last updates to render the component.

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>
      <p>Yes, you can access this value {props.isAuthenticated}</p>
    </div>
  );
}

export default withAuthentication(AnyComponent);
Enter fullscreen mode Exit fullscreen mode

It's done for this time :)

Thank you for reading
I hope you learned something here

Top comments (0)