🙌

React hooksを少しでも使いこなせるように

2024/12/23に公開

この記事はQiitaのReact Advent Calendar 2024に参加するために投稿しました。

Reactを使っている人で多くの人が一度はつまづくと思うReact hooks。use何とかってのがたくさんあって、どこで何を使えばいいか、わかりづらいですよね。
そこで、最近流行りのChatGPTさんと会話しながら、どうやって使い分ければいいか聞いてみた。

React hooksはuse何とかってのがたくさんあって、使い分けが難しいです。
どのように使い分ければいいですか?

ChatGPT

React のフック(hooks)にはたくさんの種類がありますが、それぞれ用途が異なるため、使い分けるポイントを理解すると選択が簡単になります。以下に主要なフックとその使い方を整理して説明します。

React のフック(hooks)にはたくさんの種類がありますが、それぞれ用途が異なるため、使い分けるポイントを理解すると選択が簡単になります。以下に主要なフックとその使い方を整理して説明します。

1. 基本フック
useState
用途: コンポーネント内で状態(state)を管理する。
使う場面: ユーザーの入力、トグルの状態、カウンターなど、コンポーネントに閉じた一時的な状態を管理したいとき。
例:
jsx
コードをコピーする
const [count, setCount] = useState(0);
useEffect
用途: 副作用(side effects)の処理を行う。
使う場面:
API呼び出し
DOMの操作(例: イベントリスナーの登録)
外部リソースの読み込み
例:
jsx
コードをコピーする
useEffect(() => {
  fetchData();
}, [dependency]); // dependency が変更されたときに実行
useContext
用途: コンポーネントツリー間でデータを共有する(props を渡さずに)。
使う場面: グローバルな状態(テーマや認証情報など)をコンポーネント全体で扱いたいとき。
例:
jsx
コードをコピーする
const value = useContext(MyContext);
2. 高度なフック
useReducer
用途: useState の代わりに複雑な状態遷移を管理する。
使う場面: 状態管理が複雑で、複数の状態をまとめて処理したいとき。
例:
jsx
コードをコピーする
const [state, dispatch] = useReducer(reducer, initialState);
useRef
用途: DOM要素や、コンポーネント間で共有したい値を直接参照する。
使う場面:
DOM要素に直接アクセスする(例: フォーカス制御)。
状態を再レンダリングなしで保持する。
例:
jsx
コードをコピーする
const inputRef = useRef(null);
useMemo
用途: 計算結果をキャッシュしてパフォーマンスを最適化する。
使う場面: 計算コストが高い関数の結果を無駄に再計算したくないとき。
例:
jsx
コードをコピーする
const computedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
useCallback
用途: コールバック関数をメモ化してパフォーマンスを最適化する。
使う場面: 子コンポーネントに渡す関数が頻繁に再生成されるのを防ぎたいとき。
例:
jsx
コードをコピーする
const handleClick = useCallback(() => doSomething(), [dependency]);
3. 特殊なフック
useImperativeHandle
用途: 親コンポーネントが子コンポーネントの特定の機能にアクセスできるようにする。
使う場面: カスタムコンポーネントに対する直接的な操作を親から許可したいとき。
例:
jsx
コードをコピーする
useImperativeHandle(ref, () => ({
  customMethod() {
    // Custom logic
  }
}));
useLayoutEffect
用途: useEffect と似ているが、DOM の描画が行われる直前に実行される。
使う場面: レイアウトに影響を与える副作用を実行したいとき。
例:
jsx
コードをコピーする
useLayoutEffect(() => {
  // Measure DOM size
}, []);
フックの選び方
状態管理の必要性を考える

単純な状態: useState
複雑な状態遷移: useReducer
副作用を伴う処理があるか

一般的な副作用: useEffect
レンダリングのタイミングを調整する必要がある: useLayoutEffect
パフォーマンスの最適化が必要か

計算結果のキャッシュ: useMemo
コールバック関数のキャッシュ: useCallback
DOM要素に直接アクセスするか

useRef を利用。
グローバルな状態を扱うか

Context API と useContext を利用。

まず、迷うことが多いのが、useMemoとuseCallbackの使い分けです。

ChatGPT

