Context Hooks

Context allows a component to receive information from distant parents without prop driling.


  • useContext reads and subscribes to a context.

useContext

useContext is a React Hook that lets you read and subscribe to context from your component.

Parameters

  • The context that you’ve previously created with createContext. The context itself does not hold the information, it only represents the kind of information you can provide or read from components.

Returns

useContext returns the context value for the calling component. It is determined as the value passed to the closest SomeContext.Provider above the calling component in the tree. If there is no such provider, then the returned value will be the defaultValue you have passed to createContext for that context. The returned value is always up-to-date. React automatically re-renders components that read some context if it changes.

Caveats

  • useContext() call in a component is not affected by providers returned from the same component. The corresponding <Context.Provider> needs to be above the component doing the useContext() call.

  • React automatically re-renders all the children that use a particular context starting from the provider that receives a different value. Skipping re-renders with memo does not prevent the children receiving fresh context values.

  • memo will not prevent the component from re-rendering if you only have it useContext() inside the component and the value is changed.

All components that hold same context will re-renders if only one property is changed.

Example: basic

const ThemeContext = React.createContext<{
  theme: string;
  setTheme: React.Dispatch<React.SetStateAction<string>>;
}>({ theme: "light", setTheme: () => {} });

export default function App() {
  const [theme, setTheme] = React.useState("light");
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Navbar />
    </ThemeContext.Provider>
  );
}

function Navbar() {
  const { theme, setTheme } = React.useContext(ThemeContext);
  return (
    <div>
      <p>{theme}</p>
      <button onClick={() => setTheme("dark")}>Dark theme</button>
      <button onClick={() => setTheme("light")}>Light theme</button>
    </div>
  );
}

Example: complext component

const ProductContext = React.createContext<{ product: Product } | null>(null);

export function useProducContext() {
  const context = React.useContext(ProductContext);
  if (!context) {
    throw new Error(
      "Product.* component must be rendered as child of Product component"
    );
  }
  return context;
}

export default ProductContext;
interface ProductCardProps = {
  product: Product;
  children: React.ReactNode;
}

export function ProductCard({ product, children }: ProductCardProps) {
  return (
    <ProductCardContext.Provider value={{product}}>
      {children}
    </ProductCardContext.Provider>
  )
}

function ProductCardTitle(){
  const {product} = useProductCardContext();
  return(
    <h1>{product.title}</h1>
  )
}
export default function Home() {
  return (
    <ProductCard product={product}>
      <ProductCardWrapper>
        <ProductCardTitle />
        <ProductCardPrice />
      </ProductCardWrapper>
    </ProductCard>
  );
}

Example: context with reducer

export default function TaskPage() {
  return (
    <TasksProvider>
      <h1>Day off in Kyoto</h1>
      <AddTask />
      <TaskList />
    </TasksProvider>
  );
}
type Task = (typeof initialTasks)[0];
type Action =
  | ReturnType<typeof addTodo>
  | ReturnType<typeof changeTodo>
  | ReturnType<typeof deleteTodo>;

type State = typeof initialTasks;

const addTodo = (text: string) => ({
  type: "added" as const,
  payload: { text },
});

const changeTodo = (task: Task) => ({
  type: "changed" as const,
  payload: { task },
});

const deleteTodo = (id: number) => ({
  type: "deleted" as const,
  payload: { id },
});

const TasksContext = React.createContext<State | null>(null);

const TasksDispatchContext = React.createContext<React.Dispatch<Action> | null>(
  null
);

export function TasksProvider({ children }) {
  const [tasks, dispatch] = React.useReducer<React.Reducer<State, Action>>(
    tasksReducer,
    initialTasks
  );

  return (
    <TasksContext.Provider value={tasks}>
      <TasksDispatchContext.Provider value={dispatch}>
        {children}
      </TasksDispatchContext.Provider>
    </TasksContext.Provider>
  );
}

export function useTasks() {
  return React.useContext(TasksContext);
}

export function useTasksDispatch() {
  return React.useContext(TasksDispatchContext);
}

function tasksReducer(tasks: State, action: Action) {
  switch (action.type) {
    case "added": {
      return [
        ...tasks,
        {
          id: Math.random(),
          text: action.payload.text,
          done: false,
        },
      ];
    }
    case "changed": {
      return tasks.map((t) => {
        if (t.id === action.payload.task.id) {
          return action.payload.task;
        } else {
          return t;
        }
      });
    }
    case "deleted": {
      return tasks.filter((t) => t.id !== action.payload.id);
    }
    default: {
      throw Error("Unknown action: " + action["type"]);
    }
  }
}

const initialTasks = [
  { id: 0, text: "Philosopher’s Path", done: true },
  { id: 1, text: "Visit the temple", done: false },
  { id: 2, text: "Drink matcha", done: false },
];
export function AddTask() {
  const [text, setText] = React.useState("");
  const dispatch = useTasksDispatch();

  if (dispatch === null) {
    return null;
  }

  return (
    <>
      <input
        placeholder="Add task"
        value={text}
        onChange={(e) => setText(e.target.value)}
      />
      <button
        onClick={() => {
          setText("");
          dispatch({
            type: "added",
            payload: { text },
          });
        }}
      >
        Add
      </button>
    </>
  );
}
export function TaskList() {
  const tasks = useTasks();

  if (tasks === null) {
    return <p>Make new tasks</p>;
  }

  return (
    <ul>
      {tasks.map((task) => (
        <li key={task.id}>
          <Task task={task} />
        </li>
      ))}
    </ul>
  );
}
export function Task({ task }: { task: Task }) {
  const dispatch = useTasksDispatch();

  if (dispatch === null) {
    return null;
  }
  return (
    <button
      onClick={() =>
        dispatch({
          type: "changed",
          payload: { task: { ...task, done: true } },
        })
      }
      className={cn(task.done && "line-through")}
    >
      {task.text}
    </button>
  );
}

Example: Optimizing re-renders when passing objects and functions

Whenever MyApp re-renders (for example, on a route update), this will be a different object pointing at a different function, so React will also have to re-render all components deep in the tree that call useContext(AuthContext).

There is no need to re-render if currentUser has not changed. So you need to wrap the login function with useCallback and wrap the object creation into useMemo.

function MyApp() {
  const [currentUser, setCurrentUser] = useState(null);

  function login(response) {
    storeCredentials(response.credentials);
    setCurrentUser(response.user);
  }

  return (
    <AuthContext.Provider value={{ currentUser, login }}>
      <Page />
    </AuthContext.Provider>
  );
}
import * as React from "react";

function MyApp() {
  const [currentUser, setCurrentUser] = React.useState(null);

  const login = React.useCallback((response) => {
    storeCredentials(response.credentials);
    setCurrentUser(response.user);
  }, []);

  const contextValue = React.useMemo(
    () => ({
      currentUser,
      login,
    }),
    [currentUser, login]
  );

  return (
    <AuthContext.Provider value={contextValue}>
      <Page />
    </AuthContext.Provider>
  );
}