📱

今日からReact Nativeを始めたい人のための速習React(後編)

2024/12/06に公開

この記事はReact Native 全部俺 Advent Calendar 6日目の記事です。

https://adventar.org/calendars/10741

このアドベントカレンダーについて

このアドベントカレンダーは @itome が全て書いています。

基本的にReact NativeおよびExpoの公式ドキュメントとソースコードを参照しながら書いていきます。誤植や編集依頼はXにお願いします。

今日からReact Nativeを始めたい人のための速習React (後編)

前編では、Reactの基本的な概念とコンポーネントの書き方について解説しました。この後編では、React Hooksについて詳しく説明します。

React Hooksとは?

React Hooksは、関数コンポーネントで状態管理や副作用を扱うための機能です。全てのHooksは use から始まる命名規則を持っています。

なぜHooksが必要か?

関数コンポーネントには以下のような特徴があります:

const Counter = () => {
  // この変数は毎回のレンダリングでリセットされる
  let count = 0;
  
  const increment = () => {
    count += 1;  // この変更は次のレンダリングで失われる
    console.log(count);
  };
  
  return (
    <button onClick={increment}>
      Count: {count}
    </button>
  );
};

このコードには2つの問題があります:

  1. count変数は関数が実行されるたびにリセットされる
  2. countの変更がUIの再描画(re-render)をトリガーしない

変数がリセットされるのを避けるために、グローバル変数として状態を持つのは以下の理由で推奨されません:

// 避けるべき例
let globalCount = 0;

const Counter = () => {
  const increment = () => {
    globalCount += 1;
  };
  
  return (
    <button onClick={increment}>
      Count: {globalCount}
    </button>
  );
};

問題点

  • 複数のコンポーネントインスタンス間で状態が共有されてしまう
  • テストが困難になる
  • コンポーネントの依存関係が不透明になる
  • サーバーサイドレンダリングで問題が発生する

これらの問題を解決するのがHooksです。

Hooksのルール

Hooksには以下の重要なルールがあります:

  1. Hooksは関数コンポーネントのトップレベルでのみ呼び出す
  2. 条件分岐やループの中でHooksを使用しない
  3. 通常の関数からHooksを呼び出さない
// 良い例
const Example = () => {
  const [count, setCount] = useState(0);
  const [name, setName] = useState("");
  
  return <div>{/* ... */}</div>;
};

// 悪い例
const BadExample = () => {
  if (someCondition) {
    const [count, setCount] = useState(0);  // 条件分岐の中でHook使用
  }
  
  for (let i = 0; i < 5; i++) {
    const [item, setItem] = useState("");  // ループの中でHook使用
  }
  
  return <div>{/* ... */}</div>;
};

主要なReact Hooks

1. useState - 状態管理の基本

useStateは、Reactコンポーネントに状態(state)を追加するための最も基本的なhookです。状態とは、時間とともに変化する可能性のあるデータのことで、ユーザー入力、APIレスポンス、フォームの値などがこれにあたります。

useStateの特徴

  • コンポーネントのレンダリングをトリガーする
  • コンポーネントがアンマウントされるまで値を保持する
  • 複数のuseStateを使用可能
  • 初期値は静的な値だけでなく、関数も指定可能

状態更新の注意点

  1. 状態の更新は非同期

    • setStateを呼び出した直後に新しい値が反映されているとは限らない
    • 複数の更新をまとめて1回のレンダリングで処理することがある(バッチ処理)
  2. 前の状態に依存する更新は関数を使う

    • カウンターの増加やリストへの追加など
    • 更新関数を使うことで、必ず最新の状態を参照できる
  3. オブジェクト状態の更新は必ず新しいオブジェクトを作成

    • 参照の比較で再レンダリングを判断するため
    • スプレッド演算子などを使って新しいオブジェクトを作成する
const Counter = () => {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
    </div>
  );
};

// オブジェクト状態の例
type User = {
  name: string;
  age: number;
  email: string;
};