useMemo と useCallback はどちらも パフォーマンス最適化 のために使われますが、使い方や用途が異なります。それぞれの違いと使い分けについて、以下に詳しく解説します。
1. useMemo の用途
useMemo は 値 をメモ化するために使います。

キャッシュする対象: 計算結果(値)

目的: 計算コストの高い処理を毎回再実行しないようにする。

典型的な使用例:

フィルタリングやソートの結果。
複雑なデータ変換や計算。
例:

jsx
コードをコピーする
const expensiveValue = useMemo(() => {
  // 計算コストが高い処理
  return computeExpensiveValue(a, b);
}, [a, b]);
ここでは a や b が変わらない限り、computeExpensiveValue が再実行されない。
2. useCallback の用途
useCallback は 関数 をメモ化するために使います。

キャッシュする対象: 関数

目的: コンポーネントが再レンダリングされても、関数の再生成を防ぐ。

典型的な使用例:

子コンポーネントにコールバック関数を渡す場合。
useEffect の依存関係として渡す場合。
例:

jsx
コードをコピーする
const handleClick = useCallback(() => {
  console.log('Button clicked');
}, []);
ここでは handleClick 関数が再レンダリングのたびに再生成されない。
使い分けのポイント
「計算結果」をキャッシュしたい → useMemo

例: 重い計算やフィルタリング結果をメモ化。
質問のキーワード: 「計算」「結果」「処理が重い」
「関数」をキャッシュしたい → useCallback

例: 子コンポーネントに渡すコールバック関数を再生成させたくない。
質問のキーワード: 「関数」「子コンポーネント」「コールバック」
実践的な例
useMemo を使う場面
データのフィルタリングやソートなど、計算コストが高い処理をメモ化したい場合。

jsx
コードをコピーする
const filteredItems = useMemo(() => {
  return items.filter(item => item.value > threshold);
}, [items, threshold]); // items や threshold が変わらない限り再計算しない
useCallback を使う場面
親コンポーネントから子コンポーネントにコールバック関数を渡す場合。

jsx
コードをコピーする
const handleChange = useCallback((value) => {
  setState(value);
}, []); // 状態が変わらない限り関数を再生成しない

// 子コンポーネントに渡す
<MyChildComponent onChange={handleChange} />
併用するケース
場合によっては、useMemo と useCallback を併用することもあります。

例: メモ化したデータを関数で操作する場合
jsx
コードをコピーする
const filteredItems = useMemo(() => {
  return items.filter(item => item.value > threshold);
}, [items, threshold]);

const handleClick = useCallback(() => {
  console.log(filteredItems);
}, [filteredItems]); // filteredItems に依存
注意点
使いすぎない: パフォーマンス最適化が必要ない場合には使わない方が良いです。React のデフォルトの動作(再計算や再生成)は十分に高速です。
依存関係に注意: useMemo や useCallback の依存配列を正しく指定しないと、バグや予期しない動作の原因になります。
迷ったときのまとめ
計算結果を保存 → useMemo
関数を保存 → useCallback
もし特定のケースでどちらを使うべきか迷った場合、その具体的なコードや要件を教えてもらえれば、さらにアドバイスできます!

後、useEffectはケースによっては使わないほうがいいと聞いたことがあります。
使いたくなるけど実は使わないほうがいいケースの例があれば、理由とともに教えてください。

ChatGPT

useEffect は便利ですが、誤用するとパフォーマンス問題や予期しないバグを引き起こす原因になります。特定の状況では useEffect を避けるべき場合があり、それに代わる方法を検討することが重要です。以下に、useEffect を使わない方が良いケースとその理由を解説します。

1. 状態を計算するために使う場合
例: useEffect 内で状態を計算して更新する

jsx
コードをコピーする
useEffect(() => {
  setDerivedState(computeFromProps(props));
}, [props]);
問題:
useEffect は副作用を実行するためのもので、状態の計算は副作用ではありません。
レンダリングのたびに再計算と再レンダリングが発生し、効率が悪い。
改善: 状態の計算は useMemo や useState を使うべき。
jsx
コードをコピーする
const derivedState = useMemo(() => computeFromProps(props), [props]);
2. 必要以上にリレンダリングを引き起こす場合
例: 状態を変更するたびに再レンダリングが発生する

