How to: React Hooks: useContext

(This is sort of a continuation of a previous article on useReducer. Now, we will look into continuing what we had previously built, and how, we can use useContext hook to clean up the codes. Note also, that I have converted my project from Javascript to Typescript.)

“Context provides a way to pass data through the component tree without having to pass props down manually at every level.” –

www.reactjs.org

What is useContext?

useContext will give us the ability to have a context provider that resides at a parent level and provide that context to any child level component. The child component doesn’t necessarily have to be a direct child, you can pass or skip many child levels (think of it like grand child or great grand child having a straight access to the “(main) Parent” or “Grandparent or great grandparent”).

How to useContext in React Hooks?

First, we will need to create a context using React.createContext()

const MyContext = React.createContext();

So that’s the context itself. And it is made up of two things: Provider and Consumer. First, let’s create a Provider component and wrap it around our app or parent component so we can use what the context is providing at the app level down to any children down the app might have.

function MyProvider({children) {

return <MyContext.Provider> {children}</MyContext.Provider>;  //the actual context provider
	
}

Next, let’s move the useReducer hook and all of its load function and put them inside the MyProvider component.

function MyProvider({ children }) {
  const [state, dispatch] = React.useReducer(reducer, {
    loading: false,
    more: true,
    data: [],
    after: 0
  });
  const { loading, data, after, more } = state;

  const load = () => {
    dispatch({ type: types.start });

    setTimeout(() => {
      const newData = allData.slice(after, after + perPage);
      dispatch({ type: types.loaded, newData });
    }, 1000);
  };

return <MyContext.Provider> {children}</MyContext.Provider>; 

So how does data actually get from the provider? We pass the value prop to our My.Context.Provider.

The value prop includes all the data that we want to make available to the children of the context provider.

return (
    <MyContext.Provider value={{ loading, data, more, load }}>
      {children}
    </MyContext.Provider>
  );

How to access the value that is provided by our MyContext.Provider to our child(ren) component?’

This is where the useContext hook comes into play.

function MyFirstComponent() {
  const { data, loading, more, load } = React.useContext(MyContext);

So far, this is just a simple passing or accessing of components between the parent and child component.

Now, let’s create another component or a second component (and for simplicity – I will just do it in the same file) to show how the second component can access the context provider component.

function MySecondComponent() {
  const { load } = React.useContext(MyContext);

  return (
    <li style={{ background: "green" }}>
      <button onClick={load}>Load More</button>
    </li>
  );
}

Here’s the complete sample code.

import React from "react";
import { makeStyles } from "@material-ui/core/styles";
export const allData = new Array(25).fill(0).map((_val, i) => i + 1);

const perPage = 10;
const types = {
  START: "START",
  LOADED: "LOADED",
};

const reducer = (state, action) => {
  switch (action.type) {
    case "START":
      return { ...state, loading: true };
    case "LOADED":
      return {
        ...state,
        loading: false,
        data: [...state.data, ...action.newData],
        more: action.newData.length === perPage,
        after: state.after + action.newData.length,
      };
    default:
      throw new Error("Don't understand the action");
  }
};

const MyContext = React.createContext({} as any);

function MyProvider({ children }) {
  const [state, dispatch] = React.useReducer(reducer, {
    loading: false,
    more: true,
    data: [],
    after: 0,
  });
  const { loading, data, after, more } = state;

  const load = () => {
    dispatch({ type: types.START });

    setTimeout(() => {
      const newData = allData.slice(after, after + perPage);
      // @ts-ignore
      dispatch({ type: types.LOADED, newData });
    }, 1000);
  };

  return (
    <MyContext.Provider value={{ loading, data, more, load }}>
      {children}
    </MyContext.Provider>
  );
}

function MyFirstComponent() {
  const { data, loading, more } = React.useContext(MyContext);

  return (
    <div className="App">
      <ul>
        {data.map((row) => (
          <li key={row} style={{ background: "orange" }}>
            {row}
          </li>
        ))}

        {loading && <li>Loading...</li>}

        {!loading && more && <MySecondComponent />}
      </ul>
    </div>
  );
}

function MySecondComponent() {
  const { load } = React.useContext(MyContext);

  return (
    <li style={{ background: "green" }}>
      <button onClick={load}>Load More</button>
    </li>
  );
}

export default () => {
  return (
    <MyProvider>
      <MyFirstComponent />
      <MySecondComponent />
    </MyProvider>
  );
};