React:SWRで簡単にフェッチする

SWR とは

SWR(stale-while-revalidate) は、データ取得のための React Hooks ライブラリで、次の戦略で実装されている。

  1. キャッシュからデータを返す(stale)
  2. フェッチリクエストを送る(revalidate)
  3. 最新のデータを取得する

つまり、SWR を用いることで、コンポーネントはデータの更新を継続的かつ自動的に受け取ることができるようになる:

import useSWR from "swr";

function Profile() {
  const fetcher = (...args) => fetch(...args).then((res) => res.json());

  const { data, error, isLoading } = useSWR("/api/user", fetcher);

  if (error) return <div>failed to load</div>;
  if (isLoading) return <div>loading...</div>;
  return <div>hello {data.name}!</div>;
}

useSWR フックは、 第一引数に『データの一意な識別子(通常、API の URL)』、第二引数に『データを返す任意の非同期関数』を設定する。第二引数には、ネイティブの fetchAxios のようなツールを設定できる。

userSWR フックの戻り値の data, error, isLoading からリクエストの状態("loading", "ready", "error") を判断し、対応する UI を返すことができる。

useSWR の API

API は次の構成になっている:

const { data, error, isLoading, isValidating, mutate } = useSWR(
  key,
  fetcher,
  options
);
  • 引数

    データ名 説明
    key リクエストのためのユニークなキー文字列(関数、配列、nullも可)
    fetcher データをフェッチするための Promise を返す関数
    options SWR の細かい設定を行うためのパラメータ。再検証の設定や通信諸設定、リクエスト後のコールバックなど設定できる。詳細
  • 返り値

    データ名 説明
    data fetcher によって解決された、key のデータ(ロードされてない場合 undefined
    error fetcher によって例外が投げられた際にキャッチされたデータ
    isLoading 実行中のリクエストが存在し『ロードされたデータがない』状態
    isValidating リクエスト、または再検証の読み込みがある状態
    mutate(data?,options?) キャッシュされたデータを更新する関数

コンテキストとして SWR を使う

SWR ではコンテキストとしてデータをグローバルに扱えるようにするために SWRConfig コンテキストが用意されている:

<SWRConfig value={options}>
  <Component />
</SWRConfig>

value には、useSWR の引数の option パラメータを設定できる:

import useSWR, { SWRConfig } from "swr";

function Dashboard() {
  const { data: events } = useSWR("/api/events");
  const { data: user } = useSWR("/api/user", { refreshInterval: 0 }); // SWRConfig の設定をオーバーライド
  // ...
}

function App() {
  return (
    <SWRConfig
      value={{
        refreshInterval: 3000,
        fetcher: (resource, init) =>
          fetch(resource, init).then((res) => res.json()),
      }}
    >
      <Dashboard />
    </SWRConfig>
  );
}

上記は、全ての SWR フックを同じフェッチャーを用いて更新するように設定している。

関数として利用する

useSWR で SWR を定義しない方法もある:

import { SWRConfig, useSWRConfig } from "swr";

function App() {
  return (
    <SWRConfig
      value={{
        refreshInterval: 100,
        fallback: { a: 1, b: 1 },
      }}
    >
      <Page />
    </SWRConfig>
  );
}

function Page() {
  const config = useSWRConfig();
}

上記は、コンテキストとして SWR を子コンポーネントに渡している。useSWRConfig は、グローバル設定(コンテキスト)および、ミューテーション、キャッシュを取得している。

自動再検証

自動再検証に関して次のようなオプションがある:

オプション 説明 デフォルト値
revalidateOnFocus ページ時にフォーカスを合わせるか、タブを切り替えると自動的にデータを再検証する(参考 有効
refreshInterval 画面上に表示されているフックに関連づけられたコンポーネントを時間の経過とともに更新する(参考 無効
refreshWhenHidden ウィンドウが非常時の時にポーリングする 無効
refreshWhenOffline ブラウザがオフラインの時にポーリングする 無効
revalidateOnReconnect ネットワークが回復したときに再検証する 有効
revalidateOnMount コンポーネントのマウント時に再検証する 未定義
revalidateIfStale 古いデータがある場合でも再検証する 有効

条件付きフェッチ

条件付きでデータを取得する場合には、key(第一引数)に null か関数を設定する。関数がスローまたは偽値を返した場合、SWR はリクエストを開始しない:

// 条件付きでフェッチする
const { data } = useSWR(shouldFetch ? '/api/data' : null, fetcher)
 
// user.id が定義されてない場合にスローする
const { data } = useSWR(() => '/api/data?uid=' + user.id, fetcher)

引数

デフォルトでは、keyfetcher に渡される。下記の式は同じ:

useSWR('/api/user', () => fetcher('/api/user'))
useSWR('/api/user', url => fetcher(url))
useSWR('/api/user', fetcher)

複数の引数を渡したい場合は、次のように指定する:

const { data: user } = useSWR(['/api/user', token], ([url, token]) => fetchWithToken(url, token))

オブジェクトを渡すこともできる:

const { data: user } = useSWR(['/api/user', token], fetchWithToken)
 
// 上で設定した user を使用する
const { data: orders } = useSWR(user ? ['/api/orders', user] : null, fetchWithUser)

参考

React:エフェクトから依存値を取り除く

イベントハンドラとエフェクトの選び方

まずはイベントハンドラとエフェクトの動きの概要について振り返り。

  • イベントハンドラ

    同じユーザ操作を再度実行した場合のみ再実行。副作用も含んで良い。

  • エフェクト

    props, state 変数のようなリアクティブな値が変化したときに再同期される。レンダーコードは純粋である必要がある。

ポイントとなるのは、『リアクティブな値』という点で、特定のコードがリアクティブな値に同期してついていけるようにするために、その値が異なったときに再度実行されるようにしたい。そういう場合に、エフェクトを用いる。

エフェクトからの切り離し

リンタが React 向けに設定されている場合、エフェクトで利用されている全てのリアクティブな値は、依存配列として指定されている必要がある。されていない場合、リンタエラーになる。

次の roomId, theme は依存配列に書く必要がある:

function ChatRoom({ roomId, theme }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.on('connected', () => { showNotification('Connected!', theme); });
    connection.connect();
    return () => { connection.disconnect() };
  }, [roomId, theme]);
  // ...

上記のコードは、依存配列内のコードが変化すると同期されるが、依存配列内の値の中で監視する必要のない変数がある場合には、エフェクトイベント が使える。

useEffectEvent フックを用いて、非リアクティブなロジックを抽出する。エフェクトイベントはリアクティブでないので、依存配列から削除する。エフェクトイベントでは、常に最新の state, props 取得できる。

function ChatRoom({ roomId, theme }) {
  // エフェクトイベント
  const onConnected = useEffectEvent(() => { showNotification('Connected!', theme); });

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.on('connected', () => { onConnected(); });  // <= theme が抽出された
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);
  // ...

イベントハンドラとエフェクトイベントの違いは、イベントハンドラがユーザ操作に反応して実行されるのに対して、エフェクトイベントはエフェクトからトリガされる。

また、下記のコードは初回のみ同期され、handleMove 関数は canMovetrue の状態で記録される。つまり、エフェクトにより同期されることはないため、handleMove はずっと同じ処理となる。

// ...
const [canMove, setCanMove] = useState(true);

function handleMove(e) {
  if (canMove) {
    setPosition({ x: e.clientX, y: e.clientY });
  }
}

useEffect(() => {
  window.addEventListener("pointermove", handleMove);
  return () => window.removeEventListener("pointermove", handleMove);
}, []);

一方でエフェクトイベントを用いると値が再同期され、常に最新の state, props を参照できる

// ...
const [canMove, setCanMove] = useState(true);

const onMove = useEffectEvent((e) => {
  // <= イベントエフェクトでは常に最新の state, props が見える
  if (canMove) {
    setPosition({ x: e.clientX, y: e.clientY });
  }
});

useEffect(() => {
  window.addEventListener("pointermove", onMove);
  return () => window.removeEventListener("pointermove", onMove);
}, []);
// ...

エフェクトイベントの命名のコツ

経験則的にエフェクトイベントは、コードがいつ実行されるかではなく、ユーザ視点から何が起こったのかを基準にしてつけると良い。

  • 良い例:onMessage, onTick, onVisit, onConnected
  • 悪い例:onMount, onUpdate, onUnmount, onAfterRender

エフェクトで依存値を取り除く方法

エフェクトから依存値を取り除くにはいくつかやり方がある。

  • イベントハンドラへの移動を考える
  • エフェクト内の無関係なロジックは分ける
  • リンタに依存値でないことを示し、取り除く
  • 前回値を使う
  • リアクティブでない部分をエフェクトイベントへ抽出

イベントハンドラへの移動を考える

特定のユーザ操作に対応してコードを実行する場合は、当該ロジックをエフェクトではなくイベントハンドラに移動する。

下記コードはユーザ操作をエフェクトとしている例:

function Form() {
  const [submitted, setSubmitted] = useState(false);
  const theme = useContext(ThemeContext);

  useEffect(() => {
    if (submitted) {
      post("/api/register");
      showNotification("Successfully registered!", theme);
    }
  }, [submitted, theme]); // submitted はフォーム送信というユーザ操作に対応する

  function handleSubmit() {
    setSubmitted(true);
  }

  // ...
}

『ユーザ操作はイベントハンドラに移動する』に従い修正する:

function Form() {
  const theme = useContext(ThemeContext);

  function handleSubmit() {
    post("/api/register");
    showNotification("Successfully registered!", theme);
  }

  // ...
}

エフェクト内の無関係なロジックは分ける

依存値リスト内に複数のリアクティブな値があり、各依存値がロジックに関連がない場合、エフェクトは分割する。

下記のように各ロジックが互いに関係ないものを依存値リストに含めてしまうと、不必要にエフェクトを呼び出しロジックを実行することになる:

function ShippingForm({ country }) {
  const [cities, setCities] = useState(null);
  const [city, setCity] = useState(null);
  const [areas, setAreas] = useState(null);

  useEffect(() => {
    // country にのみ関係するロジック(cities と関係)
    // ...

    // city にのみ関連するロジック(city, areas と関係)
    // ...
  }, [country, city]);

関係がないならエフェクトを分け、互いを意図せずにトリガしないようにする:

function ShippingForm({ country }) {
  const [cities, setCities] = useState(null);
  useEffect(() => {
    // country にのみ関係するロジック(cities と関係)
    // ...
  }, [country]);

  const [city, setCity] = useState(null);
  const [areas, setAreas] = useState(null);
  useEffect(() => {
    // city にのみ関連するロジック(city, areas と関係)
    // ...
  }, [city]);

リンタに依存値でないことを示す

エフェクト内のリアクティブな値は、依存値リストに宣言しなければエラーとなってしまう:

const serverUrl = "https://localhost:1234";

function ChatRoom({ roomId }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, []); // <= roomId を依存値リストに入れない場合、リンタが起こってくる
  // ...
}

そこで、リンタに依存値が不要であることを示せれば依存値から取り除くことができる。

やることとしては、リアクティブな値をコンポーネントの外に出してあげることで、サイレンダー時に変更されないことを示すことができる:

const serverUrl = "https://localhost:1234";
const roomId = "music"; // <= コンポーネント外に出して、リアクティブでなくす

export default function ChatRoom() {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, []);
  // ...
}

前回値を使ってエフェクトの依存値を切り離す

下記のコードでは、新規メッセージがあるたびにエフェクトを実行し、既存の messages 配列に新規メッセージを追加した新しい配列で messages 配列を更新する。

問題は メッセージに変化があるたびに再接続が発生してしまうところ:

function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);
  useEffect(() => {
    connection.connect();
    connection.on('message', (receivedMessage) => {
      setMessages([...messages, receivedMessage]); // メッセージ配列を更新
    });
    return () => connection.disconnect();
  }, [roomId, messages]);
  // ...

messages を更新したいが、エフェクト内に書いてしまうと再同期のトリガとなってしまう。

上記の例では、前回値に対して更新をかけており、そうした場合には、state 更新関数にコールバックを渡す方法が使える。コールバック関数を使うことで前回値を使えるようになり、そうすることで state 変数を明示的に使う必要がなくなる👀(state 更新関数にコールバックを渡す

function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);
  useEffect(() => {
    const connection = createConnection();
    connection.connect();
    connection.on('message', (receivedMessage) => {
      setMessages(msgs => [...msgs, receivedMessage]); // <= コールバック関数を渡すと、コールバックの引数には前回値が入る
    });
    return () => connection.disconnect();
  }, [roomId]); // <= 依存値リストから messages を消せた
  // ...

リアクティブでない部分をエフェクトイベントへ抽出

isMutedtrue でない時に、ユーザが新しいメッセージを受信したら音を再生したいとすると、下記のようなコードになる:

function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);
  const [isMuted, setIsMuted] = useState(false);

  useEffect(() => {
    const connection = createConnection();
    connection.connect();
    connection.on('message', (receivedMessage) => {
      setMessages(msgs => [...msgs, receivedMessage]); // <= メッセージ配列更新
      if (!isMuted) {
        playSound(); // <= 受信音を再生
      }
    });
    return () => connection.disconnect();
  }, [roomId, isMuted]); // <= isMuted の ON/OFF 再生を制御したいが、、、
  // ...

