※ 小説です。続き物ではないですが一応前回はこちら
※ 読むとReactで非同期処理が書きやすくなるかもしれません。
※ Reactの知識が前提になります、ご了承ください。RustやTauriの知識は一切不要です。
本記事で伝えたいこと
「Reactで非同期処理を扱いたいから useEffect 」 → 安直すぎる
データフェッチ系の処理 → クリーンアップを設定するなら useEffect でOK
ユーザ登録のような外部に影響のある処理 → イベントハンドラ内で実行 するべき
useEffect は「特定のイベントによってではなく、 レンダー自体によって引き起こされる副作用 を指定するためのもの」である
という内容が 公式ドキュメントに書いています 。
プロローグ
もう夜も遅いのに、どの教室にも明かりが灯っていた。
TypeScript「ふぅ…やっと落ち着いた…」
実行委員のCSSちゃんから依頼された玄関装飾用のスクリプトを納めた私は、座る場所を求めてあてもなく彷徨っていた。
今学校全体は Qiita Engineer Festa 学園祭 準備期間真っ只中。今週末の本祭に向けて皆が出し物を用意していたのだった。
TS「…隣の教室に行くか…」
もう足が限界に近かった私は、お化け屋敷と化した自学級に見切りをつけ、隣の教室にお邪魔させてもらうことにした。
隣はRustちゃんのクラスだが、人見知りなあの子のことだ、もう家に帰っているであろう…
Rust「…………」
居た。意外なことに、教室にはRustちゃん以外誰も居らず、外の喧騒が嘘のように静まり返っていた。
そういえばRustちゃんのクラスは演劇をするから教室での準備はないんだっけ。
TS「…お邪魔しま~す…」
Rust「…………」
気まずい。パソコンを見つめるRustちゃんに呼びかけても反応はなく、何やら難しそうな ―― 先日シャローコピーの説明をした時のような ―― 不思議そうな顔をしている。
TS「…あの、Rustちゃんは一体何を作っているの…?」
Rust「!!、ああ、TypeScriptさんでしたか。実装に詰まっていて気づきませんでした…」
TS「私のことは”ちゃん”付けで呼んでほしいなぁ。それより、Rustちゃんでも実装で困ることあるんだ?」
Rust「はい、Pythonさんから頼まれたアプリケーションをTauriで作っていたのですが…」
TS「へー、どんなアプリ?」
Rust「ええとですね…」
Python「Rustちゃーーーん!!!!どんな塩梅?」
外の喧騒を引き込むかのように、件の依頼主が慌ただしくやってきた。
Python「おや?君は隣のクラスのTypeScriptちゃんじゃないか」
TS「どうも…隣がアレなもので椅子を借りてます…」
Python「Rustちゃんと友達なのかい?へぇ、この子に私以外の友達が居たとは…」
それはこっちのセリフである。それにRustちゃんに失礼じゃない?…私も同じこと思ったけど…
Rust「ちょうど今TypeScriptちゃんに、Pythonさんから依頼されたアプリについて説明しようとしていたんですよ。」
Python「私のアプリに興味があるのかい?嬉しいなぁ!!」
そう言うとPythonちゃんは雑誌の記事を見せつけてきた。
Python「先日こんな記事を見つけてさ!(ダイマ)」
Python「これを読んでから、どうしても学園祭の出し物で自作の人工知能を展示したくなって、GUI周りをRustちゃんにお願いしていたんだよ1!で、どんな感じ?」
Rust「いや、それが…」
グイグイと来るPythonちゃんに対して、Rustちゃんは恐る恐る画面を見せる。