jsx
コードをコピーする
useEffect(() => {
  setData(processData(input));
}, [input]);
問題:
状態更新 (setData) が useEffect 内で行われると、再レンダリングがトリガーされる。
状態の更新自体が依存関係に含まれていれば無限ループが発生する可能性もある。
改善: useMemo を使って計算を直接レンダリングに組み込む。
jsx
コードをコピーする
const data = useMemo(() => processData(input), [input]);
3. レンダリング中にサイドエフェクトを発生させる場合
例: コンポーネントがレンダリングされるたびにAPIを呼び出す

jsx
コードをコピーする
useEffect(() => {
  fetchData();
}, [dependency]);
問題:
API呼び出しなどのサイドエフェクトは不要なタイミングで発生する可能性がある。
ユーザーアクション(ボタンクリックなど)がトリガーになる場合は、useEffect ではなくイベントハンドラーで処理すべき。
改善: 必要なときだけ呼び出すようにロジックを変更する。
jsx
コードをコピーする
const handleFetch = () => {
  fetchData();
};

// ユーザーがクリックしたときのみ実行
<button onClick={handleFetch}>Fetch Data</button>
4. コンポーネント間の状態共有に使う場合
例: グローバルな状態を useEffect で管理する

jsx
コードをコピーする
useEffect(() => {
  globalState.value = localState;
}, [localState]);
問題:
useEffect でグローバル状態を管理するのは冗長で、バグの原因になりやすい。
グローバル状態は Context を使用して管理するべき。
改善: useContext で状態を共有する。
jsx
コードをコピーする
const value = useContext(MyContext);
5. 初期化処理を厳密に分離できていない場合
例: 初回レンダリング時にだけ実行したい処理

jsx
コードをコピーする
useEffect(() => {
  initializeComponent();
}, []); // 空の依存配列
問題:
初回レンダリングのタイミングに依存しすぎると、他の初期化手段(例えばカスタムフック)を見落としがち。
改善: 初期化処理はカスタムフックや useMemo に分離。
jsx
コードをコピーする
const initializeData = useMemo(() => initializeComponent(), []);
6. 無駄な副作用を発生させる場合
例: DOM 要素を参照するためだけに useEffect を使う

jsx
コードをコピーする
useEffect(() => {
  ref.current.focus();
}, []);
問題:
単純な DOM 操作は useRef を直接使えば解決する。
不要な再レンダリングを引き起こす可能性がある。
改善: useRef のみを使用。
jsx
コードをコピーする
ref.current.focus();

useEffect を使わない方が良い場面のまとめ

ケース 理由 代替手段
状態を計算するために使う 副作用ではない useMemo
必要以上のリレンダリングを引き起こす パフォーマンス問題 useMemo
サイドエフェクトが不要な場面 無駄なAPI呼び出し イベントハンドラー
グローバル状態を更新する Context を使用すべき useContext
初期化処理を依存配列で制御する 冗長で管理が難しい カスタムフックや useMemo
単純な DOM 操作 useEffect が不要 useRef
判断のポイント
本当に副作用か?
副作用でない処理(計算や状態管理)なら useEffect を避ける。
頻繁に実行する必要があるか?
無駄な実行を減らすために useMemo や useCallback を検討。
コードが複雑化していないか?
useEffect のロジックが複雑になるなら、カスタムフックで分離。
useEffect を避けるべきか迷った場合は、その具体的なケースを教えてもらえれば、さらにアドバイスできます!

後、自分でReact hooksを作れるカスタムフックってのもあるんですよね。
作り方とどんな時に使うかを教えてほしいです。

ChatGPT

はい、自分で作るカスタムフック(Custom Hook)は、Reactの機能を再利用可能にするための強力な手段です。作り方や活用場面を理解することで、コードの保守性や可読性を大幅に向上させることができます。

カスタムフックの作り方
基本構造
カスタムフックは通常の関数として作成しますが、関数名が use で始まる必要があります。これは React がその関数がフックであることを認識するためです。

構文例:

jsx
コードをコピーする
function useCustomHook() {
  const [state, setState] = useState(initialValue);

  const someFunction = () => {
    // カスタムロジック
  };

  useEffect(() => {
    // 副作用の処理
  }, [state]);

  return { state, someFunction };
}
useState や useEffect などの他のフックを内部で利用できます。
必要な値や関数を返すことで、再利用可能なロジックを提供します。
カスタムフックを作る目的
再利用性を高める