課題は isMuted が更新されると不必要にネットワークの再接続が行われてしまうことにある。isMuted の最新値により処理を切り替えたいが isMuted には反応させたくない、そんな時に使えるのがエフェクトイベント。

エフェクトイベント内では、リアクティブな値(state, props)は最新値が使えるので下記のように抽出すれば良い:

import { useState, useEffect, useEffectEvent } from 'react';

function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);
  const [isMuted, setIsMuted] = useState(false);

  // リアクティブな値に反応させたくない部分を抽出
  const onMessage = useEffectEvent(receivedMessage => { 
    setMessages(msgs => [...msgs, receivedMessage]);
    if (!isMuted) {
      playSound();
    }
  });

  useEffect(() => {
    // ...
    connection.on('message', (receivedMessage) => {
      onMessage(receivedMessage);
    });
    return () => connection.disconnect();
  }, [roomId]); // isMuted を排除できた
  // ...

props をラップする

props が親コンポーネントから動的に変わるようなコードがある:

function ChatRoom({ roomId, onReceiveMessage }) {
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    // ...
    connection.on('message', (receivedMessage) => { // connect.on のコールバックの戻り値が receivedMessage に返る
      onReceiveMessage(receivedMessage);
    });
    // ...
  }, [roomId, onReceiveMessage]); // 親コンポーネントから動的に渡される
  // ...

