Webフロントエンドにおける副作用について考えるZennmaroKanatani💊Webフロントエンドにおける副作用について考える2022/01/10に公開2022/02/27JavaScriptTypeScriptfrontendfrontendtech個人的にHaskellを学び始めてから、世の中のソフトウェアは不純なもの(副作用)で溢れていることに気づいたので、Webフロントエンドに当てはめて考えたことをまとめてみます。ReactやVue等でのSPAを想定して執筆しています。(が、副作用の考え方自体は抽象度の高い概念なので、バックエンドやモバイルアプリ等についても応用できるところはあるかもしれません。)副作用とはWikipediaには以下のようにあります。effect)とがある。式は、評価値を得ること(※関数では「引数を受け取り値を返す」と表現する)が主たる作用とされ、それ以外のコンピュータの論理的状態(ローカル環境以外の状態変数の値)を変化させる作用を副作用という。この説明だけでは理解が難しそうな気がしますが、キーワードは太線にしているローカル環境以外の状態変数の値を変化させるという部分だと考えます。簡単な例を見ていきます。副作用を伴わない関数の例です。3何回呼び出しをしても同じ結果が返ってきます。次に副作用を伴う関数の例です。5同じ引数で同じ関数を呼び出しているにも関わらず、呼び出す度に異なる結果が返ってきます。これはaddTwo関数がaddTwo関数外の部分に影響を与えているためです。これが先の引用の中にある「ローカル環境以外の状態変数の値を変化させる」という部分に該当します。ちなみに副作用の説明についてはオブジェクト指向並に様々な説明が世間には溢れているので他の記事も参考にしてみてください。〜楽に小さく単体テストをしよう〜プログラミングで副作用と状態ってなに?副作用を排除することは可能か少なくともアプリケーションを作成する上ではほぼ不可能だと考えます。世の中のアプリケーションは副作用を伴うもので溢れているからです。先の例では簡単なプログラム内に留まった例を示しましたが、アプリケーションにおいての副作用の代表的な例はデータベースなどです。単純なSELECT文を発行する場合も、実行結果はSELECT対象のテーブルの状態に依存します。テーブルの状態はプログラムから見ると実際にアクセスしてみるまでわかりません。usersテーブルの状態に依存する先ほど「ローカル環境以外の状態変数の値を変化させる」とあったので、プログラム内に留まる話のように聞こえたかもしれませんが、実際にはデータベースを始めとしたプログラムの外に出た状態の話も含むことが多いです。ここでいうローカルという部分が文脈によってスコープが変わることがある気がしています。フロントエンドにおける副作用の例フロントエンドから直接データベースにアクセスすることはあまりないと思うので、また違った視点からの副作用の例としてZennの画面を見てみましょう。以下に2枚のキャプチャを貼ります。両方ともトップページにアクセスしたときのキャプチャです。認証した状態でアクセス認証なしでアクセス(シークレットモードでアクセス)同じURL(画面)にアクセスしているにも関わらず、ブラウザの状態によってヘッダー部分に差分が出ているのが確認できます。フロントエンドにおいて副作用を伴いがちなもの代表的なものをいくつか列挙します。他にもいっぱいあるかもしれません。バックエンドAPISPA等では基本的にバックエンドAPIをコールし、その結果を表示するような挙動が多いかと思います。このときに受け取るバックエンドAPIの結果は言わずもがなバックエンドやその先にあるDBの状態などに依存します。フロントエンドからすると実際にコールしてみないと結果がわからないです。API・DB等の状態に依存する。データが100件返ってくることもあれば0件の場合もある。Cookieなどブラウザ上に保存されるデータ全般です。LocalStorageを扱う例だと以下のようになります。localStorage.getItem('data');localStorageにデータがセットされていればセットされているデータが返りますが、そうでない場合はnullが返ってきます。同じ引数で同じAPIをコールしているのにも関わらず結果が変わってきます。この例もフロントエンドのアプリケーションからすると、実際にgetItemしてみるまでどういったデータが格納されているかわかりません。StoreやContextなど、グローバルにアクセス可能なStateも副作用を伴います。Stateに持たせることが多いと思います。簡単な例としてVuex公式のコードを見てみます。}}このCounterというコンポーネントはstore.state.countという値を表示するコンポーネントとなっており、storeに依存する形になっています。このstoreはグローバル変数のようなイメージで、Counter以外の他のコンポーネントからも参照することができる状態になっています。別のコンポーネントがこの値を更新した場合はその影響を受けてしまいます。API等が登場したこともあり、複数画面に跨るようなデータ以外でStoreを濫用するのはあまりよろしくない印象を持っています。用法容量を守って適切に使いましょう。Routing関連Routing周りも副作用を伴います。SPAはブラウザのHistoryを書き換えるという性質上、プログラム上だけでなくブラウザが持つ状態に依存する部分が多くなります。RouterでHistoryを扱う例を引用します。v5ではuseHistoryからのgoというAPIでしたが、v6ではuseNavigate、navigateと変更されています。こちらもボタン押下時にnavigate(1)等が実行されますが、実行した結果はブラウザが保持しているHistoryの状態に依存します。Parameterを使う場合も副作用を伴います。こちらも公式の例を見てみましょう。{params.invoiceId}</h2>;}当然ここのuseParams()の結果はブラウザのアドレスバーの値によって変わってしまいます。ある構成だとuseParams()で取得したIDを元にバックエンドAPIを叩いたりすることもあるので、連続的に副作用を伴うこともあります。最後にフロントエンドにおける副作用について色々書いてきましたが、大事なことは副作用が伴う部分を理解した上で、これらをどう取り扱っていくのかということだと思います。DesignでいうところのPage部分でのみ取り扱うなど、何かしらのルールを設定するのが良いと思っています。個人的には可能な限りコンポーネントを関数のように扱い、Propsにのみ依存する作りを目指すようにしています。無秩序に副作用を扱うとどこかで辛みが出てくることでしょう。フロントエンドでも副作用を意識して綺麗な設計を目指していきたいですね。Designを分かったつもりになるGitHubで編集を提案maroKanataniWebフロントエンドを中心に色々やってます。バッジを贈って著者を応援しようバッジを受け取った著者にはZennから現金やAmazonギフトカードが還元されます。バッジを贈るDiscussionmaroKanataniWebフロントエンドを中心に色々やってます。Proよくある質問LinksX(Twitter)GitHubメディアキットLegal利用規約プライバシーポリシ。