🍄

よくわからない「use~」編

2025/02/08に公開

想定読者

Reactを勉強中で、use~ってつくものをたくさんimport文で書くけどこれってなんなんだ?の人向けです。

HTML&CSSを学んだ後、静的ではなく動的なWebサイトを作りたくなりJavaScriptやTypeScriptを学び始めた人にとってReactへの挑戦心が生まれるようなより良い記事になりますように。

use~のご紹介

ReactのWebサイトのフックの欄を見たところ、useから始まるものが複数あることが確認できます。(こんなにあると思わなくて、useStateuseEffectだけ解説するつもりでした...)しかしここはめげずに全部の特徴を調べてコード付きで載せていきたいと思います。

useActionState

まずは、useActionStateから...

import { useActionState } from "react";

export default function Counter() {
  const [count, increment] = useActionState(
    (prevCount: number) => prevCount + 1,
    0
  );

  return (
    <div>
      <p>カウント: {count}</p>
      <form action={increment}>
        <button type="submit">増やす</button>
      </form>
    </div>
  );
}

このように、useActionStateを用いることでcountが管理されています。

useCallback

import { useState, useCallback } from "react";

export default function Counter() {
  const [count, setCount] = useState(0);

  const increment = useCallback(() => {
    setCount((prev) => prev + 1);
  }, []);

  return (
    <div>
      <p>カウント: {count}</p>
      <button onClick={increment}>増やす</button>
    </div>
  );
}

useCallback を使い、 increment 関数をメモ化(関数の計算結果を保存し、同じ入力なら再計算せずに保存した結果を再利用する最適化手法)する。
メモ化により、不要な計算や再レンダリングを減らし、パフォーマンスを向上させることができる。そして不要な再レンダリング(Reactコンポーネントの表示を更新するプロセスが状態(state)やプロパティ(props)が変更された時の2度目のレンダリング)を防いでいます。

useContext

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

const ThemeContext = createContext(null);

export default function App() {
  const [theme, setTheme] = useState("light");
  return (
    <ThemeContext.Provider value={{ theme, toggle: () => setTheme(theme === "light" ? "dark" : "light") }}>
      <ThemedComponent />
    </ThemeContext.Provider>
  );
}

function ThemedComponent() {
  const { theme, toggle } = useContext(ThemeContext);
  return (
    <button onClick={toggle} style={{ background: theme === "light" ? "#fff" : "#333", color: theme === "light" ? "#000" : "#fff" }}>
      {theme} モード
    </button>
  );
}

ThemedComponent で useContext を使い、テーマに応じてボタンの色を変更できますし、クリックすると lightとdark を切り替えることができます。

useDebugValue

import { useState, useDebugValue } from "react";

function useTheme() {
  const [theme, setTheme] = useState("light");
  useDebugValue(theme === "light" ? "Light Mode" : "Dark Mode");

  const toggleTheme = () => setTheme((prev) => (prev === "light" ? "dark" : "light"));
  return { theme, toggleTheme };
}

export default function App() {
  const { theme, toggleTheme } = useTheme();

  return (
    <button onClick={toggleTheme} style={{ background: theme === "light" ? "#fff" : "#333", color: theme === "light" ? "#000" : "#fff" }}>
      {theme} モード
    </button>
  );
}

useTheme カスタムフック内でuseDebugValueを使用し、開発ツールで分かりやすく表示。

useDeferredValue

import { useState, useDeferredValue } from "react";

export default function Search() {
  const [query, setQuery] = useState("");
  const deferredQuery = useDeferredValue(query); // 遅延した値

  return (
    <div>
      <input type="text" value={query} onChange={(e) => setQuery(e.target.value)} placeholder="検索..." />
      <p>検索結果: {deferredQuery}</p>
    </div>
  );
}

query はリアルタイムで更新されるが、deferredQuery は少し遅れて反映される。
高負荷な検索処理がある場合に useDeferredValue を使うと、入力の遅延を抑えつつ、レンダリングを最適化 できる。

useEffect

import { useEffect } from "react";

export default function App() {
  useEffect(() => {
    console.log("コンポーネントがマウントされました!");
  }, []); // 空の依存配列で初回のみ実行

  return <p>コンソールをチェックしてください!</p>;
}

useEffect の 第2引数に [] を指定すると、初回マウント時のみ実行 される。

useId

import { useId } from "react";

export default function Form() {
  const id = useId(); // ユニークなIDを生成

  return (
    <div>
      <label htmlFor={id}>名前:</label>
      <input id={id} type="text" placeholder="名前を入力" />
    </div>
  );
}

useId は コンポーネントごとに一意な ID を自動生成 する。

useImperativeHandle

親コンポーネントから子コンポーネントの関数を直接呼び出せるようにする

import { useRef, useImperativeHandle, forwardRef } from "react";

const InputComponent = forwardRef((_, ref) => {
  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current.focus(),
  }));

  const inputRef = useRef<HTMLInputElement>(null);

  return <input ref={inputRef} type="text" placeholder="クリックでフォーカス" />;
});

export default function App() {
  const inputRef = useRef<{ focus: () => void }>(null);

  return (
    <div>
      <InputComponent ref={inputRef} />
      <button onClick={() => inputRef.current?.focus()}>フォーカスを当てる</button>
    </div>
  );
}

forwardRef を使って 子コンポーネント (InputComponent) の ref を親で取得できるようにする。
useImperativeHandleを使い、focus 関数を ref 経由で 親コンポーネントから直接呼び出せる ようにする。
App 内のボタンをクリックすると、input にフォーカスが当たる。
useImperativeHandle を使うと カスタムのメソッドを ref 経由で親に公開 できるので、フォームやモーダルの制御などに役立ちます!