複数のコンポーネントで同じロジックを使いたい場合に役立ちます。
コードの可読性を向上させる

コンポーネントのコードが複雑になるのを防ぎ、ロジックを分離できます。
副作用の管理を分ける

API 呼び出しやイベントリスナーの登録などの副作用を、専用のフックにまとめられます。
状態管理の抽象化

状態やロジックをカスタムフックに隠蔽し、簡潔にコンポーネントを記述できます。
活用例
1. APIデータ取得フック
API からデータを取得し、状態管理とエラーハンドリングをまとめる。

カスタムフック:

jsx
コードをコピーする
import { useState, useEffect } from 'react';

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      setLoading(true);
      try {
        const response = await fetch(url);
        if (!response.ok) throw new Error('Failed to fetch');
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };
    fetchData();
  }, [url]);

  return { data, loading, error };
}
コンポーネントでの使用:

jsx
コードをコピーする
function MyComponent() {
  const { data, loading, error } = useFetch('https://api.example.com/data');

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;

  return <div>{JSON.stringify(data)}</div>;
}
2. ウィンドウサイズを監視するフック
ウィンドウのリサイズを監視して、サイズを取得する。

カスタムフック:

jsx
コードをコピーする
import { useState, useEffect } from 'react';

function useWindowSize() {
  const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight });

  useEffect(() => {
    const handleResize = () => {
      setSize({ width: window.innerWidth, height: window.innerHeight });
    };

    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize); // クリーンアップ
  }, []);

  return size;
}
コンポーネントでの使用:

jsx
コードをコピーする
function MyComponent() {
  const size = useWindowSize();

  return (
    <div>
      <p>Width: {size.width}px</p>
      <p>Height: {size.height}px</p>
    </div>
  );
}
3. 入力フォームの状態管理
複数の入力フィールドを簡単に管理できるようにする。

カスタムフック:

jsx
コードをコピーする
import { useState } from 'react';

function useForm(initialValues) {
  const [values, setValues] = useState(initialValues);

  const handleChange = (event) => {
    const { name, value } = event.target;
    setValues({ ...values, [name]: value });
  };

  const reset = () => setValues(initialValues);

  return { values, handleChange, reset };
}
コンポーネントでの使用:

jsx
コードをコピーする
function MyForm() {
  const { values, handleChange, reset } = useForm({ username: '', email: '' });

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log(values);
    reset();
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="username" value={values.username} onChange={handleChange} placeholder="Username" />
      <input name="email" value={values.email} onChange={handleChange} placeholder="Email" />
      <button type="submit">Submit</button>
    </form>
  );
}
カスタムフック作成のベストプラクティス
関数名は「use」で始める: React の規約に従い、フックであることを明示。
再利用を意識する: 汎用的で、複数のコンポーネントで利用できるよう設計する。
依存関係を管理する: useEffect や useCallback の依存配列を適切に設定する。
シンプルにする: カスタムフック内で複雑なロジックを詰め込みすぎない。
ドメインロジックを分離: 特定のビジネスロジックを持つカスタムフックは、その目的が明確であるべき。
まとめ
カスタムフックは、Reactアプリケーションにおけるロジックの再利用と整理に最適です。「複数のコンポーネントで同じロジックを使い回したい」や「コンポーネントのコードを簡潔にしたい」と感じたときが、カスタムフックを作るタイミングです。

具体的なケースがあれば、その場に応じたカスタムフックを一緒に考えることもできますので、遠慮なく聞いてください!

ところで、useQueryは頭にuseって付くけど、React hooksとは違うんですよね。

ChatGPT

はい、その通りです!useQuery は React の公式なフックではなく、通常は React Query(現在では TanStack Query) というライブラリから提供される カスタムフック です。ただし、useQuery の動作は React Hooks の仕組みを利用して実現されています。

React Hooks とカスタムフックの違い
React Hooks

React 自体が提供する組み込みフック(例: useState, useEffect, useMemo)。
React コンポーネントの状態管理やライフサイクルに特化。
カスタムフック