TS「ええっと、これは、大喜利…?」ウクレレレッスン…
Python「そう!私が独自に開発している大喜利AIさ!メルキオール、バルタザール、カスパーというテイストの違った3人をプロデュースしていてね。与えられたお題に対してちょっと変わった返答をするんだよ。」
TS「なるほど…?メルキオールさん?のはお題に対する回答なんですか?『同じお題が与えられた』と言っていますけど…」
Rust「彼女たちAIはPythonさんのサーバーでずっと稼働していて、今私が作っているアプリは対話用のインターフェースです。なので、おそらくお題を受け取っての、正直な回答ですね。」
そう説明している間、Rustちゃんはまた先程の不思議そうな顔をしていた。
Rust「時間がなかったのでスタブ (ダミーのこと)を使わず直接彼女たちに受け答えしてもらうように書いていたのですが」
そこは頑張ってでもスタブを使うべきでは…という言葉は、先程納品したスクリプトの出来を思い出して飲み込んだ。学園祭準備は佳境で他人のことを言える状況ではないのである。
Rust「そうなると流石に同じお題ばかり出すのは気が引けるので、重複しないようにしていました。そのため、このお題も初めて出しました。」
その手間を考えるとやっぱりスタブを用意したほうが良かったんじゃ…?悩める部分かも。
TS「一度しか出していないお題が二度目だと言われる、という怪奇現象で悩んでいたんだね…なるほど」
不思議そうな顔をしていたわけだ。
Python「メルキオールの気が狂ったとでも言いたいのかい?言っておくけど、彼女たちは大喜利に関してはおそらくこの学校の1年よりはまともな回答をするよ」
微妙な性能だな!…そして大喜利に対するまともな回答って何?
Rust「こんな現象はCLIでテストしていた間も起きなかったので、おそらく私が書いたReactコードがおかしいんだと思います。」
TS「なるほどね…って!わたしTypeScriptで実装する必要があったならTypeScriptわたしに頼んでくれればよかったのに!」
Python「ruby要素の乱用は楽しいかい?」
TS「さっきからちょいちょいメタいこと言ってきますねPythonさん…」
筆者「(ごめんなさい楽しいです)」
非同期関数と useEffect
Rust「TSちゃんに頼もうかなと最初は思ったのですが…忙しそうでしたので…」
TS「あー…」
確かにここ最近、何度もRustちゃんとはすれ違っていたけど、挨拶を交わすのが精一杯だったな…
Rust「画面だけならと思って挑んでみたのですが、気づけばバックエンドわたしの範囲よりもたくさん書いていました…」カチカチッ
VSCodeの画面が映る。ちょうどお題と回答を表示するためのコンポーネントを開いていたらしい。
// import部省略
function OdaiElm({ odai }: { odai: Message }) {
const [melchiorAnswer, setMelchiorAnswer] = useState<Message | undefined>(
undefined
);
const [balthasarAnswer, setBalthasarAnswer] = useState<Message | undefined>(
undefined
);
const [casperAnswer, setCasperAnswer] = useState<Message | undefined>(
undefined
);
useEffect(() => {
(async () => {
const context = [odai];
const melchiorRes = await invoke<Message>("melchior", { context });
setMelchiorAnswer(melchiorRes);
context.push(melchiorRes);
const balthasarRes = await invoke<Message>("balthasar", { context });
setBalthasarAnswer(balthasarRes);
context.push(balthasarRes);
const casperRes = await invoke<Message>("casper", { context });
setCasperAnswer(casperRes);
})();
}, [odai]);
const userElm = <MessageElm text={odai.content} avatar={get_avatar(odai)} />;
const melchiorElm = melchiorAnswer ? (
<MessageElm
text={melchiorAnswer.content}
avatar={get_avatar(melchiorAnswer)}
/>
) : (
<></>
);
// balthasarElm, casperElmはほぼ同じなので省略
return (
<Grid container>
{[userElm, melchiorElm, balthasarElm, casperElm].map((elm, id) => (
<Grid item key={id}>
{elm}
</Grid>
))}
</Grid>
);
}
export default OdaiElm;
import { useState, useEffect } from “react”;
import MessageElm from “@/components/MessageElm”;
import Grid from “@mui/material/Grid/Grid”;
import { Message } from “@/structs”;
import { get_avatar } from “@/avatar”;
import { invoke } from “@tauri-apps/api/tauri”;
function OdaiElm({ odai }: { odai: Message }) {
const [melchiorAnswer, setMelchiorAnswer] = useState(
undefined
);
const [balthasarAnswer, setBalthasarAnswer] = useState(
undefined
);
const [casperAnswer, setCasperAnswer] = useState(
undefined
);
useEffect(() => {
(async () => {
const context = [odai];
const melchiorRes = await invoke(“melchior”, { context });
setMelchiorAnswer(melchiorRes);
context.push(melchiorRes);
const balthasarRes = await invoke(“balthasar”, { context });
setBalthasarAnswer(balthasarRes);
context.push(balthasarRes);
const casperRes = await invoke(“casper”, { context });
setCasperAnswer(casperRes);
})();
}, [odai]);
const userElm = ;
const melchiorElm = melchiorAnswer ? (
) : (
<></>
);
const balthasarElm = balthasarAnswer ? (
) : (
<></>
);
const casperElm = casperAnswer ? (
) : (
<></>
);
return (
{[userElm, melchiorElm, balthasarElm, casperElm].map((elm, id) => (
{elm}
))}
);
}
export default OdaiElm;
Rust「ブログ等を漁り見様見真似で書いた部分です。処理を要約すると
-
- 親要素が持つテキストフィールドからのお題を odai 引数として受け取っている
invoke 関数でAIたちにお題等のコンテキストを渡し、回答を非同期で得ている
AIたちは順番に回答することで内容の重複を防いでいる
得た回答を useState フックの set〇〇 関数でセットしている
といった感じになっています。」
TS「なるほどなるほど…Reactが初めてとは思えない…うーん、なんで useEffect を使ったの?」
Rust「非同期関数をReactで呼び出す方法がわからなくて…とりあえずChatGPTさんに聞いたら『 useEffect 内で呼べば良い』と教えてもらったので、そのように書きました。」
ChatGPT (GPT-4) 「JavaScriptの非同期関数を連続で呼び出す際には、Promiseやasync/awaitを利用することが一般的です。これらは非同期操作を制御し、非同期処理が完了した後に次の処理を実行することを可能にします。
以下に、Reactのコンポーネント内で非同期関数を連続で呼び出す例を示します。この例では async / await を使用します。
jsx
import React, { useEffect } from ‘react’;
const MyComponent = () => {
useEffect(() => {
const asyncFunc1 = async () => {
// 非同期の操作を行う
// 例えばfetch APIを使用してデータを取得するなど
console.log(‘First function’);
};
const asyncFunc2 = async () => {
// 別の非同期の操作を行う
console.log(‘Second function’);
};
const runAsyncFunctions = async () => {
await asyncFunc1(); // 最初の非同期関数を実行
await asyncFunc2(); // その後、次の非同期関数を実行
};
runAsyncFunctions();
}, []); // 空の依存配列を指定して、このエフェクトがコンポーネントのマウント時にのみ実行されるようにします。
return (
);
};
export default MyComponent;
この例では、2つの非同期関数( asyncFunc1 と asyncFunc2 )が定義されています。それらは runAsyncFunctions 内で連続して呼び出され、先に呼び出された関数の処理が終わるのを待ってから次の関数が呼び出されます。これにより、非同期関数が順序良く実行されます。
また、 useEffect フック内でこれらの関数を呼び出しています。これにより、非同期関数はコンポーネントがマウントされた直後に一度だけ実行されます。」
ChatGPTに”さん”付けするRustちゃん可愛いな…じゃなくて!
TS「多分それが原因だね…」
Python「ChatGPTは所詮AI。頼りすぎは禁物ってことだな!」
Pythonあなたがそれ言っちゃう?
TS「ChatGPTに従ったことが悪いんじゃなくて、 useEffect の性質が原因と言いたかったのだけど…」
Rust「 useEffect に渡したクロージャは2回呼び出される、ということでしょうか?」
TS「そう。正確にはReactの StrictMode が有効化されている場合、 純粋性の確認のためにデバッグ時に限りコンポーネントが2回マウントされる 2 んだよね…マウントの過程で useEffect の無名関数も2回実行されちゃう!」ホワイトボードカキカキ…

