React Native × React Server Componentsの可能性を探る
Expo SDK 52の登場
Expo公式サイトより
ExpoはReact Nativeでの開発・デプロイを便利にするエコシステムツールです。
少し前のExpoには制約が多かったのですが、現在ではできることが大幅に広がり、
Expoを使ってReact Nativeアプリの開発をするのが主流です。
先月、Expoは大型アップデートをリリースしました。
色んな目玉機能が組み込まれたのですが、中でも注目されたのがServer Componentsです。
まだプロダクションでは利用できないものの、多くの開発者が期待を寄せています。
この記事では、それらの活用法、将来性について見ていきます。
Server-Driven UIについて
通常、UIに関することは基本的にクライアントサイドで定義します。
しかし、従来の方法には
- UI修正のために審査を待つ必要がある
- クライアントによってロジックが異なってくる
- 表示切替のためのロジックが複雑になる(ABテスト、Feature Flagなど)
といった問題点がありました。
そこでServer-Driven UIという思想が生まれました。
Server-Driven UIはサーバーサイドでレイアウトやコンポーネントを定義しようという思想です。
Airbnbがこの設計思想を提唱し、NetflixやRedditなどもこの思想を取り入れています。
React ConfのYoutubeより
クライアントが
- サーバーにリクエストする
-
サーバーからUIデータを取得
(ページレイアウト、コンポーネントのスタイリング、テキストの値など) - データを元にレンダリング
という流れでUIを構築します。
以下はJSONレスポンスからAirbnbのUIが構築されるイメージです。
A Deep Dive into Airbnb’s Server-Driven UI Systemより
Server-Driven UIを実現するServer Components
Server ComponentsはReact18で登場した機能です。
Server Componentsは
- レンダリングリクエストをサーバーに投げる(クライアント)
- レンダリングリクエストを受け付ける(サーバー)
- Reactツリーを構築する(サーバー)
- RSCペイロードに変換する(サーバー)
- クライアントにRSCペイロードを返却する(サーバー)
- RSCペイロードをもとにUIを構築する(クライアント)
という手順でレンダリングされます。
RSCペイロードを詳しく見ていきましょう。
Introducing Universal React Server Components in Expo Routerより
3行目に着目すると、
4: I[123, ["/node_modules/react-native/index.js"], "Text"]
となっており、
サーバーからReact Nativeの<Text>
を使いますよというデータが送信されています。
つまり、サーバーサイドでコンポーネントを1から定義することができます。
(クライアント側でオリジナルなコンポーネントを定義し、サーバ側でレイアウトだけ定義するものではない)
実際、以下のようにしてServer Componentsを定義できます。
'use server';
import { Image, Text, View } from 'react-native';
export async function Pokemon() {
const res = await fetch('https://pokeapi.co/api/v2/pokemon/2');
const json = await res.json();
return (
<View style={{ padding: 8, borderWidth: 1 }}>
<Text style={{ fontWeight: 'bold', fontSize: 24 }}>{json.name}</Text>
<Image source={{ uri: json.sprites.front_default }} style={{ width: 100, height: 100 }} />
{json.abilities.map(ability => (
<Text key={ability.ability.name}>- {ability.ability.name}</Text>
))}
</View>
);
}
ExpoチームはReact NativeでもServer Componentsを利用して、完全なServer-Driven UIを実現しようとしているのです。
メリット
UIを瞬時に柔軟に切り替えられる
サーバーサイドでUIのレイアウトを定義するため、UIの変更内容をサーバーにデプロイすると、クライアントのUIを瞬時に切り替えられます。
使えそうな場面としては、
- 緊急のUI修正をしたいとき
- ユーザーごとに異なるレイアウトを表示したいとき
- A/Bテストをしたいとき
などでしょうか。
AppleやGoogleの審査を受けずに即時反映できるので、大きなメリットになりそうです。
データフェッチにSuspenseが使える
Server Componentはasync functionとして定義できることから、Suspenseと相性がいいです。
Suspenseを使うことで、データ待機時のスケルトン表示などが簡単に実装できます。
<Suspense fallback={<SkeltonComponent />}>
<SomeComponent />
</Suspense>
アプリサイズを削減できる
Server Componentsを使うことでクライアント側のサイズを抑えることができます。
ExpoではNext.jsのようにServer ComponentsとClient Componentsを共存させることができます。(experimentalですが)
Next.jsのDiscussionより
動きの必要な部分だけClient Componentsにすることで、クライント側のサイズを減らすことができます。
また、OTA(Over the Air)アップデートと比較しても、JavaScriptのサイズは小さく済むそうです。
OTAアップデートの仕組みと課題
React Nativeアプリは、JavaScriptからブリッジを経由して、ネイティブUIを描写します。
つまり、JavaScriptを差し替えればネイティブUIを変更できます。
OTAアップデートはこの性質を利用したものです。
EASサーバーから最新のJavaScriptをダウンロードすることで、
審査なしでネイティブUIの更新をすることができます。
リリース後にOTAアップデートを1回もpublishしていない場合、アプリに埋め込まれたJavaScriptを読み込んでネイティブUIに変換します。
OTAアップデートをpublishするとどうでしょうか。
起動時に最新のJavaScriptがダウンロードされると、次の起動時からそちらを優先して読み込みます。ただし、アプリに埋め込まれたJavaScriptは削除されません。
OTAアップデートの仕組みによって
- アプリに埋め込まれたJavaScript
- OTAアップデートのJavaScript
の両方が共存するため、アプリサイズが大きくなってしまいます。
これはデメリットの1つです。
他にも、React Nativeのコントリビューターの1人であるSzymon Rybczak氏は、OTAアップデートのデメリットについて言及しています。
we need to replace entire JavaScript bundle, which can be quite large in size
非常に大きいサイズのアプリ全体のJavaScriptバンドルを置き換える必要があります。
This can be particularly challenging with a slow internet connection, as it can take a while to download.
これはダウンロードに時間がかかるため、特に低速のネット環境で弊害になります。
OTAアップデートではアプリ全体のJavaScriptバンドルをダウンロードします。
そのため、JavaScriptのサイズは課題になります。
一方、Server Componentsは必要なときに必要な部分だけサーバーからデータが送られるため、
より合理的と言えるかもしれません。
デメリット
サーバーが必要になる
Server-Driven UIという名前の通り、サーバーが必要になります。
モバイルとWebをExpoで並行して開発するのであれば、Webアプリをサーバーにデプロイするため、良いかもしれません。
しかし、ExpoでWebアプリも開発するケースはそれほどあるのでしょうか?
Web開発では、Next.jsが圧倒的な人気を誇っており、エコシステムが整っています。
また、Webとモバイルは性質がかなり異なるため(Cookieの有無とか)、ExpoでWebも作るケースは少ないのではないかと個人的に思います。
WebはNext.js、モバイルはExpoというケースの方が多そうです。
個人的な予想なのですが、ExpoはOTAアップデート用のサーバーを提供しているので、
Server Components用のサーバーも提供するのではないかと予想しています。
これが実現すれば、Server-Driven UIの敷居がグッと下がりそうです。
サーバーへの負担が増える
Server-Driven UIを導入する場合、データ送信の他に、UIのレンダリングもサーバーで行います。
従来の方法と比べてサーバーへの負担が増えるため、ランニングコストが上がる可能性があります。
こちらの件も、Server Components用のサーバーも提供され、フリープランなどが用意されればある程度許容できるかもしれません。
ストアポリシーを考慮する必要がある
AppleやGoogleがServer-Driven UIに対して今後規制をかけるかもしれません。
なぜなら、Server Componentsにネイティブビュー・イベントを組み込むことができるからです。
Context Menuを組み込んだり、
Introducing Universal React Server Components in Expo Routerより
Apple Mapsなどを組み込んだりすることが可能です。
Introducing Universal React Server Components in Expo Routerより
プレゼン内ではこの仕組みについて解説していなかったので、考察してみます。
例えば、Apple MapsのネイティブビューはReact Nativeに搭載されていないため、
react-native-mapsというネイティブモジュールをクライアントに入れておく必要があります。
react-native-mapsをクライアントに入れた状態で、以下のようなServer Componentsのレンダリングリクエストが投げられるとします。
'use server';
import MapView from 'react-native-maps';
export const AppleMapsComponent = () => {
return (
<>
<Text>Hogehoge</Text>
<MapView
initialRegion={{
latitude: 37.78825,
longitude: -122.4324,
latitudeDelta: 0.0922,
longitudeDelta: 0.0421,
}}
/>
</>
)
}
サーバーは以下のような内容を含むRSCペイロードを返します。(予想)
9999: I[123, ["/node_modules/react-native-maps/src/index.ts"], "MapView"]
これを受け取ったクライアントは、ネイティブモジュールを呼び出してApple Mapsを描画することができるのだと思います。
そのため、
- クライアントに特定のネイティブモジュールを入れる必要がある
- Server Componentsに何でもかんでもネイティブビュー・イベントは組み込めない
- OTAアップデートと同様、ネイティブモジュールを追加したい場合は審査に出す必要がある
というのが私の予想です。
とはいえ、サーバー側でネイティブビューやイベントも定義できるとなると、セキュリティ上の懸念が出てきます。
もしかしたら、今後ストアの運営側が新たな規制を作るかもしれません。
まとめ
Server-Driven UI、モバイルでのServer Componentsの理解が難しく、記事を書くのにかなり時間がかかりました、、
冷静に考えると、Sever-Driven UIはWebでは当たり前だったことをモバイルにも適用しようとしているように見えます。
「モバイルアプリはクライアントで色々やるのが当たり前」という常識を覆すようなアップデートで面白いなと思いました。
この記事で少しでも理解の助けになれたら幸いです。
参考
Discussion