ユーザーまたはライブラリによって作られた再利用可能なロジック。
React Hooks(useEffect, useState など)を内部で使っていることが多い。
React Query の useQuery もこのカテゴリに属します。
useQuery とは?
useQuery は React Query(TanStack Query) によって提供されるカスタムフックで、データフェッチやキャッシュ管理、再取得のロジックを簡略化するために使われます。

基本的な特徴
データフェッチを自動化: API からのデータ取得を簡単に実装できる。
キャッシュ機能: フェッチしたデータをキャッシュに保存し、再取得の回数を減らす。
ステータス管理: ロード中、エラー発生中、成功時の状態を簡単に判別。
ポーリングや再取得: データの定期更新や依存関係の変更時の再フェッチをサポート。
useQuery の例
基本的な使い方
javascript
コードをコピーする
import { useQuery } from '@tanstack/react-query';

function MyComponent() {
  const { data, isLoading, error } = useQuery(['todos'], fetchTodos);

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return (
    <div>
      {data.map(todo => (
        <p key={todo.id}>{todo.title}</p>
      ))}
    </div>
  );
}

async function fetchTodos() {
  const response = await fetch('https://jsonplaceholder.typicode.com/todos');
  if (!response.ok) {
    throw new Error('Network response was not ok');
  }
  return response.json();
}
ポイント
クエリキー:

['todos'] のようなキーを指定してキャッシュを管理。
同じキーを使うとキャッシュされたデータを再利用。
フェッチ関数:

データ取得のロジックを関数として渡す。
ステータス管理:

isLoading: データを取得中。
error: エラーが発生した場合。
React Hooks との関係性
内部で React Hooks を使っている

useState や useEffect を駆使してキャッシュ管理や API コールを実現。
例えば、データの取得状態(isLoading)は useState を用いて管理されています。
「カスタムフック」の一例

useQuery は、React Query チームが提供するカスタムフック。
独自のロジックを React Hooks を活用してパッケージ化している。
React Query(useQuery)を使うべき場面
データ取得が複雑な場合:

データのキャッシュ、再フェッチ、エラーハンドリングなどを効率化したい。
複数のコンポーネントでデータを共有したい場合:

React Query がキャッシュを管理するため、状態を別途 useState で持つ必要がなくなる。
リアルタイムデータが必要な場合:

ポーリングやデータの自動リフレッシュが簡単に実装可能。
アプリの状態管理を簡素化したい場合:

React Query を使えば、Redux や Context を使わずにサーバー状態を管理できる。

React Hooks とライブラリ提供のフックの違い

特徴 React Hooks ライブラリ提供のフック(例: useQuery)
提供元 React 本体 外部ライブラリ(例: React Query)
主な用途 状態管理、ライフサイクルの操作 特定のタスク(例: データフェッチ、認証管理)
再利用性 自作のカスタムフックで再利用可能 特定ライブラリのロジックを抽象化
内部構造 React の基本機能に依存 React Hooks を内部で利用
まとめ
useQuery は React の公式フックではなく、ライブラリ(React Query)が提供するカスタムフックです。
データフェッチやキャッシュ管理、ステータス管理などを簡単に実現できる便利なツールです。
React Query を使用すると、複雑なデータ管理ロジックを省略でき、React Hooks の良さを最大限に活かした設計が可能になります。
useQuery や React Query を導入するかどうか迷っている場合、アプリケーションの要件に基づいてアドバイスもできますので、遠慮なくご相談ください!

ここまで出てきたReact hooksをいくつか使って、簡単なTODOアプリを作ってみてください。

ChatGPT

以下は、React Hooks(useState, useEffect, useMemo, useCallback, カスタムフックなど)を活用した簡単なTODOアプリのコード例です。

機能概要
TODOリストの追加と削除。
TODOの検索(useMemo を利用して効率化)。
TODOの状態(完了/未完了)を切り替え可能。
ローカルストレージにTODOを保存し、ページリロード後も保持(useEffect)。

コード例

import React, { useState, useEffect, useMemo, useCallback } from 'react';

// カスタムフック: ローカルストレージを管理
function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(error);
      return initialValue;
    }
  });

  const setValue = (value) => {
    try {
      setStoredValue(value);
      window.localStorage.setItem(key, JSON.stringify(value));
    } catch (error) {
      console.error(error);
    }
  };

  return [storedValue, setValue];
}