Rust「各所に console.log を入れてデバッグしてみると、確かに2回呼ばれていますね。」
useEffect(() => {
(async () => {
+ console.log("Process Start");
const context = [odai];
+ console.log(`${context.map((m) => m.content)}`);
const melchiorRes = await invoke<Message>("melchior", { context });
setMelchiorAnswer(melchiorRes);
context.push(melchiorRes);
+ console.log(`${context.map((m) => m.content)}`);
const balthasarRes = await invoke<Message>("balthasar", { context });
setBalthasarAnswer(balthasarRes);
context.push(balthasarRes);
+ console.log(`${context.map((m) => m.content)}`);
const casperRes = await invoke<Message>("casper", { context });
setCasperAnswer(casperRes);
context.push(casperRes);
+ console.log(`${context.map((m) => m.content)}`);
+ console.log("Process End");
})();
}, [odai]);

Python「ここで同じお題が2回与えられたから混乱したわけか…あれ?同じ行の出力が直ちになされていたりで、同じ関数を2回実行しているように見えないのだけど…?」
TS「これは中で実行されているのが非同期関数だからだね!同じ関数が2つ、並行処理されているので、近いタイミングで出力されているんだよ。処理順を描き表してみるとこんな感じ3」カキカキ…

Rust「 console.log の出力結果は読みにくいですが、どう動いているかはわかりましたね。ところで原因となる StrictMode を有効にした記憶はないのですが…テンプレートに最初から書いてあったのかな…」
VSCode タブ > Search欄に StrictMode
Rust「 main.tsx というファイルにて React.StrictMode タグが指定されていますね、これでしょうか?」
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
TS「そうそう。それで囲まれていると StrictMode が有効になって、2回走っちゃう」
Python「じゃあ消してしまえば?」
Rust「それはなんというか…負けた気がします。」
Python「うーん…もう本祭も近いしとりあえず問題なければ良いんじゃない…?」
TS「Pythonさん的にはそれでいいかもしれないけど…」
Rust「 StrictMode みたいな名前の機能を外して後悔しなかった試しがないので…できれば残したいです」
Python「それは確かに…他に解決方法はないかな…?」
Rust「クロージャが2回走ってしまうことさえ分かれば、レンダリングをまたいで状態を保持できる useRef を使うことで invoke 関数の呼び出しは回避できる気がします。」
TS「Rustちゃん短期間でそこまで身につけたの?すごいなぁ…」
Rust「こんな感じでしょうかね。」
+ import { useRef } from "react";
// 省略
function OdaiElm({ odai }: { odai: Message }) {
// 省略
+ const running = useRef(false);
useEffect(() => {
+ if (running.current) {
+ return;
+ }
+
+ running.current = true;
(async () => {
const context = [odai];
const melchiorRes = await invoke<Message>("melchior", { context });
// 省略
setCasperAnswer(casperRes);
+ running.current = false;
})();
}, [odai]);
// 省略
}
TS「 console.log の出力を見た感じ、期待通り無名関数は1回しか実行されていないみたいだね」

Rust「他言語ではセマフォやミューテックスと呼ばれる、排他制御方法を参考にしました…が…」
Python「が?」
…
…
…
TS&Rust「「
_人人人人人人人人人人人人人人人_
> 違う、こうじゃない!!!! <
 ̄Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^ ̄
