この記事は「Concurrent Mode時代のReact設計論」シリーズの5番目の記事です。
シリーズ一覧
- Concurrent Mode時代のReact設計論 (1) Concurrent Modeにおける非同期処理
- Concurrent Mode時代のReact設計論 (2) useTransitionを活用する
- Concurrent Mode時代のReact設計論 (3) SuspenseやuseTransitionが何を解決するか
- Concurrent Mode時代のReact設計論 (4) コンポーネント設計にサスペンドを組み込む
- Concurrent Mode時代のReact設計論 (5) トランジションを軸に設計する
- Concurrent Mode時代のReact設計論 (6) Concurrent Modeと副作用
- Concurrent Mode時代のReact設計論 (7) ステート管理ライブラリの展望(仮)
- Concurrent Mode時代のReact設計論 (8) まとめ(仮)
トランジションを軸に設計する
Concurrent Modeの目玉機能の一つであるuseTransition
については、これまでの記事でも何度も触れてきました。Concurrent Mode時代のアプリ設計において最も厄介なのがこのuseTransition
であると言っても過言ではなく、まさにuseTransition
を制する者がConcurrent Modeを制するといえます。
今回は、useTransition
とは結局何なのか、useTransition
を設計に組み込むときの考え方について議論します。これまではuseTransition
は「startTransition
関数とisPending
の値を得られるフック」と説明していました。機能面で見るとこれは間違いではありませんが、いまいち釈然としませんね。例えばuseState
なら「ステートを定義する」、useEffect
なら「レンダリング後の副作用を定義する」といった、設計の側面から見たフックの効能を理解しなければなりません。
では、useTransition
は何をするためのフックなのでしょうか。筆者の考えでは、それはトランジションを定義することです。では、トランジションとは一体何なのでしょうか。
元来Reactにおいては、アプリのロジックの上では画面の変化はステートの更新という形に抽象化されていました。ここまで見たように、Concurrent Modeでは新たに「変化後の画面をすぐ表示できない(=サスペンドした)場合は前の画面にフィードバックを表示する」という要求を叶えられるようになります。このためにはReact本体によるサポートの一環として「サスペンド中の状態の検知」という機能が必要です。この部分を抽象化した概念がトランジションなのです。
ですから、useTransition
の恩恵を受けるアプリを作るためにはトランジションを組み込んだ設計が必要となります。
トランジションの所有者
一般に、フックの呼び出しというのはコンポーネントに属するものです。例えばuseState
などは最もわかりやすい例で、これはそれが使われたコンポーネントに属するステートを宣言するものです。もちろん、useTransition
も例外ではありません。つまり、トランジションはコンポーネントに属するのです。実際、useTransition
を使うとisPending
という真偽値が得られるのですから、そのisPending
を取り扱うのはuseTransition
を使ったコンポーネントの責任です。
ある意味で、useTransition
はuseState
に近いフックです。それは、isPending
というある種の状態を管理するフックだからです。そうなると、isPending
という状態は誰が管理するのか、言い換えればどのコンポーネントがuseTransition
を呼び出すのかという問題が発生することになります。
末端のコンポーネントがuseTransition
を呼ぶのか、それとも上層のコンポーネントにまとめるのかというのは難しいテーマであり、筆者もまだ結論を出せてはいません。昨今のステート管理ライブラリはアプリのステートをコンポーネント間で分散させるのではなく一箇所のストアにまとめるという戦略を取っていることが多いですが、Concurrent Mode時代のステート管理ライブラリはそれに加えてuseTransition
も一緒に管理してくれるのかもしれません。ただ、複数のトランジションを定義するには複数回useTransition
を呼び出すしかないので、上手に管理するのは難しそうですね。
React公式の考え方
Reactの公式ドキュメントには、「ボタンのコンポーネントがuseTransition
を呼び出す」という例があります。このButton
コンポーネントはuseTransition
を呼び出してstartTransition
関数を取得し、ボタンがクリックされたときに与えられたonClick
をstartTransition
で包んで呼び出します。
// ドキュメント (https://reactjs.org/docs/concurrent-mode-patterns.html) から引用
function Button({ children, onClick }) {
const [startTransition, isPending] = useTransition({
timeoutMs: 10000
});
function handleClick() {
startTransition(() => {
onClick();
});
}
// ...
return (
<>
<button onClick={handleClick} disabled={isPending}>
{children}
</button>
{isPending ? spinner : null}
</>
);
}
Button
に渡されたonClick
によって行われたステート更新によってサスペンドが発生した場合、useTransition
の効果によりステート更新前の状態がレンダリングされます。ただし、その際useTransition
が返すisPending
がtrue
になることから、ボタンはdisabledの状態になります。この状態はサスペンドが解消するまで(サスペンド時に投げられたPromiseが解決するまで)続きます。
話を戻すと、つまりこのButton
コンポーネントはトランジションを内包しており、onClick
で与えられたステート変化を自動的にトランジションに包んで行なってくれるうえに、サスペンド中の表示まで面倒を見てくれるというたいへん便利なコンポーネントだということです。コンポーネントを使う側は、useTransition
のことなど意識せずにこのButton
コンポーネントをボタンとして使うだけでuseTransition
の恩恵を受けることができます。
公式ドキュメントに載っているだけあって、これもuseTransition
のための設計におけるひとつの解です。しかしながら、これはあくまでお手軽な例であって、より複雑なアプリケーションに適用できる設計ではないというのが筆者の考えです。その理由は、この設計は以下に示す2つの問題点を抱えているからです。
問題点1: トランジションとステート更新が分離している
関数startTransition
は、コールバック中でステート更新を行なって初めて意味を持ちます。一方で、Button
コンポーネントはstartTransition
のコールバック中でonClick
を呼び出します。
つまり、Button
コンポーネントは暗黙のうちに「onClick
を呼び出すとステート更新が発生する」ことを要求しています。このことがインターフェースからは見えづらくなっています。言い方を変えれば、startTransition
の呼び出しとその中でのステート更新という、本来一体となって書かれるべき処理がButton
コンポーネントの内側と外側に分かれてしまっています。
ロジックを分割するのは良いこととはいえ、セットで意味を持つものを分断するのは逆にプログラムを分かりにくくするばかりで良くありません。
ただ、startTransition
は中でステート更新を行わなくてもエラーなどが起きるわけではなく、ただ何も起きないだけです。なので、onClick
内でステート更新を行わないのが有害なことかと聞かれればそうでもありません。
問題となるのは例えば「startTransition
の中で何か非同期処理を行なったあとにステート更新を行う」というような場合で、非同期処理を挟んだ時点でstartTransition
の中という扱いではなくなるため、この場合は期待した動作とはならないでしょう。Concurrent Mode時代にそんなやり方で非同期処理を行うべきではないと言われれば返す言葉がありませんが、それはアプリ全体の設計に関わる話であり、こんな末端のutilityコンポーネントがアプリ全体の設計を意識しないと使えないというのはどうにも微妙です。
問題点2: startTransition
の中と外の併用ができない
ステート更新の全てをstartTransition
の中で行うのではなく、中と外の両方でステート更新を行いたいという場合もあります。
外で行われた更新はサスペンドしてもuseTransition
のサポート対象となりませんが、その代わりにステート更新が即座に反映され、サスペンド中のレンダリング(isPending
がtrue
になった状態)にも反映された状態になります。
ですから、ユーザーへのフィードバックを素早く返しつつ非同期処理を行うというユースケースでは、フィードバックを返すために必要なステート更新はstartTransition
の外で行い、非同期処理に相当するステート更新はstartTransition
の中で行う必要があります。
先ほどのButton
コンポーネントではonClick
は問答無用でstartTransition
の中に入れられるため、このようなユースケースに対応できません。
中と外の併用例
「startTransition
の中と外の両方でステートを更新するなんて、そんな面倒なケース本当にあるの?」と思われた読者の方もいるかもしれませんので、ひとつ例をお出しします。
最初の記事で宣伝したアプリでは、穴が空いたTypeScriptプログラムが提示され、ユーザーが全ての穴を埋めると正誤が判定されます。下のスクリーンショットは正しく穴を埋めた状態の表示で、プログラムの背景が緑色になると共に「NEXT」ボタンが表示されます。
正誤の判定はWorker上で動くTypeScriptコンパイラが行い時間がかかるため、非同期処理となります。ユーザーが最後の穴を埋めた瞬間に正誤判定処理を表すFetcher
が作成されます。このFetcher
は当然ステートに入れられますが、非同期処理であるためこのときの更新はstartTransition
の中で行う必要があります。実際、アプリでは正誤判定中に「evaluating...」という表示を行うためにuseTransition
を使用しています。
一方、ユーザーが穴を埋めるという操作を行なった(プログラム中の穴を選択した状態で下の選択肢をクリックした)場合、穴が埋まるというUI上のフィードバックはなるべくすぐに行う必要があります。つまり、ユーザーが正誤判定を待っている間、画面は穴が全部埋まった状態で「evaluating...」と表示されることになります。
そのためには、穴を埋めるためのステート更新はstartTransition
の外で行う必要があります。もし中に入れてしまうと、正誤判定が終わるまで画面に反映されず、ユーザーは選択肢をクリックしたあと画面が無反応のまま待たされることになります。
以上のことから、ユーザーが選択肢をクリックした瞬間のステート更新はstartTransition
の中と外の両方で行う必要がありました。
なお、フィードバックといっても、startTransition
の外で行なったステート更新は当然ながらサスペンド終了後も残り続けます。今回は、ユーザーが埋めた穴は正誤判定完了後も残り続けるのが正しいのでこれで問題ありません。あくまでサスペンド中にのみ一時的に表示したいという場合はisPending
に頼ることになります。
Button
コンポーネントの改善案
では、話をButton
コンポーネントに戻しましょう。このコンポーネントが持つ2つの問題のうち、特に問題点2(startTransition
の中と外を併用したステート更新ができない)が問題であり、Button
コンポーネントの使途を大きく制限しています。
これをどう解決すべきかという話は、結局useTransition
をどう設計に組み込むべきかという話そのままです。ですから、唯一解があるような話ではないのですが、とりあえず一つ案を出してみます。
それは、onClick
にコールバックでstartTransition
を渡すというものです。この方針では、トランジションの所持者をButton
にしたままで前述の2つの問題を解決します。具体的にはこのようなコードになるでしょう。
// ドキュメント (https://reactjs.org/docs/concurrent-mode-patterns.html) から引用
function Button({ children, onClick }) {
const [startTransition, isPending] = useTransition({
timeoutMs: 10000
});
function handleClick() {
onClick(startTransition);
}
// ...
return (
<>
<button onClick={handleClick} disabled={isPending}>
{children}
</button>
{isPending ? spinner : null}
</>
);
}
ポイントはhandleClick
で、中身はonClick(startTransition);
となっています。このことから分かるように、Button
を使う側はこのような使い方をすることになります。
<Button onClick={(startTransition)=> {
startTransition(()=> {
setState(new Fetcher(()=> ...));
});
}}>load something</Button>
こうすることで、前述の「startTransition
とステート更新がButton
の内外に分散している」及び「startTransition
の中と外を併用したステート更新ができない」という2つの問題点が解消されていますね。Button
コンポーネントの役割も「トランジションを内包しており、ボタンクリック時にそのトランジションを用いてステート遷移をすることができる」とより明確なものになりました。
トランジションを持つのがButton
でいい(絶対にButton
の中でしかフィードバックを返さないという強い意志がある)ならこの設計もありでしょう。
一方で、より広範囲にわたってフィードバックを表示したい場合は、その全体をカバーするためにより上流にuseTransition
を配置しなければいけません。
どこにどれだけuseTransition
を置くか
繰り返しますが、useTransition
により宣言されるトランジションはコンポーネントに属するものです。そして、useTransition
がisPending
を返すことから、トランジションを所持するコンポーネントは必然的にそのトランジション内で発生したサスペンドに際してユーザーに適切なフィードバックを返す責任を負います。
ですから、その責任をどのコンポーネントが負うのかを考えてuseTransition
を配置する必要があります。これがトランジションに係るアプリ設計の基本です。もうひとつ、startTransition
関数を入手できるというのもuseTransition
の機能ですが、こちらはコンポーネントツリーの上へも下へも受け渡しがしやすいものです。上に渡すのは先ほど見たButton
の改善案のような感じです。下に渡すほうが簡単なのはいうまでもありませんね。一方で、isPending
を上に受け渡すのは不可能です。ですから、フィードバックをレンダリングするコンポーネントかそれより上にuseTransition
を配置しなければいけません。
トランジションとステート更新はセットで扱われることや、isPending
もステートに似た性質を持つものであることから、useTransition
の配置の方針はステート(useState
やuseReducer
)の配置の方針と似たものになります。ステートを上の方の1箇所で管理する(useReducer
を使う場合などはこれになりがちですね)場合はそこにuseTransition
も配置するのが素直な設計です。一方で、ステートを分散させる方針の場合はuseTransition
も適宜分散させることが考えられます。
また、Reduxなどを使用する場合にありがちな、ステートを一元管理するような設計の場合でも、ちょっとしたフラグなどは末端のコンポーネントが持ってよいという説もあります。トランジションの最中かどうかを表すisPending
もこれに近いところがありますから、必ずしもコンポーネントツリーの上の方に持っていく必要は無いかもしれません。トランジションもまた、ステート管理にまつわる終わりのない議論に組み込まれることになるということです。
また、複数の異なるトランジションを取り扱うためには複数のuseTransition
呼び出しが必要です。というのも、useTransition
から返されるstartTransition
とisPending
は実はペアになっています。すなわち、startTransition
中でサスペンドが発生した場合、同じuseTransition
から返されたisPending
がtrue
になります。
場合によって異なるフィードバックを使い分けたい場合は、トランジションを複数用意して使い分けることになります。このことから分かるのは、トランジションの本質がisPending
を見てフィードバックを表示するところにあるということです。トランジションの設計とは、どのような場合にどのようなフィードバックが発生するかを整理して、それぞれに対してトランジションを定義することなのです。
別の言い方をすれば、我々は同じ種類のステート更新をするときは同じトランジションを用い、違う種類のステート更新をするときは違うトランジションを用いるべきであるということになります。
例えば、Reactアプリの典型的なトランジションの一つはページ遷移です。多くの場合、ページ遷移の待機中はどこからどこへのページ遷移かに依らずに同じようなフィードバックが返されます。例えばGitHubで画面上部にローディングバー的なものが出るようなイメージです。それを実現するためには、どのページ遷移にも同じトランジションを用いるべきです。ここから言えるのは、ページ遷移のトランジションは必然的に、各ページの中でなくより上位の(アプリ全体を統括する)コンポーネントで定義されるということです。
一方で、先ほどのアプリの例にあったような正誤判定を行うというトランジションの場合、それが発生するのは問題表示ページの中なので、ページ内にトランジションを定義することができます(もちろん、場合によってはより上に持っていくこともできるでしょう)。
トランジションとステート更新の直交性
トランジションという概念が優れている点は、ステート更新との間に直交性があることです。つまり、サスペンドを伴うステート更新を行う際に「どのようなステート更新を行うか」と「どのトランジションを用いるか」の間に依存関係が無く、両者をそれぞれ自由に選ぶことができます。どのステート更新をする際にどのトランジションを使っても良いのです。逆に言えば、Concurrent Modeにおいては画面を更新する際には、ステートの更新ロジックに加えてトランジション(より具体的にはstartTransition
関数)を調達しなければならないということです。
この直交性より、「非同期処理中は前の画面に留まる」という処理を「ステート更新(及びSuspenseによるサスペンドのサポート)」と「トランジション」の2つの要素に疎結合な形で分解することに成功しています。
例えば、複数の種類のステート更新に際して、同じトランジションを利用することができます。その典型例はやはりページ遷移の場合であり、どのページからどのページに遷移する場合でも同じトランジションを使うのが普通でしょう。同じトランジションというのは結果としては同じフィードバックエフェクトということになりますが、これを単なる「コードの共通化」ではなく「(実体として)同じトランジション」という一段上の抽象度で表現できることは特筆に値します。APIデザインの妙と言えるでしょう。
設計の具体例
ここまでをまとめると、useTransition
はトランジションを定義するフックであり、アプリの設計に当たってはuseTransition
をコンポーネントツリーの中のどこで呼び出すかをuseState
と同様の観点から考えるべきであるということでした。
前回までに出てきたRoot
とかPageA
とかの例を見直してみましょう。2つ前の記事ではRoot
はこのように定義されていました。
export const Root: FunctionComponent = () => {
const [state, setState] = useState<AppState>({
page: "A"
});
const goToPageB = () => {
setState({
page: "B",
usersFetcher: new Fetcher(() => fetchUsers())
});
};
return (
<Suspense fallback={null}>
<Page state={state} goToPageB={goToPageB} />
</Suspense>
);
};
const Page: FunctionComponent<{
state: AppState;
goToPageB: () => void;
}> = ({ state, goToPageB }) => {
if (state.page === "A") {
return <PageA goToPageB={goToPageB} />;
} else {
return <PageB usersFetcher={state.usersFetcher} />;
}
};
ポイントは、画面Bに遷移するためのステート更新を行う関数goToPageB
をRoot
が定義していることです。今いるページを表すステートをRoot
が持っているのでこれは自然ですね。
次にPageA
はこのようになっていました。
const PageA: FunctionComponent<{
goToPageB: () => void;
}> = ({ goToPageB }) => {
const [startTransition, isPending] = useTransition({
timeoutMs: 10000
});
return (
<p>
<button
disabled={isPending}
onClick={() => {
startTransition(() => {
goToPageB();
});
}}
>
{isPending ? "Loading..." : "Go to PageB"}
</button>
</p>
);
};
PageA
はuseTransition
を呼び出すことでトランジションを定義しています。これは何のトランジションかというと、言わずもがな、画面Aから画面Bへの遷移のためのトランジションです。画面遷移時は、PageA
内で定義したトランジション内でgoToPageB
を呼び出すことで画面Bへの遷移を行います。サスペンド中は画面A内のボタンが「Loading...」という表示になります。
この設計は、この記事の内容に照らせば微妙な設計です。なぜなら、startTransition
とsetState
という、本来セットで扱われなければならない2つの関数がコンポーネント境界によって分断されているからです。
これに対しては2つの方策が考えられます。一つはuseTransition
をRoot
に移すもの、もう一つはuseTransition
をPageA
の中に置いたままにするものです。先ほど述べたように、ページ遷移のためのトランジションであることを考えるとRoot
に移すのが手であるように思えます。その一方で、今回トランジションに反応するのがPageA
内のボタンであるという事情から、useTransition
をPageA
内に残す方向性にも一理あります。
まずは前者から見ていきましょう。
Root
にuseTransition
を移す場合
この場合はRoot
はこんな感じになります。
export const Root: FunctionComponent = () => {
const [startTransition, isPending] = useTransition({
timeoutMs: 10000
});
const [state, setState] = useState<AppState>({
page: "A"
});
const goToPageB = () => {
startTransition(() => {
setState({
page: "B",
usersFetcher: new Fetcher(() => fetchUsers())
});
});
};
return (
<Suspense fallback={null}>
<Page state={state} isPending={isPending} goToPageB={goToPageB} />
</Suspense>
);
};
const Page: FunctionComponent<{
isPending: boolean;
state: AppState;
goToPageB: () => void;
}> = ({ state, isPending, goToPageB }) => {
if (state.page === "A") {
return <PageA isPending={isPending} goToPageB={goToPageB} />;
} else {
return <PageB usersFetcher={state.usersFetcher} />;
}
};
Root
がuseTransition
を呼び出して、得られたstartTransition
はgoToPageB
に組み込まれました。これにより、goToPageB
の中ではstartTransition
とsetStage
がセットで扱われるようになり、設計上の問題点が解消されました。また、サスペンド中のフィードバック描画をPageA
が担う関係で、isPending
はPageA
に渡されています。
この設計ではPageA
の中身が薄くなりますが、一応示しておくとこういう感じです。
const PageA: FunctionComponent<{
isPending: boolean;
goToPageB: () => void;
}> = ({ isPending, goToPageB }) => {
return (
<p>
<button disabled={isPending} onClick={goToPageB}>
{isPending ? "Loading..." : "Go to PageB"}
</button>
</p>
);
};
このように、Root
にuseTransition
を移した場合はロジックがRoot
に偏重する形になります。昨今の風潮からするとこれは悪いことではありません。PageA
にstate
とisPending
を別々に渡しているのが微妙に思えた方もいるかもしれませんが、まあ適当なオブジェクトに詰め込むことも可能でしょう。
また、こうしてみると将来のステート管理ライブラリがuseTransition
も組み込んだ設計になりそうな予感がしてきます。そうすれば、isPending
を自然にステートに混ぜ込んで扱うことができるかもしれません。
PageA
にuseTransition
を残す場合
もう一つ考えられるのはuseTransition
をPageA
に残す場合です。この場合は、先ほどReact公式ドキュメントのButton
コンポーネントの例で議論したのと同じテクニックが使えます。つまり、goToPageB
にstartTransition
を渡すのです。具体的に実装すると、以下のようになります。まずRoot
はこうです。Root
が定義するgoToPageB
はstartTransition
を引数として受け取り、それを使用します。つまり、goToPageB
は「与えられたトランジションの上で画面Bへのページ遷移を行う」という関数になります。
export const Root: FunctionComponent = () => {
const [state, setState] = useState<AppState>({
page: "A"
});
const goToPageB = (startTransition: React.TransitionStartFunction) => {
startTransition(() => {
setState({
page: "B",
usersFetcher: new Fetcher(() => fetchUsers())
});
});
};
return (
<Suspense fallback={null}>
<Page state={state} goToPageB={goToPageB} />
</Suspense>
);
};
const Page: FunctionComponent<{
state: AppState;
goToPageB: (startTransition: React.TransitionStartFunction) => void;
}> = ({ state, goToPageB }) => {
if (state.page === "A") {
return <PageA goToPageB={goToPageB} />;
} else {
return <PageB usersFetcher={state.usersFetcher} />;
}
};
次に、PageA
はこのようになります。
const PageA: FunctionComponent<{
goToPageB: (startTransition: React.TransitionStartFunction) => void;
}> = ({ goToPageB }) => {
const [startTransition, isPending] = useTransition({
timeoutMs: 10000
});
return (
<p>
<button disabled={isPending} onClick={() => goToPageB(startTransition)}>
{isPending ? "Loading..." : "Go to PageB"}
</button>
</p>
);
};
PageA
内でuseTransition
を用いてstartTransition
を取得し、それをgoToPageB
に渡しています。つまり、PageA
がトランジションを定義し、goToPageB
に使ってもらっています。
こちらの設計はisPending
をPageA
が持ってほしい場合に有利です。先ほども述べたように、このような設計にも一定の合理性があります。
筆者は、個人的にはこちらの設計のほうが好みです。その理由は、こちらの方がトランジションとステート更新の直交性を活かす余地があるからです。つまり、goToPageB
は、PageA
が定義したトランジションだけでなく、他のトランジションと組み合わせて使う余地があります。例えば、画面CからBに遷移したいときは画面Cの中で定義されたトランジションを使うということも、同じgoToPageB
を用いて可能になります。
さらに言えば、次のような実装によりgoToPageB
に「デフォルトのトランジション」を用意することすらできるでしょう。
export const Root: FunctionComponent = () => {
const [state, setState] = useState<AppState>({
page: "A"
});
const [startDefaultTransition, isPending] = useTransition();
const goToPageB = (
startTransition: React.TransitionStartFunction = startDefaultTransition
) => {
startTransition(() => {
setState({
page: "B",
usersFetcher: new Fetcher(() => fetchUsers())
});
});
};
return (
<Suspense fallback={null}>
<Page state={state} goToPageB={goToPageB} />
</Suspense>
);
};
こうすることで、goToPageB
はデフォルトではRoot
があらかじめ用意しておいたトランジションを用いるが、goToPageB
を使う側が用意した別のトランジションを使うこともできるというなかなか高性能な関数になります。
まとめ
今回はuseTransition
をどのように使うかについて議論しました。この記事では、そもそもuseTransition
はトランジションを定義するものであると位置付けています。Reactの公式ドキュメントでも、useTransition
はステートの更新をトランジションで包むためのものであるという説明があります。
トランジションはConcurrent Modeの目玉機能であり、サスペンドという部分は比較的分かりやすくて設計に議論の余地がないことも踏まえると、トランジションの設計がアプリの動作設計の軸に添えられがちです。
どのコンポーネントがトランジションを定義すべきかという点については、基本的にはステートと同じ考え方が通用します。ローディング中のフラグ用にステートを定義するのと同じ気持ちでトランジションを定義するのがよいでしょう。
気をつけるべきことは、トランジションとステート更新をむやみに分離しないことです。ただし、その一方でトランジションとステート更新の直交性を活かすことも重要です。筆者お勧めのパターンは、記事中で何度か示したようにstartTransition
を受け渡すパターンです。
次回の記事はアプリ設計に残る最後の謎である「副作用」についての話です。