これまでのおさらい
はじめに
BASE株式会社でシニアエンジニアを務めているプログラミングをするパンダ(@Panda_Program)と申します。本連載はバックエンドの開発者向けに特化したフロントエンド入門です。
対象読者
本連載の対象読者はオブジェクト指向プログラミングの基礎を理解しており、フロントエンド開発に興味があるバックエンドエンジニアの方です。バックエンド開発で主流であるオブジェクト指向プログラミングと比較しながらフロントエンドの考え方を理解することで、スムーズにフロントエンド開発を始められることを目的としています。
データ取得(データフェッチ)の方法を比較する
連載第3回となる本記事はAPIサーバーからのデータ取得(データフェッチ)がテーマです。本記事ではバックエンドのデータ取得の方法を簡単にまとめた後、Reactの副作用、JSの非同期処理、Reactのデータ取得方法の変遷と現在の主流を紹介します。
バックエンドの基本は同期的処理
一般的にバックエンドは同期的な処理をします。普段は意識しないかもしれませんが、同期的な処理であるということはフロントエンドでのデータ取得を理解するために念頭に置くべき事項です。では同期的処理とは改めてどういうことでしょうか。
コードは逐次実行、つまり上から順番に処理されます。ある処理が終わるまでは次の処理は始まりません。これが同期的(sync, synchronous)な処理の特徴です。
以下のようなコードでは、文字列処理A
が表示されてから1秒後に処理B
が表示されます。これは処理を同期的にしているからです。
console.log('処理A') sleep(1000) console.log('処理B')
JavaScriptのデータ取得は非同期処理
一方、JavaScriptで構築するフロントエンドアプリケーションにおいて、データ取得は同期処理ではなく非同期処理として扱われます。
非同期(async, asynchronous)な処理とは、結果を待たずに別のタスクを進められる処理のことです。JavaScriptにおいて、同期的な処理が完了した後に非同期処理が実行されるものと大まかに考えてもらって良いかと思います[1]。
JavaScriptの非同期処理には、addEventListenerで登録されてイベントに応じて発火する処理や、非同期処理の状態や結果を表現するオブジェクトであるPromiseを返すfetchなどの処理、setTimeoutに渡されて時間差で実行される処理があります。
console.log('処理A') setTimeout(() => { console.log('非同期処理') }, 1000) console.log('処理B')
このコードを実行すると、処理A
と処理B
が続けて表示された後、1秒後に非同期処理
が表示されます。setTimeoutに渡された関数(コールバック関数)は、同期的な処理が終わった後に非同期で処理されるからです。
処理A 処理B 非同期処理
では、なぜイベントに応じて発火する処理やAJAXによるデータ取得といったブラウザの主要な処理は非同期処理として扱われるのでしょうか。
それは、実行環境がブラウザであるということが影響します。仮にJavaScriptが同期的な処理のみであった場合、全ての処理が終わるのを待つ必要があります。以下のコード例をご覧ください。
// この二つの fetch が同期的な処理だと仮定した場合 const users = fetch('https://example.com/users/1'); const todos = fetch('https://example.com/todos/2'); // 上記の処理が終わらないと、クリックイベントのハンドラを登録できない document.addEventListener('click', () => {/* ... */})
例えば、JavaScriptがエンドポイントAにHTTPリクエストを投げた場合、Aからレスポンスが返ってくる間はエンドポイントBにリクエストを送れません。AとBの処理を待っている間は次に進まず、ボタンクリックのハンドラの登録すらできません。
このため、HTTPリクエストのように時間がかかる処理を待っている間、ユーザーはUIがフリーズしたように感じてしまいます。実際は以下のような流れで処理されます。
// 非同期処理なのでリクエストを送るが、レスポンスを待たずに処理は次に進む const users = await fetch('https://example.com/users/1'); const todos = await fetch('https://example.com/todos/2'); // addEventListener自体は同期的な処理であるため、 // fetchのレスポンスを待たずにイベントハンドラを登録できる document.addEventListener('click', () => {/* ... */})
JavaScriptが非同期処理を扱うことで、画面の描画や処理がブロックされるといった問題が発生することを避けることができます。JavaScriptでは、API経由のデータ取得は非同期的な処理なのです。
Reactにおける副作用とは何か
ここまではJavaScriptにおけるデータ取得の考え方を紹介しました。本記事はReact入門なので、次にReactのデータ取得の考え方を紹介します。
Reactは関数型プログラミングに大きな影響を受けて作られたUIライブラリです。関数型プログラミングではデータ処理に純関数が用いられます。Reactにおける純関数の考え方は前回紹介したので割愛します。ここでは簡単に「stateとpropsに応じて一意なUIを決定すること」だと捉えてもらって構いません[2]。 UI = f(state, props)
というやつです。
Reactは予測可能な処理である純関数を好みます。Reactの中の世界に閉じこもっている限り、アプリケーションは純粋なデータ処理だけに専念できます。しかし、そのReactにおいて、外部のデータソースへのアクセスは副作用であると考えられています。
Webアプリケーションを構築するためには外部からのデータ取得が不可欠です。ただし、外の世界は予測不可能です。
例えば、外部のAPIサーバーの処理が重く、レスポンスが遅くて画面を表示するためのデータがすぐに揃わないかもしれません。また、サーバーダウンしてレスポンスを返さないこともあるでしょう。それでもHTTPレスポンスを送ったり、直接DOMを操作したり、チャットサーバーからのメッセージをsubscribeしたり、React外の予測できない世界に出ることは実用的なアプリケーションを構築するためには必要不可欠です。
そこで、Reactはこのような副作用(effect)を許容しています。ただし、副作用のある処理はイベントハンドラとuseEffectというReact Hooksの中(限定された場所)でしか実行できません。なお、React Hooksとは、関数コンポーネントで状態管理や副作用を扱うための特殊な関数です。
2018年にReact Hooksが登場した当初はuseEffectによるデータ取得がベーシックな方法でした。GETリクエストはuseEffectの中で、POST/PUT/DELETEのリクエストはボタンやフォームのイベントハンドラの中で実行することが一般的でした。
しかし、外部APIからのデータ取得はJavaScriptにとって非同期処理であり、Reactにとって副作用です。Reactは副作用のない純粋な関数を好みます。このため、Reactのエコシステムの成長やReactの進化に伴い、useEffectでのデータ取得について今ではあまり好ましくない方法だと見なされています。
では、フロントエンドからはどのようにデータ取得をすればいいのでしょうか。次にReactにおけるデータの取得方法の変遷を見ていきましょう。
- [1]JavaScriptの同期処理・非同期処理を詳しく知りたい方は「非同期処理:Promise/Async Function / JavaScript Primer - 迷わないための入門書」をご覧ください。
- [2]詳しくはReact公式ドキュメント「コンポーネントを純粋に保つ」をご覧ください。