」」
Python「某ポスターかな4」
TS「なんていうかこれじゃない感が…言語化できないけどこれは正解ではない気がする…!! useRef が必要になるという新たな問題が発生している感が…」
Rust「排他制御が必要な場面なら周辺に関連した記述が見られるはずです。例えばRustわたしの場合だと std::thread::spawn や tokio::spawn でマルチスレッドや並行処理の文脈が見受けられたりしますが、 useEffect に排他制御が必要というのがなんか違うような気がします。」
TS「うーん、useEffect の使い方を履き違えている気がしてきた…こういう時は基本に立ち返って」
Python「公式ドキュメントの確認だね!」
TS「私のセリフ持って行かないで!><」
コンポーネントよ純粋であれ
Rust「先程TSちゃんは『純粋性の確認のために2度マウントされる』と言っていた気がするのですが、そこを詳しく知りたいです」
TS「ええと、純粋について書いているドキュメントは…これかな」
TS「始めの方は純粋関数に関する記述だけど…Rustちゃんは純粋関数は知ってる…?」
Rust「可変なグローバル変数を見ると蕁麻疹が出る 5ぐらいには存じています。」
TS「そかそか、ということはこのドキュメントの最初の方もすでに理解してそうだね。」
Rust「そうですね。コンポーネントは純粋関数にする、つまり『同じ引数が与えられたら副作用を引き起こさずに常に同じ結果を返す』ようにする。このことを意識して作っていくものだと思っていました。」
純粋関数とは
同じ引数に対して、同じ返り値を返す
副作用を引き起こさない
という、「参照透過性」を満たす関数のこと
Python「副作用…?グローバル変数恐怖症…?グローバル変数の使用にどんな問題があるんだ…?」
TS&Rust「あー…(察し)」
Rust「…ちょっと宗教的にグローバル変数は書きたくないのでTSちゃん頼めますか6?」
TS「りょーかい。Python書けるか自信ない7けど…えーっと…」カタカタ…
Python「なんか気を遣われてる…?!わざわざ合わせてくれなくても…」
sum: int = 0 # グローバル変数
def add_to_sum(i: int) -> int:
global sum
sum += i
return sum
TS「例えばこの関数について、 add_to_sum(5) は何を返してくるでしょうか…?」
Python「5じゃない?」
TS「さっきの add_to_sum(5) 実行後の add_to_sum(10) は?」
Python「15だね」
TS「色々な引数で何回も実行した後の add_to_sum(5) は…?」
Python「… sum の中身次第だから 即答はできない かな…」
TS「そう… sum という 外環境の変数に依存 するから 即答できない というのが大切…一方で」カタカタ…
def add(a: int, b: int) -> int:
return a + b
TS「この関数は純粋関数なんだけども、 add(1, 1) は?」
Python「 1+1 は?みたいな質問ですよねそれ…2」
TS「 add(4294967296, 8589934592) は?」
Python「(数クロックだけ考えて)…12884901888」
TS「 add(2, 3) は?」
Python「5」
TS「 add(4294967296, 8589934592) は?」
Python「12884901888」
TS「なんで即答できたんですか?」
Python「いやだってさっきも聞いてきたじゃん()」
TS「さっきの sum みたいに外部の状況で変わったりしない…?」
Python「内部の処理は 引数だけで決まる 関数みたいだから、引数を教えてもらえれば同じ答えを返して良いでしょう。」
TS「そう、これが純粋関数にするメリットで、 一度計算したらどこかに記憶キャッシュしておけば、再計算が不要 になるんだよ!」
Python「あっ!競プロで使われるメモ化の考えか!」
TS「Reactではレンダリング計算回数を減らすために、メモ化の考えを多用するんだよね。関連するフックもたくさんあって、だから純粋関数ばかり書くので(ライブラリ使用者が)可変なグローバル変数を使う場面はない 感じ」
Python「不変な、定数としての利用はランタイム時での純粋性が保てるから大丈夫そう…ところでさっき言っていた副作用という単語について説明に出てこなかったような…?」
TS「そうだね、ここからは可変なグローバル変数を使うデメリット、副作用の話になります…」カタカタ…
sum: int = 0 # グローバル変数
def add_and_save_to_sum(a: int, b: int) -> int:
res = a + b
### 副作用
global sum
sum += res
###
return res
TS「この関数でさっきの add_and_save_to_sum(4294967296, 8589934592) みたいな問答をしたとして、どうなるかな…?」
Python「引数だけで返り値が決まるから、同じくメモ化できそう…?」
TS「じゃあ、同時に sum の値がどうなるか聞いたら…?」
Python「問答を始める前に尋ねられることを知らないとキツい…あ、メモした値を返すだけじゃなくて sum の計算も一緒にしなきゃいけない…?!」
TS「そう、つまり、 単純にキャッシュした値に置き換えることはできない という弊害が出ちゃう!これはこの関数が単に計算して値を求めるだけじゃなくて 関数の実行が外部に影響を与えている からだよね。この影響が『副作用』!」
Python「さっきの問答でsumの値を求める必要があったみたいに、副作用がある関数だと実行することに意味ができちゃうから、実行したことにして計算済みの値に置き換えるってわけにいかないのか…」
TS「そう!Reactのレンダリングだと、副作用で全体に影響がある場合スキップするわけにいかなくなるから、とても困るのです() ここまでをまとめるとこんな感じかな」カキカキ…
改めて
純粋関数とは 計算結果をキャッシュした値で置き換えても問題がない 関数であり、そのためには
同じ引数に対して、同じ返り値を返す
副作用を引き起こさない
言い換えると、外部への影響を引き起こさない
という、「参照透過性」を満たしている必要がある
TS「補足しておくと、今回の説明では可変なグローバル変数が目の敵にされていたけど、グローバル変数以外にも関数を不純にする方法は色々あるよ!詳細は割愛」
Rust「話をReactに戻しますが、TSちゃんは StrictMode は『純粋性の確認のために』マウントを2回行うみたいなことを先程言っていましたよね?」
TS「言った言った。これもこのページに書いてあるね。」
- StrictMode で純粋でない計算を検出
TS「純粋関数はキャッシュした値を返してもよい。返しているはず。それならば何度呼び出しても問題ない。なのでデバッグ時はとりあえず2回呼び出して表示が変になっていないかを確認している…という考えみたい!」カキタシ…

