React Custom Context Hook

Updatable React context Hook with custom logic in TypeScript.


React Hooks brings status management to functions and enables us to get rid of JavaScript classes whatsoever. Now, both components and Hooks are mere functions.

Hooks are a good idea. So good that it was worth mentioning in the Technology Radar.

As React is an unopinionated framework, it is hard to find a consistent set of good practices and integrous architectural principles.

The same is true for Hooks. Hooks can be used in different ways, based on the use-case or personal taste.

The Context Hook is one of the less common but still very useful built-in Hooks.

Context Hook

Using context makes it easy to share data through components without passing it as properties.

Practically, it addresses awkward situations like this:

const App = () => {
  const theme = "dark";
  return (
    <Menu theme={theme} />
    <Content theme={theme} />
    <Footer theme={theme}  />
  );
}

const Menu = ({theme}}) => {
  return (
    <div>
      <Button theme={theme} label="Home" />
      <Button theme={theme} label="About" />
    </div>
  );
}

const Button = ({theme, label}) => {
  return (
    <a className={theme}>
      {label}
    </a>
  )
}

...

The build-in Hook useContext accepts a context object and returns the current context value:

const value = useContext(MyContext);

A component calling useContext will always re-render when the context value changes.

The solution for the problem above looks like follows:

const ThemeContext = createContext("light");

const App = () => {
  return (
    <ThemeContext.Provider value="dark">
      <Menu />
      <Content />
      <Footer />
    </ThemeContext.Provider>
  );
}

const Menu = () => {
  return (
    <div>
      <Button label="Home" />
      <Button label="About" />
    </div>
  );
}

const Button = ({label}) => {
  const theme = useContext(ThemeContext);
  return (
    <a className={theme}>
      {label}
    </a>
  )
}

...

To provide functionality for changing the current value of the context, we must use it together with other Hooks. Combining multiple Hooks to work out a common goal is a case for a custom Hook.

Custom Context Hook

A custom Hook is a function whose name starts with use and that may call other Hooks.

Our Hook will create a context for holding a value of the current theme (light or dark). The user can change the theme anytime. The current value will be stored in localStorage to be used on the next visit.

We will create our useTheme TypeScript-first:

// useTheme.tsx

import {createContext, useContext, useState} from "react";

// context object structure
type ContextType = {
    theme: string;
    updateTheme: (theme: string) => void;
};

// create an empty context
const ThemeContext = createContext<ContextType>({
    theme: "",
    updateTheme: () => {}
});

// context provider container
export const ThemeProvider = (prop: 
    {value?: string, children: JSX.Element | JSX.Element[]}) => {

  const [theme, setTheme] = useState<string>(
    localStorage.getItem("theme") || prop.value || "light");

  const updateTheme = (theme: string) => {
    setTheme(theme);
    localStorage.setItem("theme", theme);
  };

  return (
    <ThemeContext.Provider value={{theme, updateTheme}}>
      {prop.children}
    </ThemeContext.Provider>
  );
};

// custom context hook
const useTheme = () => useContext(ThemeContext) as ContextType;

export default useTheme;

Our context object holds the current value and update function. To take the complicated initialization away from the user code, we provide a convenient ThemeProvider function. This provider container initializes the context with a state and the update function. The state is initialized with the value from localStorage or a user-provided default value respectively. The update function actualizes the state and storage when the context value has changed.

Next, we will use our Hook in the components. The whole component tree must be closed in the context provider:

// App.tsx

import Menu from "./Menu";
import Content from "./Content";
import Footer from "./Footer";
import {ThemeProvider} from "./useTheme";

const App = () => {
  return (
    <ThemeProvider value="dark">
      <Menu />
      <Content />
      <Footer />
    </ThemeProvider>
  );
}

export default App;

Now, all children of the provider have access to its context.

We will use the update function to change the theme based on the user’s preferences:

// Menu.tsx

import Button from "./Button";
import useTheme from "./useTheme";

const Menu = () => {
  // update function from the context
  const {updateTheme} = useTheme();

  const lighten = () => updateTheme("light");
  const darken = () => updateTheme("dark");

  return (
    <div>
      <nav>
        <a onClick={lighten}>🌝</a>
        <a onClick={darken}>🌚</a>
      </nav>
      <Button label="Home" />
      <Button label="About" />
    </div>
  );
}

export default Menu;

Finally, the value is used to render components with the corresponding theme:

// Button.tsx

import useTheme from "./useTheme";
import "./Button.css";

const Button = (prop: {label: string}) => {
  // receive the current context value
  const {theme} = useTheme();
  return (
    <button className={theme}>
      {prop.label}
    </button>
  )
}

export default Button;

Source Code

A demo application with mentioned code is on my GitHub.

Happy reacting!