YouTube Summaries | Always Use a Custom Hook for Context API

October 29th, 2023

TL;DW:

  • Custom hooks pair well with the React Context API for improving code organization and type guarding useContext consumption
  • It introduces a custom hook (e.g., useThemeContext) to simplify context consumption and error handling.
  • TypeScript typing is encouraged to catch errors at compile-time and ensure correct data types.

Context API Setup

Creating a Context

Contex’s can be created using the createContext function and an initial value that specifies what consumers should receive if they attempt to access the context outside of the provider component.

// Creating a theme context
const ThemeContext = createContext({
  theme: "light",
  setTheme: () => {}, // Placeholder function
});

Wrapping Components

Once the context is defined, the relevant parts of the application that will consume the context should be wrapped with the context provider component to make the context accessible. This is typically done at a high level in the component tree to ensure many components can access it without prop drilling.

// Wrapping the entire application with the theme context provider
<ThemeContext.Provider value={{ theme, setTheme }}>
  {/* Your app components */}
</ThemeContext.Provider>

Problems to Address

Issue 1: Carrying Context Variables

One issue with this architecture is the need to carry around the context variable and manually import it in every component where it’s required. This can lead to code duplication and make the codebase harder to maintain.

import { useContext } from "react";
// ...
const { theme, setTheme } = useContext(ThemeContext);

Issue 2: Null Value Check

Another issue is the requirement to check for null values before using the context data. Because the initial value was specified as an empty object, it can lead to runtime errors when accessing context data directly.

// Checking for null before using context data
if (theme !== null) {
  // Use theme and setTheme safely
} else {
  throw new Error("ThemeContext must be used within a ThemeContext provider.");
}

Custom Hook Solution

To address these problems, a custom hook can be created. The custom hook encapsulates context consumption and provides a more efficient and reusable solution.

// Creating a custom hook for consuming the theme context
function useThemeContext() {
  const context = useContext(ThemeContext);

  if (context === null) {
    throw new Error(
      "useThemeContext must be used within a ThemeContext provider."
    );
  }

  return context;
}

With the custom hook in place, there is no longer a need to carry around the context variable or manually check for null values.

// Consuming the theme context using the custom hook
const { theme, setTheme } = useThemeContext();

The custom hook abstracts away these issues and provides a cleaner and more readable way to work with context data.

TypeScript Typing

TypeScript pairs well with this paradigm as type definitions for the context and values allow errors to be caught at compile-time. This ensures that you’re working with the correct types and helps prevent runtime issues.

// Creating TypeScript type definitions for the theme context
type Theme = "light" | "dark";

type ThemeContextType = {
  theme: Theme;
  setTheme: (theme: Theme) => void;
};

These types can be used to specify the context and values within your application.

const ThemeContext = createContext<ThemeContextType>({
  theme: "light",
  setTheme: () => {},
});

Final Code Example

Putting all of this together, here’s an example of creating a custom hook useThemeContext that is used by a Content as well as Header component.

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

// Step 1: Create a Theme Context
type Theme = "light" | "dark";

type ThemeContextType = {
  theme: Theme;
  setTheme: (theme: Theme) => void;
};

const ThemeContext = createContext<ThemeContextType | null>(null);

// Step 2: Create a Custom Hook for Consuming the Theme Context
function useThemeContext() {
  const context = useContext(ThemeContext);

  if (context === null) {
    throw new Error(
      "useThemeContext must be used within a ThemeContext provider."
    );
  }

  return context;
}

// Step 3: Create a ThemeProvider Component
const ThemeProvider: FC = ({ children }) => {
  const [theme, setTheme] = useState<Theme>("light");

  const toggleTheme = () => {
    setTheme((prevTheme) => (prevTheme === "light" ? "dark" : "light"));
  };

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <button onClick={toggleTheme}>Toggle Theme</button>
      {children}
    </ThemeContext.Provider>
  );
};

// Step 4: Create Components that Use the Custom Hook
const App = () => {
  return (
    <ThemeProvider>
      <Header />
      <Content />
    </ThemeProvider>
  );
};

const Header = () => {
  const { theme, setTheme } = useThemeContext();

  return (
    <header
      style={{ background: theme === "light" ? "lightgray" : "darkgray" }}
    >
      <h1>Header</h1>
      <button onClick={() => setTheme("light")}>Set Light Theme</button>
      <button onClick={() => setTheme("dark")}>Set Dark Theme</button>
    </header>
  );
};

const Content = () => {
  const { theme } = useThemeContext();

  return (
    <main style={{ color: theme === "light" ? "black" : "white" }}>
      <h2>Content</h2>
    </main>
  );
};

export default App;