Rust「TSちゃんとPythonさんが先程していた add(4294967296, 8589934592) の問答に似たことをデバッグ時に行って、副作用の影響で変な現象が起きたりしないか確認していたわけですね。なるほど」
Rust「(ところで、 純粋関数ではない useEffect のクロージャも2回呼ばれている のが何故なのか気になる…うーん?なんで?)」
↑ Rustちゃんは後述のある勘違いをしています。useEffectも2回実行される理由は後述のこちらやこちら
Python「なるほどね…純粋関数や副作用がどういうものかはわかったよ、ありがとう…そして二人共なーんか上からだったけど…」
TS「それはその説明のためで、ごめんなさい…」
Python「いや、TSちゃんはいいんだ、Rustちゃんさ。立派にうちのAIたちに対して副作用を起こしているよね…?お題を出すって形でさ!」
Rust「!!!」
依頼したのPythonちゃんなのにその指摘は…合っているけど理不尽では()
Python「それに宗教的に書けないとかどうとか言ってたけど、さっき挙げていた解決方法の useRef って思いっきり可変なグローバル変数8みたいな感じじゃない?」
Rust「ゴフッ!」
TS「Rustちゃん!!大丈夫?」
Rust「…意識しないようにしていたんですよ…!それにそう思ったからこそあの解決方法は間違っているって言いましたし…一応RustわたしにもRc<RefCell>やArc<Mutex>を使った代替方法がありますし、TSちゃんにそこスレッドセーフまで求めていませんでしたし…ゴニョゴニョ…」
Rust「それに!入出力を伴うプログラムで副作用を全く起こさないのは不可能です!」
今日のRustちゃん表情豊か…疲れているのかな
Rust「だから副作用を書くために・・・・・・・・・ useEffect フックを使っていました。この書き方なら問題ないはずです。」
TS「それなんだけどね、公式ドキュメントの後半曰く、多分それが今回の問題の原因というか争点だね…」
Rust「え…?」
useEffect は「どんな副作用を起こしてもいい場所」というわけではない
- 副作用を引き起こせる場所
TS「このセクションを要約するとこんな感じだね…
-
- Rustちゃんが言った通り、副作用を起こさないプログラムの方が珍しい
-
- でも副作用が起きるのは普通 レンダリングとは無縁なイベントハンドラ内のはず
イベントハンドラは純粋である必要はない
どうしても レンダー後に 副作用を起こしたい場合 useEffect を使えば可能だけど 最終手段である
」
Python「Rustちゃんはいきなり最終手段を取っていたわけか…」
Rust「てっきり名前から『副作用を起こしていい場所』という意味かと思っていました…うーん?ちょっとまだ『レンダリングとは無縁』の辺りがよくわからないです。」
TS「 useEffect の関連するドキュメントを読んでみた方が良さそう…これかな」
TS「また純粋・不純に関係した記述が最初の方に書いている9ね…えーっと…
Reactには2つのロジックがある。
レンダーコード: コンポーネントのレンダリングに使う。さっきメモ化の話をしたように、単純な計算を行うだけで、純粋じゃなきゃならない
イベントハンドラ: 計算だけではなく何かを実行するものであり、特定のユーザアクション(クリックやフォーム送信)を起因とする副作用 が含まれる
」
Rust「イベントハンドラはレンダリングと切り分けて考えてしまって良い…?…!そうか! イベントハンドラ内に副作用をまとめる 必要があったんだ!…そもそも function OdaiElm({ odai }: { odai: Message }) の引数と返り値が1対1対応していないのがおかしくて…あ、イベントハンドラなら非同期関数も書ける…!修正方法わかりました、ありがとうございます…!」カタカタカタカタカタ…
TS「コンポーネントを分割する時に関連する処理を書く場所って迷うよね…その辺の混乱が原因だったのかな?ともかく解決方法わかったみたいで良かった!」
Python「でも副作用を書くのがイベントハンドラで事足りるなら、 useEffect ってなんのためにあるんだ ?」
TS「確かに…このドキュメントに続きが書いてるね…」
Rust「(ピタッ…カタカタカタ…)」
Rustちゃんも気になるようだけど、「聞きながら修正する」つもりなのか再びタイピングを開始した。
前から思ってたけどRustちゃんって結構…いやかなり横着をするよね…彼女の信条パラダイムとのギャップが激しい…
TS「ドキュメントの続き
しかしこの2つのロジックでは足りないときがある。
例えばチャットルームアプリを作っていて ChatRoom コンポーネントの表示中に接続するとする。 チャットルームへの接続は副作用 があるけど、クリックのような トリガとなるイベントがあるわけではない 。
」
TS「?…接続する時にボタンを押させるだろうし少し無理がある例じゃない…?まぁいいや続き」
TS「
エフェクトは、特定のイベントによってではなく、レンダー自体によって引き起こされる副作用を指定するためのものである
サーバーへの接続はこのエフェクトによって行われる
…!」
TS「なるほど…扱える処理は、 レンダリング自体がトリガとなるもの って思えばいいのかな…?」
Python「特別な場合にしか使わないということ…?」
TS「ドキュメントのところどころに書かれている『 外部システムとの同期 』がその特別な場合みたい。」
Python「メルキオールたちとの会話は多分『フォーム送信』が近いだろうし、『同期』ではないから確かに useEffect を使うのは間違いだろうね」
TS「『外部システムとの同期』で他にわかりやすい例が下の方に載ってそう…なになに…?」
本題ではないので「エフェクトの書き方」という項目の最初の方を飛ばしました。エフェクトの依存値関係の話題は次の記事様が詳しいです。
【React】useEffect の標準動作は「依存配列の中身が変わると実行」ではない – Qiita
「レンダリング後、常に実行が基本」
「配列が指定されている場合、再レンダリング時に依存値配列に含まれるすべての値に変化がなければ実行スキップ。だから(結局?)、空配列だと( ![].every((a, b) => a == b) は 常に false なので)マウント時のみ実行され以降はすべてスキップ」
と考えると良い感じらしいです。
useEffect を使う場面
外部システムとの同期例① 打ち消せる場合
レンダリング時に実行され外部に副作用を与えるケース
⇒ その副作用を クリーンアップで打ち消せる なら useEffect で記述して良い
TS「チャットルームの例から見ていきますか」
import { useEffect } from 'react';
import { createConnection } from './chat.js';
export default function ChatRoom() {
useEffect(() => {
const connection = createConnection();
connection.connect();
}, []);
return <h1>Welcome to the chat!</h1>;
}
TS「useEffectの第二引数が空配列なので、『 ChatRoom がマウントされた』⇒『チャットルームに繋がった』という形でマウントをトリガにしたいケースなんだね…ドキュメントの続きは…」
このエフェクトはマウント時にのみ実行されるため、コンソールに “✅ Connecting…” が 1 回だけ表示されると思うかもしれません。しかし、コンソールを確認すると、”✅ Connecting…” が 2 回表示されているはずです。なぜこれが起こるのでしょうか?
… (中略。外部への副作用の弊害が記載) …
このようなバグは、手動での徹底的なテストがないと見逃しやすいものです。これらをすばやく見つけるために、開発環境では React は、初回マウント直後にすべてのコンポーネントを一度だけ再マウントします。
Python「 思いっきりRustちゃんが引っかかっていた StrictMode の挙動の説明だ! 」
“✅ Connecting…” のログが 2 回表示されることで、実際の問題に気付くことができます。つまり、コンポーネントがアンマウントされたときに接続を閉じるコードがないということです。
この問題を解決するには、エフェクトからクリーンアップ関数を返すようにします。
useEffect(() => {
const connection = createConnection();
connection.connect();
return () => {
connection.disconnect();
};
}, []);
TS「『クリーンアップ』として useEffect の返り値に設定した無名関数は、(スキップされなかった)再レンダリング時と最後のアンマウント時に実行されるので、この例だとアンマウント時にのみ実行されるね。 StrictMode の影響で2回チャットルームに入るけど、 最初の接続はすぐ退出されるので、問題ない !」カキカキ…