function TodoApp() {
  const [todos, setTodos] = useLocalStorage('todos', []); // ローカルストレージでTODOを保存
  const [input, setInput] = useState('');
  const [search, setSearch] = useState('');

  // TODOの追加
  const addTodo = useCallback(() => {
    if (input.trim() === '') return;
    setTodos((prevTodos) => [
      ...prevTodos,
      { id: Date.now(), text: input, completed: false },
    ]);
    setInput('');
  }, [input, setTodos]);

  // TODOの削除
  const deleteTodo = useCallback(
    (id) => {
      setTodos((prevTodos) => prevTodos.filter((todo) => todo.id !== id));
    },
    [setTodos]
  );

  // TODOの状態切り替え
  const toggleComplete = useCallback(
    (id) => {
      setTodos((prevTodos) =>
        prevTodos.map((todo) =>
          todo.id === id ? { ...todo, completed: !todo.completed } : todo
        )
      );
    },
    [setTodos]
  );

  // 検索フィルター(useMemoで効率化)
  const filteredTodos = useMemo(() => {
    return todos.filter((todo) =>
      todo.text.toLowerCase().includes(search.toLowerCase())
    );
  }, [todos, search]);

  return (
    <div style={{ padding: '20px', maxWidth: '600px', margin: 'auto' }}>
      <h1>TODO App</h1>

      {/* 新しいTODOの入力 */}
      <input
        type="text"
        value={input}
        onChange={(e) => setInput(e.target.value)}
        placeholder="Add a new todo..."
        style={{ padding: '10px', width: '70%', marginRight: '10px' }}
      />
      <button onClick={addTodo} style={{ padding: '10px' }}>
        Add
      </button>

      {/* TODOの検索 */}
      <div style={{ marginTop: '20px' }}>
        <input
          type="text"
          value={search}
          onChange={(e) => setSearch(e.target.value)}
          placeholder="Search todos..."
          style={{ padding: '10px', width: '80%' }}
        />
      </div>

      {/* TODOリストの表示 */}
      <ul style={{ listStyle: 'none', padding: 0, marginTop: '20px' }}>
        {filteredTodos.map((todo) => (
          <li
            key={todo.id}
            style={{
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'space-between',
              marginBottom: '10px',
              padding: '10px',
              border: '1px solid #ddd',
              borderRadius: '4px',
              backgroundColor: todo.completed ? '#d4edda' : '#f8d7da',
            }}
          >
            <span
              onClick={() => toggleComplete(todo.id)}
              style={{
                textDecoration: todo.completed ? 'line-through' : 'none',
                cursor: 'pointer',
              }}
            >
              {todo.text}
            </span>
            <button
              onClick={() => deleteTodo(todo.id)}
              style={{
                padding: '5px 10px',
                backgroundColor: 'red',
                color: 'white',
                border: 'none',
                borderRadius: '4px',
                cursor: 'pointer',
              }}
            >
              Delete
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default TodoApp;
コードのポイント
useState: TODOリストや入力値、検索キーワードを管理。
useEffect: ローカルストレージとの同期。
useMemo: 検索フィルターを効率化。
useCallback: 関数の再生成を防ぎ、子コンポーネントに渡す際のパフォーマンスを最適化。
カスタムフック: useLocalStorage を利用して、ローカルストレージを抽象化。

拡張アイデア
優先度機能: TODO に優先度(高・中・低)を追加。
カテゴリー分類: TODO を複数のカテゴリーに分けて管理。
完了済みTODOのアーカイブ: 完了済みのTODOを別リストに移動。

このコードは、シンプルながらも複数の React Hooks を活用しており、React アプリケーション開発の良い練習になると思います。さらに改良したい点や機能を追加したい場合は、気軽にご相談ください!

と、駆け足でChatGPTと会話しながらReact hooksについて学んでみました。
まあChatGPTも100%正解をいうわけではないので、鵜呑みにしすぎず「これでいいのかな?」って思ったときは公式ドキュメントに当たるのがいいと思います。
日本語版React公式ページのReact hooksについての項目はこちら
Tanstack Queryについてはこちら。(英語ですが)

Discussion