React で変数の更新を監視するには?
React で変数の値が更新された場合、その変数を使った表示を更新するのは、ステートで監視すれば良いので簡単です。しかし、変数が更新された際に、その変数を使って別の操作をする場合の記述方法を紹介します。
基本はステート管理
React でデータの更新に合わせて、表示データを更新するには、ステートを使って管理するのが基本です。 今回の課題は、Vue の公式チュートリアル(英語版)のステップ10を基にしています。この課題は、「JSON placeholder」と呼ばれるサイトから、 ID を基にしたデータを取得して表示する物です。
ボタンがクリックされる度に ID を更新して、それに伴うデータを取得して表示するというのが課題です。
一番シンプルなのは、ボタンがクリックされたイベントを検出して、その際に一緒にそれに対応するデータを取得して表示データの更新を行うという方法です。この方法を使う場合、これ迄のステートを使った、データ管理を行えば実現可能です。
import React, { useEffect, useState } from "react";
import "./styles/step10.css";
interface TodoData {
userId: number;
id: number;
title: string;
completed: boolean;
}
export default function Step10(): JSX.Element {
const [todoId, setTodoId]: [
number,
React.Dispatch<React.SetStateAction<number>>
] = useState(1);
const [todoData, setTodoData]: [
TodoData | null,
React.Dispatch<React.SetStateAction<TodoData | null>>
] = useState(null as null | TodoData);
useEffect((): void => {
fetchData(todoId);
}, []);
return (
<React.Fragment>
<div>
<p>Todo id: {todoId} </p>
<button className="btn btn-primary" onClick={() => buttonClicked()}>
Fetch next todo
</button>
{todoData === null ? (
<p>Loading...</p>
) : (
<React.Fragment>
<pre>{JSON.stringify(todoData)}</pre>
</React.Fragment>
)}
</div>
</React.Fragment>
);
function fetchData(id: number): void {
setTodoData(null);
fetch(` h t t p s://jsonplaceholder.typicode.com/todos/${id}`).then(
(res: Response): void => {
res.json().then((tododata: TodoData): void => {
setTodoData(tododata);
});
}
);
}
function buttonClicked():void {
setTodoData(null);
const id: number = todoId + 1;
setTodoId(id);
fetchData(id);
}
}
のように記述すれば、機能は実現できます。
最初のデータは、この部品が表示できる状態(マウントされた状態)になった時に、「_useEffect()_」を使って取得するようにしています。 そのあとは、ボタンがクリックされる度に更新された ID に相当するデータをネットワークから取得して表示データを更新すれば、 必要な機能が実装できます。
Vue のチュートリアルのやり方
Vue の公式チュートリアル(英語版)では、「Watcher」を使ってこの機能を実現しています。 実装のやり方は二段階になっています。
* ボタンがクリックされたら ID の値を更新(1づつ加算)
* ID が更新されたのを検出して、新しいデータをネットワークから取得
上の例では、この二つの処理を一緒にして、ID が更新される際に同時に新しいデータを取得していました。 Vue のチュートリアルの方法は、これを別々に行う方法です。
React でも同じような実装が可能です。
import React, { useEffect, useState } from "react";
import "./styles/step10.css";
interface TodoData {
userId: number;
id: number;
title: string;
completed: boolean;
}
export default function Step10(): JSX.Element {
const [todoId, setTodoId]: [
number,
React.Dispatch<React.SetStateAction<number>>
] = useState(1);
const [todoData, setTodoData]: [
TodoData | null,
React.Dispatch<React.SetStateAction<TodoData | null>>
] = useState(null as null | TodoData);
useEffect((): void => {
fetchData();
}, [todoId]);
return (
<React.Fragment>
<div>
<p>Todo id: {todoId} </p>
<button
className="btn btn-primary"
onClick={() => setTodoId(todoId + 1)}
>
Fetch next todo
</button>
{todoData === null ? (
<p>Loading...</p>
) : (
<React.Fragment>
<pre>{JSON.stringify(todoData)}</pre>
</React.Fragment>
)}
</div>
</React.Fragment>
);
function fetchData(): void {
setTodoData(null);
fetch(` h t t p s://jsonplaceholder.typicode.com/todos/${todoId}`).then(
(res: Response): void => {
res.json().then((tododata: TodoData): void => {
setTodoData(tododata);
});
}
);
}
}
のようになります。「_useEffect()_」 で、更新を監視する変数(この場合「_todoId_」)を指定するだけです。こうすることで、監視している変数の値が更新されると、「_useEffect()_」の処理を実行するようになっています。
この方法の場合、ボタンがクリックされた場合には「_todoId_」を更新するだけで、あとは、値が更新されると、「_useEffect()_」の処理を呼び出して新しい表示データを取得してくれる仕組みになっています。
どちらの方法が良いのか?
今回の例の場合、どちらの方法を利用しても大きな違いはありません。 Vue の公式チュートリアル(英語版)に沿ったやり方を紹介するために二つ目の方法を紹介しています。
今回のように、「_todoId_」が更新される際には、毎回新しいデータを取得するような場合は最初に紹介した、ステートのみで管理する方が、プログラムを作成した以外の人がコードを見た場合にも、必要な処理が分かりやすくなっています。
今回の実装の場合には、殆ど問題にならないと考えられますが、
function buttonClicked(): void {
setTodoData(null);
const id: number = todoId + 1;
setTodoId(id);
fetchData(id);
}
上の関数で「setTodoId(id)」を実行した場合、「_todoId_」のデータが更新されるまでには、時間がかかります。(非同期処理になります)従って、非常に短い間隔でボタンをクリックした場合、「_todoId _」の更新が完了していない場合には、同じデータを2度取得する可能性があります。つまり、本来は、二回クリックされた場合、「_todoId_」は「2」増える事になりますが、「1」しか増えない場合もあり得るという事になります。
今回の例では、見かけ上は、クリックが認識されていないように見えるだけなので、殆どの場合はこうしたケースでも問題になるケースは少ないという事です。
useEffect()を使う場合の注意
二つ目の例では、「_useEffect()_」を使う場合の注意が必要です。 これも、今回の例では大きな問題ではありません。
今回の例では、最初の初期化の場合もデータを取得しているので余り問題ではありません。 当たり前ですが、元々の「_useEffect()_」は、この「部品」が表示できる状態(マウントされた状態)に必要な初期化処理をするための機能です。ところが、この初期化でデータを取得しないような使い方をする場合には、このままでは機能しない事になります。
その場合には、このデータの取得をするかしないかを決めるための仕組みも含めて実装する必要があります。 解決策としては、「_useRef()_」などを利用して、それが、最初のレンダリング(表示)かどうかを管理するような仕組みが必要になります。今回は、特に必要がないので詳しくは別の機会に紹介する事にします。
まとめ
今回の課題では、データの更新によって、別のデータの更新が必要な場合の処理です。 シンプルなのは、最初のデータの更新の際に依存関係のあるデータの更新を行う方法がソースコードも読みやすく便利ですが、データの更新のイベントを検出して、別々に処理することも可能です。
ステートの更新のタイミングをきちんと理解しておく必要があります。
今回の記事では、通常のステートで依存関係のあるデータの処理をまとめて行う方法と、Vue の公式チュートリアル(英語版)のステップ10と似たような方法による実装例を紹介してみました。