Python「接続という副作用があっても、然るべきタイミングで 打ち消せている から問題ないということか…」
Rust「今完全に理解しました… useEffect のクロージャが2回実行されるのは、 副作用が起きないはずのレンダリング をトリガにしたクロージャについて、副作用がどうしても生じる場合に 適切に対処できているか を調べるためだったんですね…」
手を止めたRustちゃんは、心底納得した ―― 悟りでも開いたかのような ―― 顔をして言った。
Python「そして大喜利AIの場合はもちろん対処できていなくて、それをメルキオールが教えてくれていたんだな。」
TS「『覆水盆に返らず』な、useEffect を使ってはいけない副作用の例も下の方に載っているけど、もう一個使って問題ない例を見てみますか」
外部システムとの同期例② そもそも副作用がないと言える場合
レンダリングをトリガとして実行するが、 データフェッチ等 の外部に影響を与えないケース
⇒ そのままでも問題ない。なるべくクリーンアップに処理の中止や取得したデータの破棄を入れた方が良い
TS「次に問題ないのはデータフェッチの例。要はレンダリングをトリガとして実行されるけど”ほぼ”副作用がない場合は、対処する必要もそこまでないので問題なさそうという感じ」
useEffect(() => {
let ignore = false;
async function startFetching() {
const json = await fetchTodos(userId);
if (!ignore) {
setTodos(json);
}
}
startFetching();
return () => {
ignore = true;
};
}, [userId]);
TS「フロントエンドで非同期と言えば大体データフェッチだろう10し、多分ChatGPTが思い描いていた非同期処理はこれなんじゃないかな…?」
Rust「 非同期関数かどうかで useEffect を使うか決めるのではなくて、どんな副作用があるかで決めるべき だったんですね…混乱の根源がわかりました」
Python「悩んでいる問題と解決方法の着眼点が異なると、なかなかググれなくて困るよね…ChatGPTに聞いても今回みたいなことになるし」
TS「今回の例をもう少し見てみるよ。アンマウント後や userId が変わってもうデータが要らなくなったのに set〇〇 が呼ばれるのを防ぐために、クリーンアップを利用してデータの破棄を行っているみたい。データを取得できるタイミングとレンダリングのタイミングはズレているだろうから、不整合を防ぐためにもあった方が良さそうだね。」
useEffect を使ってはいけない例「商品購入」
「いつ起きるか、何回起きるかがわからないレンダリング」をトリガとしてはいけない処理を、
誤って useEffect 内で実行してはいけない
書こうとしている処理が POST クエリ等なら立ち止まって!
TS「ここまででもうわかると思うけど、クリーンアップを設定してもどうしようもならない処理、例えば商品購入やユーザー登録、」
Python「そしてAIにデータ送信!だよね?」
Rust「うっ…」
TS「Rustちゃんをいじめないで…ともかく、そういった 『覆水盆に返らず』な処理 は useEffect に指定しちゃだめってことだね!」
商品購入の例
useEffect(() => {
// ? Wrong: This Effect fires twice in development, exposing a problem in the code.
fetch('/api/buy', { method: 'POST' });
}, []);
TS「こういう処理は イベントハンドラに書け って修正例が出てる」
function handleClick() {
// ✅ Buying is an event because it is caused by a particular interaction.
fetch('/api/buy', { method: 'POST' });
}
TS「そして、あ…」
これで分かるのは、再マウントでアプリケーションのロジックが壊れるなら、通常それは既存のバグが明らかになったのだということです。ユーザの視点から見ると、ページを訪れることと、ページを訪れてリンクをクリックして別のページに行ってから戻るボタンを押すこととの間に、違いがあってはいけません。React は、開発環境でコンポーネントを 1 度再マウントすることで、この原則に従っていることを確認します。
Rust「 useEffectのクロージャはバグ発見のために2回走る って、ここにはっきり書いていたんですね…辿り付けた気持ちです」
TS「感無量だけど…useRefによる回避は言わば例外握り潰しだったわけだね…」
改めて元のコード依頼品を見てみよう
Rust「とりあえず急ぎで修正してみました!」
TS「早!どう変わったんだろう…?元々はこんな感じだったね」
// import部省略
function OdaiElm({ odai }: { odai: Message }) {
const [melchiorAnswer, setMelchiorAnswer] = useState<Message | undefined>(
undefined
);
const [balthasarAnswer, setBalthasarAnswer] = useState<Message | undefined>(
undefined
);
const [casperAnswer, setCasperAnswer] = useState<Message | undefined>(
undefined
);
useEffect(() => {
(async () => {
const context = [odai];
const melchiorRes = await invoke<Message>("melchior", { context });
setMelchiorAnswer(melchiorRes);
context.push(melchiorRes);
const balthasarRes = await invoke<Message>("balthasar", { context });
setBalthasarAnswer(balthasarRes);
context.push(balthasarRes);
const casperRes = await invoke<Message>("casper", { context });
setCasperAnswer(casperRes);
})();
}, [odai]);
const userElm = <MessageElm text={odai.content} avatar={get_avatar(odai)} />;
const melchiorElm = melchiorAnswer ? (
<MessageElm
text={melchiorAnswer.content}
avatar={get_avatar(melchiorAnswer)}
/>
) : (
<></>
);
// balthasarElm, casperElmはほぼ同じなので省略
return (
<Grid container>
{[userElm, melchiorElm, balthasarElm, casperElm].map((elm, id) => (
<Grid item key={id}>
{elm}
</Grid>
))}
</Grid>
);
}
export default OdaiElm;
import { useState, useEffect } from “react”;
import MessageElm from “@/components/MessageElm”;
import Grid from “@mui/material/Grid/Grid”;
import { Message } from “@/structs”;
import { get_avatar } from “@/avatar”;
import { invoke } from “@tauri-apps/api/tauri”;
function OdaiElm({ odai }: { odai: Message }) {
const [melchiorAnswer, setMelchiorAnswer] = useState(
undefined
);
const [balthasarAnswer, setBalthasarAnswer] = useState(
undefined
);
const [casperAnswer, setCasperAnswer] = useState(
undefined
);
useEffect(() => {
(async () => {
const context = [odai];
const melchiorRes = await invoke(“melchior”, { context });
setMelchiorAnswer(melchiorRes);
context.push(melchiorRes);
const balthasarRes = await invoke(“balthasar”, { context });
setBalthasarAnswer(balthasarRes);
context.push(balthasarRes);
const casperRes = await invoke(“casper”, { context });
setCasperAnswer(casperRes);
})();
}, [odai]);
const userElm = ;
const melchiorElm = melchiorAnswer ? (
) : (
<></>
);
const balthasarElm = balthasarAnswer ? (
) : (
<></>
);
const casperElm = casperAnswer ? (
) : (
<></>
);
return (
{[userElm, melchiorElm, balthasarElm, casperElm].map((elm, id) => (
{elm}
))}
);
}
export default OdaiElm;
Python「本来はお題確定のフォーム送信時にイベントハンドラとして動かすべき処理が、useEffect で指定されていたのが問題だったよね」
Rust「はい。OdaiElm.tsx に関しては、 useEffect 等のフックの使用をやめ、表示に集中したシンプルな純粋関数になるように努めました!」
// import部省略
interface OdaiElmProps {
odai: Message;
melchiorAnswer: Message | undefined;
balthasarAnswer: Message | undefined;
casperAnswer: Message | undefined;
}
function OdaiElm(props: OdaiElmProps) {
const { odai, melchiorAnswer, balthasarAnswer, casperAnswer } = props;
const userElm = <MessageElm text={odai.content} avatar={get_avatar(odai)} />;
const melchiorElm = melchiorAnswer ? (
<MessageElm
text={melchiorAnswer.content}
avatar={get_avatar(melchiorAnswer)}
/>
) : (
<></>
);
// 省略
return (
<Grid container>
{[userElm, melchiorElm, balthasarElm, casperElm].map((elm, id) => (
<Grid item key={id}>
{elm}
</Grid>
))}
</Grid>
);
}
export default OdaiElm;
import MessageElm from “@/components/MessageElm”;
import Grid from “@mui/material/Grid/Grid”;
import { Message } from “@/structs”;
import { get_avatar } from “@/avatar”;
interface OdaiElmProps {
odai: Message;
melchiorAnswer: Message | undefined;
balthasarAnswer: Message | undefined;
casperAnswer: Message | undefined;
}
function OdaiElm(props: OdaiElmProps) {
const { odai, melchiorAnswer, balthasarAnswer, casperAnswer } = props;
const userElm = ;
const melchiorElm = melchiorAnswer ? (
) : (
<></>
);
const balthasarElm = balthasarAnswer ? (
) : (
<></>
);
const casperElm = casperAnswer ? (
) : (
<></>
);
return (
{[userElm, melchiorElm, balthasarElm, casperElm].map((elm, id) => (
{elm}
))}
);
}
export default OdaiElm;
TS「とてもスッキリしたね!」
Rust「その代わり急ぎで修正したのでという言い訳をさせてもらいつつ…親要素の App.tsx がカオスなことになっています…」
TS「あるあるだね…後で分割しようか」
Rust「コールバックのsend非同期関数を定義して、その中で invoke 等を呼んでいます。外部に useState フックの set〇〇 関数以外に依存する変数がなかったので、 useCallback を使い、レンダリングごとにコールバックが作成されるのを防いでみました。」
// import部省略
function App() {
// 省略
const [odai, setOdai] = useState<Message | undefined>(undefined);
const [melchiorAnswer, setMelchiorAnswer] = useState<Message | undefined>(
undefined
);
const [balthasarAnswer, setBalthasarAnswer] = useState<Message | undefined>(
undefined
);
const [casperAnswer, setCasperAnswer] = useState<Message | undefined>(
undefined
);
const send = useCallback(async (odai_str: string) => {
console.log("Process Start");
const odai = {
character: "user",
content: odai_str,
};
setOdai(odai);
const context = [odai];
console.log(`${context.map((m) => m.content)}`);
const melchiorRes = await invoke<Message>("melchior", { context });
setMelchiorAnswer(melchiorRes);
context.push(melchiorRes);
console.log(`${context.map((m) => m.content)}`);
const balthasarRes = await invoke<Message>("balthasar", { context });
setBalthasarAnswer(balthasarRes);
context.push(balthasarRes);
console.log(`${context.map((m) => m.content)}`);
const casperRes = await invoke<Message>("casper", { context });
setCasperAnswer(casperRes);
context.push(casperRes);
console.log(`${context.map((m) => m.content)}`);
console.log("Process End");
}, []);
return (
<ThemeProvider theme={theme}>
<br />
{odai ? (
<OdaiElm
odai={odai}
melchiorAnswer={melchiorAnswer}
balthasarAnswer={balthasarAnswer}
casperAnswer={casperAnswer}
/>
) : (
<></>
)}
<Grid container>
<Grid item>
<PromptFieldElm send={send} />
</Grid>
</Grid>
</ThemeProvider>
);
}
export default App;
import { useState, useCallback } from “react”;
import PromptFieldElm from “@/components/PromptFieldElm”;
import useMediaQuery from “@mui/material/useMediaQuery”;
import { useMemo } from “react”;
import { createTheme, ThemeProvider } from “@mui/material/styles”;
import OdaiElm from “@/components/OdaiElm”;
import Grid from “@mui/material/Grid/Grid”;
import { Message } from “@/structs”;
import { invoke } from “@tauri-apps/api/tauri”;
function App() {
// https://amateur-engineer.com/react-mui-dark-mode/
const prefersDarkMode = useMediaQuery(“(prefers-color-scheme: dark)”);
const theme = useMemo(
() =>
createTheme({
palette: {
mode: prefersDarkMode ? “dark” : “light”,
},
}),
[prefersDarkMode]
);
const [odai, setOdai] = useState(undefined);
const [melchiorAnswer, setMelchiorAnswer] = useState(
undefined
);
const [balthasarAnswer, setBalthasarAnswer] = useState(
undefined
);
const [casperAnswer, setCasperAnswer] = useState(
undefined
);
const send = useCallback(async (odai_str: string) => {
console.log(“Process Start”);
const odai = {
character: “user”,
content: odai_str,
};
setOdai(odai);
const context = [odai];
console.log(`${context.map((m) => m.content)}`);
const melchiorRes = await invoke(“melchior”, { context });
setMelchiorAnswer(melchiorRes);
context.push(melchiorRes);
console.log(`${context.map((m) => m.content)}`);
const balthasarRes = await invoke(“balthasar”, { context });
setBalthasarAnswer(balthasarRes);
context.push(balthasarRes);
console.log(`${context.map((m) => m.content)}`);
const casperRes = await invoke(“casper”, { context });
setCasperAnswer(casperRes);
context.push(casperRes);
console.log(`${context.map((m) => m.content)}`);
console.log(“Process End”);
}, []);
return (
{odai ? (
) : (
<></>
)}
);
}
export default App;
Python「ぶっちゃけ OdaiElm.tsx に書いていた処理をそのまま持ってきた感じだな…」
Rust「コードの整理は追々…とりあえず動くものができましたし、実行してみた感じ2回呼ばれているみたいなことはなさそうです!」