props に反応させたくない場合は、エフェクトイベントへ退避してあげれば良い:

function ChatRoom({ roomId, onReceiveMessage }) {
  const [messages, setMessages] = useState([]);

  // ラッパ関数オブジェクトを用意
  const onMessage = useEffectEvent(receivedMessage => {
    onReceiveMessage(receivedMessage);
  });

  useEffect(() => {
    // ...
    connection.on('message', (receivedMessage) => {
      onMessage(receivedMessage); // コールバックにはラッパ関数を渡す
    });
    // ...
  }, [roomId]); // 依存値リストから排除できた
  // ...

参考

OSS 開発活動は怖くないよ

初めてOSSにIssueを立てました🙌この記事ではアラサーおじさんの僕が OSS 開発活動を通して感じたことや学んだことなどをまとめておこうと思います。

なんで OSS 開発活動をしてみようと思った?

業務でも個人でも OSS を利用をする機会がとても増えてきていて、そのOSSの活動がどのように行われているのか気になっていた。一方で、OSS開発活動は つよつよなエンジニア達による主戦場という認識であったため敬遠していました。

お世話になっているプログラミングスクールのフィヨルドブートキャンプから案内があり、せっかくの機会ということで OSS開発活動に参加してみました💪

OSS 開発活動は OSS Gate コミュニティで実施されているワークショップにて実施しました!

覚えておきたい、OSS 開発活動の肝

OSS活動の肝の考えは『助け合いの精神』ということ。

上記を踏まえるとOSSの活動は Issue の報告のみでもよく、 必ずしも改善方法を提案する必要はない ということでした。

これまでの OSS 活動のイメージは、バグを見つけたら『バクと改善方法の提案』までがセットだと思っていたので、活動のハードルはだいぶ下がった気がします。

またIssueを立てた際に、『採用されようが採用されまいがOSS活動としてはOK』ということもお聞きした。そうした部分も OSS のコミットをする上で、だいぶ障壁が下がった気がしました。

ただあくまでも『助け合い』の場なので、要望のみの場合は対応されるかどうかは微妙で、『バグ報告』や『バグ報告+解決策』の方が採用される可能性は高いということでした。

OSS 開発活動の作業時のポイント

  • 作業時は常にメモする

    なぜならIssueを立てる際に情報を書く必要があり、手順を残しておくことがとても大事なため。

  • OSS 使用時に詰まったところはコントリビュートのチャンス

    自分が詰まったところは誰かも必ず詰まる。すなわち、改善の種!!

  • コードを書くだけがOSS開発参加の方法でない

    バグ報告だけでも要望を出したりするだけでも OSS 活動になる。なぜなら誰かが使ってフィードバックすることでソフトウェアが進化し、よりよくなっていくため。OSS は『みんなで助け合い』ながら良いソフトウェアを作っていく。

それって OSS

OSS かどうかの判断は、Open Source Initiative に管理されているかで行う。

OSSには、グレーという部分はなく上記に登場しないものは OSS でないと判断する(OSS の性質を満たすかどうかは別の話)

OSS 活動を進める際に上記で管理されないようなライセンスの場合は、ライセンス原文をちゃんと読んで自分の信念(コードのオープン性に関する考え)に一致するかを判断して進めることが求められる。

余談

OSS Gate のワークショップでは OSS にリクエストを出すところまでの一連を実施します。

ワークショップ中は、サポータの方に指導いただきながら進めるのですが、ちょっとでも良い点があれば褒めてくれるので、とても嬉しいと気持ちになります☺️

参考

React:Effect とは

Effect を理解するためには次のロジックを押さえておく必要がある。

  • レンダーコード

    コンポーネントのトップレベルにあるもので、propsstate から画面に表示したい JSX を返す場所。レンダーコードは純粋関数*1

  • イベントハンドラ

    コンポーネント内にネストされた関数で、計算だけでなく実行する。特定のユーザアクションによってプログラムの状態を変更する(副作用を与える)。

上記だけでは十分でないシーンがある。例えば、サーバとの通信を維持するような処理は、純粋関数ではないのでレンダーコードへは書けないし、『クリックする』といった特定のイベントもない。

そこで使うのが Effect で、特定のイベントによってではなく、レンダー自体によって引き起こされる副作用を指定するためのもので、コミットの最後に画面が更新された後に実行される。

Effect は React 外の外部システムとの同期が必要な場合*2において使用されるものと覚えておけば良さそうで、そうしたシーン以外では不要。

Effect の書き方

Effect は大きく3つのフローからなる。

  1. Effect を宣言する
  2. Effect の依存値の配列を指定する
  3. 必要に応じてクリーンアップを追加する

Effect を宣言する

まずは useEffect をインポートする:

import { useEffect } from "react";

次に、コンポーネントのトップレベルで呼び出し、Effect 内にコードを記述する:

function MyComponent() {
  useEffect(() => {
    // Recat はレンダーされた後に、Effect 内の処理を実行する
    // レンダーされる度にやってほしい処理を書く
  });
  return <div />;
}

Effect で囲わない場合、レンダー中に実行されることになる。レンダーコードは純粋関数である必要があり、副作用があるようなものは記述できない。

そこで、useEffect でラップし、レンダーの計算処理の外に出してしまう。そうすることで、React は Effect の処理はレンダー後に実行すべき処理と認識し、レンダーが完了した後に実行するようにできる。

Effect の依存値の配列を指定する

デフォルトでは、Effect は全てのレンダー後に実行されるが、望ましくない場合がある(キーストロークごとのレンダー後に何かを実行するなど)。

そこで、useEffect の呼び出しの第2引数に依存値の配列を渡し、React に再実行不要な Effect を指示することができる:

useEffect(() => {
  // ...
}, []);

Effect の処理が依存する変数を配列内に定義することで、渡した変数が変化するときのみ Effect の処理が実行するように React に教えることができる。

useEffect(() => {
  if (isPlaying) {
    // 依存変数はここで使われている
    // ...
  } else {
    // ...
  }
}, [isPlaying]); // Effect が依存する変数を書く

React は配列内の全ての値が前回レンダー時と同じ場合のみ Effect の処理をスキップする。

必要に応じてクリーンアップを追加する

クリーンアップ関数は、次のタイミングで React が呼び出す関数:

  • Effect が再度実行される前
  • コンポーネントがアンマウント(削除) される時の最後

書き方は次のように return でクリーン関数を返すようにする:

  useEffect(() => {
    const connection = createConnection();
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, []);

上記コードにおいて クリーンアップが無い場合、再マウント時に connection.connect() が呼ばれ2つ目の接続などが実行されてしまう。

それをクリーンアップ関数を用いることで、一度切断してから接続というフローを取らせることができるようになる。

ちなみに React ではそうした不具合を見つけやすくるために開発環境においては、初回マウント直後に全てのコンポーネントを一度だけ再マウントし気づきやすくしているらしい👀

参考

*1:同じ入力に対して常に同じ出力(副作用を与えない)

*2:ブラウザ APIサードパーティウィジェット、ネットワーク等

React:ref とは

DOM ノードにアクセスする時に使うのが ref

本来 React がレンダー結果に応じて DOM を自動更新するので、コンポーネントで DOM を操作することはないが、ノードへのフォーカスやサイズ・位置測定であったり、React が公開していないブラウザ API 呼び出しなどをしたいときに使える。

ブラウザ API 呼び出し時の注意点としては、フォーカスやスクロールなどの非破壊的なアクションであれば問題ない。一方で、React を介さずに DOM を書き換えるような操作には細心の注意が必要。

理由としては、React を介さずに直接 DOM を書き換えてしまうと、React は以降で正しくそれらを管理できなくなってしまうため。

よって、React が管理する DOM ノードを手動で変更する場合においては、React が更新しない部分のみとする。

ref の使い方

  1. useRef フックのインポート

     import { useRef } from 'react';
    
  2. コンポーネント内で宣言

     const myRef = useRef(null);
    
  3. 参照したい DOM ノードの ref 属性にオブジェクトを渡す

     <div ref={myRef}>
    
  4. React が <div> に対応する DOM ノード作成後に組み込みブラウザ API を使用する

     myRef.current.scrollIntoView();
    

ポイントだなと思ったのは、ref 属性を設定するのは非破壊的なアクションを実行したい DOM ノードで、何をするかはイベントハンドラで設定する:

import { useRef } from "react";

export default function Page() {
  const ref = useRef(null);

  return (
    <>
      <nav>
        {/* イベントハンドラで ref 属性を設定した DOM ノードに対して実行したい組み込みブラウザ API を呼び出す */}
        <button onClick={() => ref.current.focus()}>Search</button>
      </nav>

      {/* 非破壊的なアクション(フォーカス)を実行したい DOM ノードに対して ref 属性を設定 */}
      <input placeholder="Looking for something?" ref={ref} />

    </>
  );
}

ref を参照すべきでないタイミング

Reactでの更新は2つのフェーズからなり、更新タイミングでは ref を参照すべきでない。

  • レンダー中に、React はコンポーネントを呼び出して画面に表示される内容を決定する

レンダー中は DOM がまだ作成されていないため ref.currentnull になっており、またレンダー中は DOM ノードが更新されていないため参照すべきでない。

  • コミット中に、React は DOM に変更を適用する

React が ref.current をセットするのはコミット中。React は DOM を更新する前に一度 ref.currentnull に設定する。

React は DOM を更新後に当該ノードの ref をセットするようになっている。よって、コミット中も値が確定しているわけではないので、参照すべきでない。

以上から、ref を参照すべきタイミングはイベントハンドラになる。

参考

React:リデューサ関数とは

リデューサ関数とは

コンポーネントが複雑になっていくと、state の更新を追跡するのが大変になる。

例えば、タスクの追加・編集・削除などがある場合、それぞれに対応するボタンにイベントハンドラを割り付け、セット関数によって更新するという流れになる:

export default function TaskApp() {
  const [tasks, setTasks] = useState(initialTasks);

  function handleAddTask(text) {
    setTasks([
      ...tasks,
      { id: nextId++, text: text, done: false, },
    ]);
  }

  function handleChangeTask(task) {
    setTasks(
      tasks.map((t) => {
        if (t.id === task.id) { return task; }
        else { return t; }
      })
    );
  }

  function handleDeleteTask(taskId) {
    setTasks(tasks.filter((t) => t.id !== taskId));
  }
  // ...略...
}

そこで使えるのがリデューサ関数で、コンポーネントの外部に関数に state ロジックを移動させることができる。

リデューサ関数への移行は次のステップで行える。

  1. state セットをアクションのディスパッチに置き換える
  2. リデューサ関数を作成する
  3. コンポーネントからリデューサを使用する

state セットのアクションをディスパッチに置き換える

リデューサは、state をセットして『何をするか』を指示するのではなく、イベントハンドラから『アクション』をディスパッチ(割り当て)して指定する。

ディスパッチへの置き換えは次の手順で進める:

  • state をセットするロジックを全て削除
  • イベントハンドラからアクションをディスパッチする
 function handleAddTask(text) {
   dispatch({
     type: 'added',
     id: nextId++,
     text: text,
   });
 }

 function handleChangeTask(task) {
   dispatch({
     type: 'changed',
     task: task,
   });
 }

 function handleDeleteTask(taskId) {
   dispatch({
     type: 'deleted',
     id: taskId,
   });
 }

ディスパッチに渡しているオブジェクトを "アクション"と呼ぶ。慣習として、何が起こったかを説明する type を与え、他のフィールドで追加情報を渡す。

typeコンポーネント固有のものとする。

リデューサ関数を作成する

リデューサ関数に state のロジックを記述する。以下の手順を踏む:

  • 現在の state を最初の引数として宣言
  • action オブジェクトを2番目の引数として宣言
  • リデューサから次の state を返す(state へのセットは React が処理する)
function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [
        ...tasks,
        {
          id: action.id,
          text: action.text,
          done: false,
        },
      ];
    }
    case 'changed': {
      return tasks.map((t) => {
        if (t.id === action.task.id) {
          return action.task;
        } else {
          return t;
        }
      });
    }
    case 'deleted': {
      return tasks.filter((t) => t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}

上記のように、リデューサ関数は state を引数として受け取るので、コンポーネントの外部で宣言可能。

コンポーネントからリデューサを使用する

React から useReducer フックをインポートする。

useState の呼び出しを useReducer で置き換える:

const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

良いリデューサの書き方

  • リデューサは純粋である必要あり

    同じ入力に対して常に同じ出力とし、オブジェクト配列を書き換えせずに更新する。よって、リデューサの中で動的に変化するようなロジック入れない。

  • 各アクションは、複数データの更新でも単一のユーザ操作を記述する

    個々にアクションをディスパッチするよりも、1つにまとめてディスパッチする方が効率的

直接 state をセットするのとリデューサの違いのまとめ

分類 説明
state のセット イベントハンドラに『何をするか(タスク)』をセットする
リデューサ イベントハンドラに『何をしたか(アクション)』を割り当てる

ToDo アプリのコードを参考にすると、コード的には次のような違い:

// state を直接セットする場合は、何をするかをセットする
function handleAddTask(text) {
  setTasks([
    ...tasks,
    {
      id: nextId++,
      text: text,
      done: false,
    },
  ]);
}

// reducer の場合は、アクションを割り当てる
function handleAddTask(text) {
  dispatch({
    type: 'added',  // 慣習として type にアクションの説明を設定
    id: nextId++,
    text: text,
  });
}

上記の場合、reducer のロジックがないため、ロジック部分をリデューサ関数内に記述する:

function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [
        ...tasks,
        {
          id: action.id,
          text: action.text,
          done: false,
        },
      ];
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}

リデューサ関数では、第1引数に現在の state, 第2引数に action オブジェクトを渡し、リデューサ関数では 次の state が戻り値になる。

参考

React:イベントハンドラと state

インタラクティビティの追加 – Reactで理解した内容をまとめていきます💪

イベントへの応答

イベントハンドラ

クリック、フォーム入力といったユーザ操作に対して応答し取りがされる独自関数のことを イベントハンドラ と呼ぶ:

export default function Button() {
  function handleClick() {
    alert("hogehoge");
  }

  return <button onClick={handleClick}>Click me</button>;
}

上記の handleClick()イベントハンドラで、props として <button> タグに渡している。イベントハンドラは次のように記述する:

  • 通常、コンポーネント内部で定義
  • イベント名の先頭に handle をつけた名前にする(慣習的に onイベント名というハンドラ名にする。インラインで定義することも可能。)

重要なのは ハンドラ を渡すことで、ハンドラを呼び出すことではない。
(正:onClick={handleClick}、誤:onClick={handleClick()})

慣習的に独自のイベント名は on で始まるものにする。

イベントオブジェクト

イベントハンドラはイベントオブジェクトを唯一の引数として受け取り、慣習的に e (event)と言う変数で受け取る。

イベントオブジェクトで次のようなこともできる:

  • 伝搬の停止

    イベントオブジェクトは伝搬の停止にも使用できる。親への伝搬を停止する際には e.stopPropagation() を呼び出す:

  function Button({ onClick, children }) {
    return (
      <button onClick={e => {
        e.stopPropagation();
        onClick();
      }}>
    );
  }
  • デフォルト動作の防止

    ブラウザのイベントには <form> タグのようにボタンクリック時にページ全体のリロードを実行するものがある。SPA とかだとリロードなどはやめてほしい状況がある。そうしたときには e.preventDefault() を呼び出す:

  export default function Signup() {
    return (
      <form onSubmit={e => {
        e.preventDefault();
        alert('Submitting!');
      }}>
        <input />
        <button>Send</button>
      </form>
    );
  }

state:コンポーネントのメモリ

コンポーネントでデータ更新するために必要なこと

コンポーネントのデータ更新を行うには次のことが必要:

  • レンダー間でデータを保持する(state変数
  • 変数を更新し、React がコンポーネントを再度レンダーするようにトリガする(state セッタ関数

state 変数にデータを保持して、値更新はセッタ関数で行う。ローカル変数の変更はレンダーのトリガにならないので注意。

state は次のようにして使う:

import { useState } from "react";

const [index, setIndex] = useState(0); // state 変数の定義。index を 0 で初期化している

function handleClick() {
  setIndex(index + 1); // state 変数の更新
}

useState 関数からは常に 2 個の要素が返される。2回目以降のレンダーでは、React が state が更新されていることを覚えているので、useState(0) は更新されたオブジェクトを返す。([0, setIndex]は返らない)

state セット関数にはコールバック関数が渡せる

state セット関数にはコールバック関数が渡せ、そのコールバックには前の変数値(前回値)が渡される。

それを利用して、コールバック関数内では前の状態を利用して新しい状態を計算することができる。

const [count, setCount] = useState(0);

const increment = () => {
  setCount(prevCount => prevCount + 1); // <= コールバック関数を渡す(前の変数値を利用して新しい状態を計算)
};

// ...

<button onClick={increment}>Increment</button>

上記の例では、コールバック関数の実行結果が setCount により count を計算する。コールバック関数内では、前回値を利用して新しい値を計算する。

フックとは

React の useState やその他の use で始まる関数のことをフック(Hook)と呼ぶ。フックは React がレンダーされているときにだけ使える特別な関数で、さまざまな React の機能に『接続(Hook into)』して使用できるため、フックと呼ばれる。

フックは、コンポーネントのトップレベルもしくはカスタムフック内(組み込みでないもの)でしか呼び出せないので注意。

参考