const UserForm = () => {
  const [user, setUser] = useState<User>({
    name: "",
    age: 0,
    email: ""
  });
  
  // 良い例:スプレッド演算子で新しいオブジェクトを作成
  const updateName = (newName: string) => {
    setUser({
      ...user,
      name: newName
    });
  };
  
  return (
    <input
      value={user.name}
      onChange={e => updateName(e.target.value)}
    />
  );
};

2. useEffect - 副作用の管理

useEffectは、Reactの「純粋なレンダリング」の外側で実行する必要のある処理(副作用)を扱うためのhookです。副作用とは、コンポーネントの見た目以外に影響を与える処理全般を指します。

useEffectが必要となる主なケース

  1. 外部システムとの同期

    • APIからのデータ取得
    • WebSocketの接続管理
    • ブラウザAPIの利用(localStorage、geolocation等)
  2. 非React部分の制御

    • DOMの直接操作
    • サードパーティライブラリの初期化
    • アナリティクスの送信
  3. 状態の副次的な更新

    • 他の状態に依存する状態の更新
    • URLパラメータと状態の同期

useEffectの実行タイミング

  1. マウント時

    • コンポーネントが最初に表示されるとき
    • 依存配列が空の場合はこのタイミングでのみ実行
  2. 更新時

    • 依存配列に指定した値が変更されたとき
    • コンポーネントの再レンダリング後に実行
  3. アンマウント時

    • コンポーネントが画面から削除されるとき
    • クリーンアップ関数が実行される

useEffectを使う際の重要な考慮点

  1. クリーンアップの必要性

    • メモリリークを防ぐため、必要に応じてリソースを解放する
    • イベントリスナー、タイマー、サブスクリプションなど
  2. 依存配列の適切な設定

    • 必要な依存を全て含める
    • 不要な依存を含めない
    • 依存を減らすためにコールバック関数やメモ化を活用
  3. 実行タイミングの制御

    • 不要な実行を避けるため、依存配列を適切に設定
    • 条件付きで実行する場合は、エフェクト内で条件分岐
const UserProfile = ({ userId }: { userId: string }) => {
  const [user, setUser] = useState<User | null>(null);
  
  useEffect(() => {
    const fetchUser = async () => {
      const response = await fetch(`/api/users/${userId}`);
      const data = await response.json();
      setUser(data);
    };
    
    fetchUser();
  }, [userId]);  // userIdが変更されたときだけ再実行
  
  if (!user) return <div>Loading...</div>;
  
  return <div>{user.name}</div>;
};

// クリーンアップが必要な例
const ChatRoom = ({ roomId }: { roomId: string }) => {
  const [messages, setMessages] = useState<Message[]>([]);
  
  useEffect(() => {
    const ws = new WebSocket(`ws://chat.example.com/${roomId}`);
    
    ws.onmessage = (event) => {
      setMessages(prev => [...prev, event.data]);
    };
    
    return () => {
      // クリーンアップ関数
      ws.close();
    };
  }, [roomId]);
  
  return (
    <div>
      {messages.map(msg => <div key={msg.id}>{msg.text}</div>)}
    </div>
  );
};

3. useMemo/useCallback - パフォーマンス最適化

これらのhooksは、不要な再計算や再レンダリングを防ぐためのパフォーマンス最適化ツールです。

useMemo

何度計算しても計算結果が変わらないようなものを再描画のたびに毎回計算し直すのは効率が悪いので、一度計算したら結果を保存しておいて使いまわそうというアイデアをメモ化と呼びます。
Reactでこれを行うためのhooksがuseMemoです。

一般的にuseMemoは以下のようなケースで利用されます。

  1. 計算コストの高い処理の結果をキャッシュ
    • 複雑な計算や大量のデータ処理
    • 結果が頻繁に必要とされる処理
  2. オブジェクトの参照を安定化
    • propsとして渡すオブジェクト
    • useEffectの依存配列に含まれるオブジェクト

useCallback

Reactは再描画するべきコンポーネントを決定するために渡されているpropsが変わっているかを確認していて、変わっていなければそのコンポーネントの再描画をスキップする最適化を勝手にやってくれています(リコンサイルという仕組みです)。
しかし、propsにイベントハンドラーをアロー関数を直接渡すと、再描画ごとに新しい関数が渡されているとReactは判断してしまい、結果としてReactの再描画の最適化が効かなくなってしまいます。
これによるパフォーマンスの悪化を防ぐために、関数をメモ化するhooksがuseCallbackです。

