😽

Context APIで始めるグローバル状態管理

2025/01/12に公開

はじめに

今回はグローバルな値で状態を管理するためにContext APIを用いた状態管理についてまとめていきたいと思います。
グローバルな値で状態を管理するということは、アプリケーション全体で共有したいデータを、Propsバケツリレーをせずに、必要なコンポーネントで直接参照・更新できるようにする仕組みです。

前回こちらの記事で記載したTodolistの状態管理をContext APIを用いた状態管理に変更してみます。

そもそもContext APIとは?・メリット

Context APIは、Reactの組み込み機能として提供されているグローバル状態管理の手法の1つです。

Reactで複数のコンポーネント間でデータを共有する際、通常は親から子へpropsを渡していきます(Props バケツリレー)。

const App = () => {
  const [todos, setTodos] = useState([]);
  return (
    <TodoList todos={todos}>
      <TodoItem todos={todos} />
    </TodoList>
  );
};

この方法には以下のような問題があります。

  • コンポーネントが増えるとpropsの受け渡しが複雑に
  • コードの保守が大変
  • コンポーネントの再利用が難しい

そしてContext APIを使うと上記が解消され、以下のようなメリットがあります。

  • どのコンポーネントからでも直接状態にアクセス可能
  • Props バケツリレーが不要になる
  • コードがシンプルになる

実装手順

以下の手順で実装を進めていきます。

  1. Context・状態の定義
  2. Provider(状態の提供者)の実装
  3. カスタムフックの作成
  4. アプリケーションでの使用

context・状態の定義

まず、Contextファイルを作成する必要があるので、今回はTodoContext.tsxを作成します。
そして共有したい状態とその型を定義します。

interface TodoContextType {
  todos: Todo[];
  newTodo: string;
  searchKeyword: string;
  filteredTodos: Todo[];
  setNewTodo: (text: string) => void;
  setSearchKeyword: (text: string) => void;
  addTodo: () => void;
  deleteTodo: (id: number) => void;
}

const TodoContext = createContext<TodoContextType | undefined>(undefined);

Provider(状態の提供者)の実装

次に、状態を管理・提供するProviderを実装します。

export const TodoProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  // 状態の定義
  const [todos, setTodos] = useState<Todo[]>([
    { id: 1, title: "Learn React", completed: false },
    { id: 2, title: "TypeScript", completed: false },
  ]);
  
  // 省略...(その他の状態と関数の定義)

  // 共有する値の準備
  const value = {
    todos,
    newTodo,
    searchKeyword,
    filteredTodos,
    setNewTodo,
    setSearchKeyword,
    addTodo,
    deleteTodo,
  };

  return <TodoContext.Provider value={value}>{children}</TodoContext.Provider>;
};

呼び出しもとで状態を共有するためにProviderを作成します。

export const TodoProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  return (
    <TodoContext.Provider value={value}>
      {children}
    </TodoContext.Provider>
  );
};

そしてグローバルで使えるようにカスタムフックとして作成。

export const useTodo = () => {
  const context = useContext(TodoContext);
  if (context === undefined) {
    throw new Error('useTodo must be used within a TodoProvider');
  }
  return context;
};

今回はAppコンポーネントで呼び出せるようにする。

const App = () => {
  return (
    <TodoProvider>
      <TodoApp />
    </TodoProvider>
  );
};

export default App;

作成したContextファイルの内容

import React, { createContext, useContext, useState, useEffect } from "react";
import { Todo } from "../types/Todo";

interface TodoContextType {
  todos: Todo[];
  newTodo: string;
  searchKeyword: string;
  filteredTodos: Todo[];
  setNewTodo: (text: string) => void;
  setSearchKeyword: (text: string) => void;
  addTodo: () => void;
  deleteTodo: (id: number) => void;
}

const TodoContext = createContext<TodoContextType | undefined>(undefined);

export const TodoProvider: React.FC<{ children: React.ReactNode }> = ({
  children,
}) => {
  const [todos, setTodos] = useState<Todo[]>([
    { id: 1, title: "Learn React", completed: false },
    { id: 2, title: "TypeScript", completed: false },
  ]);
  const [newTodo, setNewTodo] = useState<string>("");
  const [searchKeyword, setSearchKeyword] = useState<string>("");
  const [filteredTodos, setFilteredTodos] = useState<Todo[]>([]);

  const addTodo = () => {
    if (!newTodo.trim()) return;
    const newTodoItem: Todo = {
      id: todos.length + 1,
      title: newTodo,
      completed: false,
    };
    setTodos([...todos, newTodoItem]);
    setNewTodo("");
  };

  const deleteTodo = (id: number) => {
    setTodos(todos.filter((todo) => todo.id !== id));
  };

  useEffect(() => {
    const filtered = todos.filter((todo) =>
      todo.title.toLowerCase().includes(searchKeyword.toLowerCase())
    );
    setFilteredTodos(filtered);
  }, [searchKeyword, todos]);

  const value = {
    todos,
    newTodo,
    searchKeyword,
    filteredTodos,
    setNewTodo,
    setSearchKeyword,
    addTodo,
    deleteTodo,
  };

  return <TodoContext.Provider value={value}>{children}</TodoContext.Provider>;
};

export const useTodo = () => {
  const context = useContext(TodoContext);
  if (context === undefined) {
    throw new Error("useTodo must be used within a TodoProvider");
  }
  return context;
};

まとめ

これらの機能を適切に組み合わせることで、メンテナンス性が高くなり、コンポーネントの再利用が容易になり、状態管理が一元化されるコードになりました。

しかしContextを使用する際に、気をつける必要があることもあります。
例えばパフォーマンスの観点です。Context の値が変更されると、そのContextを購読しているすべてのコンポーネントが再レンダリングされる特性があります。なので、適切に設計しないとパフォーマンスの問題を引き起こす可能性があります。

今回は小規模のアプリケーションの状態管理なので、Context APIで良かったのですが、より複雑な状態遷移が必要な場合はReduxZustandなどのライブラリを使用する必要があります。

またそれらのライブラリも今度の機会に触っていこうと思います。

Discussion