useMemo

import { useState, useMemo } from "react";

export default function App() {
  const [number, setNumber] = useState(0);

  // 重い計算を useMemo でメモ化
  const expensiveCalculation = useMemo(() => {
    console.log("計算中...");
    return number * 2;
  }, [number]);

  return (
    <div>
      <p>入力された数値: {number}</p>
      <p>計算結果: {expensiveCalculation}</p>
      <button onClick={() => setNumber(number + 1)}>数値を増やす</button>
    </div>
  );
}

useMemoを使って、expensiveCalculationの結果をnumberが変わらない限り再計算しないようにする。
numberが変わるときのみ、計算結果が更新され、その他のレンダリングは無駄な計算を防ぎ、console.log で計算中のログを確認でき、再計算が発生するタイミングを知ることができる。

useReducer

import { useReducer } from "react";

// 状態を管理するリデューサー関数
const counterReducer = (state: number, action: { type: string }) => {
  switch (action.type) {
    case "increment":
      return state + 1;
    case "decrement":
      return state - 1;
    default:
      return state;
  }
};

export default function App() {
  // useReducerを使ってカウンター状態を管理
  const [count, dispatch] = useReducer(counterReducer, 0);

  return (
    <div>
      <p>現在のカウント: {count}</p>
      <button onClick={() => dispatch({ type: "increment" })}>増加</button>
      <button onClick={() => dispatch({ type: "decrement" })}>減少</button>
    </div>
  );
}

useReducerは、状態の更新ロジックをリデューサー関数 (counterReducer)に分離して管理。
useReducerは複雑な状態管理やロジックを外部に切り出すのに便利である。

useRef

import { useRef } from "react";

export default function App() {
  const inputRef = useRef<HTMLInputElement>(null);

  const focusInput = () => {
    inputRef.current?.focus(); // input フィールドにフォーカスを当てる
  };

  return (
    <div>
      <input ref={inputRef} type="text" placeholder="ここに入力" />
      <button onClick={focusInput}>フォーカスを当てる</button>
    </div>
  );
}

useRefは、コンポーネント内のDOM要素への参照を保持します。
ref によって input フィールドを操作できるようにし、ボタンをクリックするとフォーカスを当てます。
useRefはレンダリングをトリガーせず、状態を持たない参照を保持します。

useState

import { useState } from "react";

export default function App() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>現在のカウント: {count}</p>
      <button onClick={() => setCount(count + 1)}>増加</button>
      <button onClick={() => setCount(count - 1)}>減少</button>
    </div>
  );
}

useStateを使って カウントの状態 (count) を管理。
setCount を使ってボタンがクリックされたときにカウントを増減させる。
シンプルに 状態管理 を行うため、非常に直感的に使えるフックである。
(個人的には使いやすいと感じる)

useSyncExternalStore

import { useSyncExternalStore } from "react";

// 簡単なカスタムストア
let storeState = 0;
const listeners = new Set<Function>();

function subscribe(listener: Function) {
  listeners.add(listener);
  return () => listeners.delete(listener);
}

function getStoreState() {
  return storeState;
}

function setStoreState(newState: number) {
  storeState = newState;
  listeners.forEach(listener => listener()); // リスナーに通知
}

export default function App() {
  const state = useSyncExternalStore(subscribe, getStoreState);

  return (
    <div>
      <p>現在の状態: {state}</p>
      <button onClick={() => setStoreState(state + 1)}>状態を増やす</button>
    </div>
  );
}

グローバル状態 storeStateを管理し、subscribeとgetStoreStateを定義して外部ストアのように動作させている。
useSyncExternalStoreを使って、コンポーネントが外部ストアの状態に同期できるようにしている。
setStoreState を使ってボタンをクリックすると状態を更新し、その変更が即座に反映される。
(使ったことないけど使いやすそう?)

useTransition

import { useState, useTransition } from "react";

export default function App() {
  const [isPending, startTransition] = useTransition();
  const [query, setQuery] = useState("");
  const [items] = useState(["apple", "banana", "orange", "grape", "melon"]);

  const filteredItems = items.filter(item =>
    item.toLowerCase().includes(query.toLowerCase())
  );

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = e.target.value;
    startTransition(() => {
      setQuery(value);
    });
  };

  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={handleChange}
        placeholder="アイテムを検索"
      />
      {isPending ? <p>読み込み中...</p> : null}
      <ul>
        {filteredItems.map((item, index) => (
          <li key={index}>{item}</li>
        ))}
      </ul>
    </div>
  );
}

【重い処理をするのに向いている】
useTransition を使って、入力中の検索クエリに基づいてリストをフィルタリング。
startTransition によって、検索入力の処理を優先度の低い非同期処理として実行。
フィルタリング中は、isPending を使って ローディング状態 を表示。
startTransition を使用することで、UIのスムーズさを保ちつつ、遅延が発生する部分をバックグラウンドで処理。

まとめ

私自身、useStateUseEffectくらいしか意識して使っていなかったので、やはりドキュメントを読んで試してみる、実際に使ってみるというのは大事ですね。私の中で「ドキュメントを読む」というのは難しいことで避けたい気持ちが強くあったのですが、useActionStateuseCallbackで書き方が違うのにもかかわらず内容が同じというのもなかなか刺激的でした。

今後の方針

月に1本以上記事をあげることを目標にしているので、また読んでいただけると嬉しいです。

それでは、さよーならー!

Discussion