メモ化できていればいいので、useMemoと役割は全く変わりません。関数をメモ化するのに特化させたuseMemoと思っていれば大丈夫です。

最適化hooks使用の注意点

  1. 過剰な最適化を避ける

    • 明確なパフォーマンス問題がない場合は不要
    • メモ化自体にも一応ごく僅かにコストがある
    • どうするべきか悩んだらメモ化しておくほうが無難
  2. 適切な依存配列の設定

    • 必要な値だけを依存に含める
    • 依存を最小限に保つ
    • eslintのルールでこれを強制できるので積極的に利用する
const ExpensiveComponent = ({ numbers }: { numbers: number[] }) => {
  // 重い計算をメモ化
  const sum = useMemo(() => {
    console.log("Calculating sum...");
    return numbers.reduce((acc, curr) => acc + curr, 0);
  }, [numbers]);

  const handleClick = useCallback(() => {
    console.log(`Current sum: ${sum}`);
  }, [sum]);
  
  return <button onClick={handleClick}>Show Sum</button>;
};

4. useRef - 値の参照と保持

useRefは、レンダリングに影響を与えずに値を保持するためのhookです。また、DOM要素への直接アクセスも提供します。

useRefの主な用途

  1. DOM要素への参照

    • 要素の測定
    • フォーカス制御
    • アニメーション
    • サードパーティライブラリとの統合
  2. レンダリングに影響しない値の保持

    • インターバルID
    • 以前の値の記憶
    • コンポーネントのマウント状態の追跡

useRefの特徴

  1. 値の変更がレンダリングをトリガーしない

    • UIに直接反映する必要のない値に適している
    • パフォーマンスへの影響が少ない
  2. 値が持続する

    • レンダリング間で値が保持される
    • コンポーネントがアンマウントされるまで維持
  3. ミュータブルな参照

    • .currentプロパティを通じて値を変更可能
    • 変更は即座に反映される

useRefの使用に関する注意点

  1. レンダリングに関係する値には使用しない

    • UIに表示される値はuseStateを使用
    • 値の変更をトリガーにした処理にはuseEffectを使用
  2. typescriptでの型安全性

    • 初期値nullの場合は適切な型ガードが必要
    • オプショナルチェイニングの活用
  3. クリーンアップの重要性

    • DOM要素への参照はアンマウント時に自動的にクリア
    • その他の値は必要に応じて手動でクリーンアップ
const TextInputWithFocus = () => {
  const inputRef = useRef<HTMLInputElement>(null);
  
  const focusInput = () => {
    inputRef.current?.focus();
  };
  
  return (
    <>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>Focus Input</button>
    </>
  );
};

// タイマーの例
const Timer = () => {
  const [count, setCount] = useState(0);
  const timerRef = useRef<number>();
  
  useEffect(() => {
    timerRef.current = window.setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
    
    return () => {
      if (timerRef.current) clearInterval(timerRef.current);
    };
  }, []);
  
  return <div>Count: {count}</div>;
};

まとめ

React Hooksは、関数コンポーネントに状態管理や副作用の機能を追加する強力なツールです。各hookには明確な用途があり、適切に組み合わせることで複雑なUIロジックを実装できます。

重要なポイント

  1. 基本的なパターン

    • 状態管理 → useState
    • 副作用 → useEffect
    • パフォーマンス最適化 → useMemo/useCallback
    • 参照管理 → useRef
  2. 使用上の注意

    • フックのルールを厳守
    • 適切な依存関係の管理
    • 過剰な最適化を避ける
  3. 応用

    • カスタムフックによるロジックの再利用
    • 複数のフックの組み合わせ
    • サードパーティライブラリの活用

これらの知識を基に、実際のReact Native開発でコンポーネントを実装していくことができます。
興味のある方は、Reactの公式チュートリアルをやってみてください。

Discussion