Context APIで始めるグローバル状態管理
はじめに
今回はグローバルな値で状態を管理するために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 バケツリレーが不要になる
- コードがシンプルになる
実装手順
以下の手順で実装を進めていきます。
- Context・状態の定義
- Provider(状態の提供者)の実装
- カスタムフックの作成
- アプリケーションでの使用
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で良かったのですが、より複雑な状態遷移が必要な場合はReduxやZustandなどのライブラリを使用する必要があります。
またそれらのライブラリも今度の機会に触っていこうと思います。
Discussion