エピローグ
TS「コールバックの名前が send なのが改善点かも…?このコールバック関数って送信だけじゃなくて結果の受け取りもしちゃっているよね。」
Rust「元のコードは odai 変数を確定させているだけでシンプルだった部分ですね…」
TS「全部の回答を受け取るまで実行される…うーん、非同期関数の実行時間は短くしたいかも」
Rust「ちょうど今バックエンドわたし側でお題と回答をキャッシュできるように作り替えていたところです!だから出題をIDで管理すれば…」
TS「お題とIDをバックエンドRustちゃん側に投げてさっさと return してしまって、フロントエンドわたし側は何か他のタイミングで回答を取得できればいいね!」
Rust「Tauri APIにlistenという、バックエンドこっち側から投げたイベントを受け取れるコールバック設定関数があるので、この関数と返り値で渡される unlisten クリーンアップ関数を使えば…」
TS「どこかのコンポーネントの useEffect でイベントをリッスンするようにして解決だ…!」
Python「(議論が白熱してるな…!…私もラボでAIの改良をしよう)」
…
…
Rust「その…TypeScriptちゃん…」
TS「ん?」
Rust「…フロントエンドの方、実装の続きをお願いしたくて…良いでしょうか…?」
TS「もちろん!一緒にやろ!」
そして本祭まで、私達の戦いは続くのだった…
~ fin ~
あとがき
えー、最初に謝ります…
ごめんなさい!!!!
なぜ謝ったかというと、小説スタイルではなくまともな技術記事ベースの方が多分読みやすかったからです!!
しかし前回同様、Tauriまとめ記事の「TypeScriptとRustがペアプロする物語が書きたい!」という思いをまたまたこらえきれず、小説スタイルで書いてしまいました…
↓ ヨカッタラヨンデミテホシイデス
なぜこらえきれなかったかというと、前回はたしかにペアプロはさせられたのですが、結局「Tauriを」題材にできたわけではなく、それでモヤモヤしていたからです…不完全燃焼だったんですね。
今回の記事を書いた技術的経緯の方は(自分の中では)結構しっかりしていると信じたいです。直感ですが、「Tauriみたいに、Reactで非同期で変なことをする 状況はそんなになくて、大体はデータフェッチとかばかり」なんじゃないかなと思います。そこで「あんまり情報が落ちていないのではないか」という動機から書きました。筋は通っているはず()
Tauriを使えば例えばIoT用のリモコンを作ったり、OSが提供するAPIと連携させてみたり、何したりと、Reactの域を超えて色々なことができます。そのため、今回のような非同期に関する落とし穴についての記事はきっと需要があるのではないかと思います。Tauriが気になった方は是非やってみてほしいです!
筆者は、フロントエンド(特にReact)はまだまだにわかな部分がありますので、間違い等ございましたら是非コメント等していただけると幸いです。
ここまで読んでいただき誠にありがとうございました!
参考
参照透過性 – Wikipedia
他の資料も色々見たのですが冒頭で「その式をその式の値に置き換えてもプログラムの振る舞いが変わらない」という記述ではっきり目的を示してくれていたのが良かったです。
実行環境
-
- tauri: 1.4
-
- cargo: 1.69.0
-
- yarn: 1.22.19
- tauri-cli: 1.4.0
> rustup –version
rustup 1.26.0 (5af9b9484 2023-04-05)
info: This is the version for the rustup toolchain manager, not the rustc compiler.
info: The currently active `rustc` version is `rustc 1.69.0 (84c898d65 2023-04-16)`
> cargo –version
cargo 1.69.0 (6e9a83356 2023-04-12)
> node -v
v18.4.0
> npm -v
8.13.1
> yarn -v
1.22.19
> cd tauri-project
> yarn tauri –version
yarn run v1.22.19
$ tauri –version
tauri-cli 1.4.0
Done in 0.15s.
わかりやすさのためにQiita記事を載せさせていただきましたが、一応、公式ドキュメントへのリンクも載せます。: https://react.dev/reference/react/StrictMode ↩
JS/TSの(ブラウザにおける)プロセスはシングルスレッドな上に、本来はPromiseのコールバックとして呼ばれている感じですので、誤解を与えてしまう図な可能性がありますが、あくまでも処理順番の直感的な図と捉えていただければと思います。一応補足しておきました。 ↩
ちなみに元ネタのほうは「違う、そうじゃない」 ↩
データ競合を防ぐ目的と、この純粋関数の話は少し文脈が違います。が、Rustちゃんはこの仕様について理解する過程で純粋関数についても理解したのだと思ってください。 ↩
Rust「Rustわたしの場合でも &mut 等を使えば副作用のある関数の説明はできたりしますが、ややこしくなるので今回は黙っています!」 ↩
Pythonには組み込みで sum 関数がありますが記事執筆時筆者もその存在を忘れて変数として使ってしまっていました…TSちゃんは sum 関数を知らなかったという設定でお願いします(あまり良くないだけで直すほどのものではない) ↩
スコープや挙動等を考慮するとどちらかと言うとstatic変数の方が合っていますが、尺の都合上説明を省略しています。 ↩
ドキュメント由来の部分を示すため引用風に書いていますが、内容を要約したものになります。だからTSちゃんの口調のままです。 ↩
筆者は経験が浅いので嘘かもです!!!要出典です!!! ↩