$30 off During Our Annual Pro Sale. View Details »

Server Component を理解する(したい)

Avatar for morio morio
July 24, 2023
30

Server Component を理解する(したい)

Avatar for morio

morio

July 24, 2023
Tweet

Transcript

  1. それってなに? • サーバだけで動く React のコンポーネント • 2018 年に発表され、2020 年に最初の RFC

    が出た • https://github.com/reactjs/rfcs/blob/main/text/0188-server-componen ts.md • 2022年12月に出た Next.js 13 の App Router でパンピーも試せるようになった • 現時点で唯一のフレームワークに載った RSC の実装 • 先月に出た Next.js 13.4 で App Router は stable になりました
  2. とりあえず実装してみた • Next.js で、同じ内容のアプリを App Router と Page Directory で作ってみ

    る • Todo アプリ • DB あり • Todo の作成/削除 • Todo のトグル • Todo の検索
  3. これまで • DB へのアクセスは API Routes を立てて行う • マウント時、useEffect で

    API に問い合わせ • クライアントで state 作ってデータ入れる
  4. 登場人物紹介 • Server Component(SC) • サーバでしか実行されないコンポーネント • hooks(useState, useEffect)、イベントハンドラが使えない •

    Client Component(CC) ◦ サーバ、クライアントで実行されるコンポーネント ◦ これまでのコンポーネントと同じ ▪ hooks、イベントハンドラが使える • app directory のコンポーネントは基本 SC • “use client” で CC
  5. App Router • API は立てる必要なし、コンポーネントの中で直接 DB へのアクセスを記 述していい • Server

    Actions で更新系も表現可能 • useEffect は不要 • コンポーネントのトップレベルで await する • Suspense 使える • 問い合わせの結果を保持する state は不要 • クライアントに配信する JS のサイズも減る
  6. const Todo = () => { useEffect(()=> { fetchTodo().then((v) =>

    setTodo(v)); }, []) if(!todo) return <>Loading…</> return (<> … <Author /> </>) } const Author = () => { useEffect(()=> { fetchAuthor().then((v) => setAuthor(v)); }, []) if(!author) return <>Loading…</> return (<> … <Tag /> </>) const Tag = () => { useEffect(()=> { fetchTag().then((v) => setTag(v)); }, []) if(!tag) return <>Loading…</> return (<> … </>) }
  7. const Todo = () => { const fetchAll = async

    () => { await Promise.all([ fetchTodo(), fetchAuthor() fetchTag() ]).then(([todo, author,tag]) => { setTodo(todo) setPosts(posts) setTag(tag) }) } useEffect(()=> { fetchAll() }, []) if(!todo) return <>Loading…</> … }
  8. Relay Relay (Meta 製 GraphQL Client)は、Fetch-on-render のように colocation し つつ、

    Render-as-you-fetch へ移行する方法を提供する https://relay.dev/docs/guided-tour/rendering/queries/#render-as-you-fetch • Fragment でコンポーネント自身のコードとデータ取得のコードを並置 • 親で一括でデータ取得することで、Waterfall を避ける • Suspense でローディング境界を定義できる Relay & GraphQL じゃないと最高になれないのか?😡
  9. というわけで RSC は、GraphQL や Relay などのライブラリに依存せず、React のコアで Waterfall 問題を解決しようとするもの サーバ/DB間であれば

    Waterfall があったとしても程度を抑えられる そしたら他にもいいことがあった: • バンドルサイズ削減 • コードスプリットの自動化
  10. どうやっているのか - JSX ツリーをシリアライズしたものをサーバから送信 - HTML は送らない - クライアントでデシリアライズして、クライアント側のツリーとマージし て更新

    • https://github.com/reactwg/server-components/discussions/5(最初に php のコードが出てきたりしておもろい) • https://www.plasmic.app/blog/how-react-server-components-work
  11. 1⃣: サーバにページの JSX をシリアライズして送るようリクエスト JSX は React.createElement() の糖衣構文 { "type":

    "div", "key": null, "ref": null, "props": { "children": "hoge" }, "_store": {} } console.log(<div>hoge</div>) →
  12. 2⃣: サーバでデータを取得してシリアライズ シリアライズはサーバで行われるので、SC では Node.js の API を使える シリアライズ不能なものは SC

    の props に書けない(イベントハンドラ) → const Post = async ({postId}) => { const data = await db.findOne(...) return (<main> .... </main>) }; { “type”: “main”, “props”: { “children”: [ { … } } }
  13. const Fetcher = async () => { const data =

    await fetch(…) return … } const Parent = () => { <> parent <Suspense fallback="loading"> <Fetcher /> </Suspense> </> } J0:["$","",null,{"children”:[“parent”,[“$”,”$2",null,{"fallback”:”loading”,”children”:”…”}]]}] S2:"react.suspense" J0:["$","",null,{"children”:[“parent”,[“$”,”$2",null, …]]}]
  14. 4⃣: クライアントでデータを受け取り、JSXにデシリアライズする • その際CCの参照が含まれる場合は動的に import しつつ、復号された JSX に マージ •

    こうすることでCCのステートを保持しつつ、更新(mutation, navigation)が あった場合は新しいデータで画面を更新できる
  15. SSR と RSC はどう違うのか? - SSR と RSC は別物で、併用可能 App

    Router では、初回アクセスは HTML を SSR で返し、以降は RSC のフォーマットでやり取りする
  16. まとめ • RSC とは、React チームが勧める新しいデータ取得のための戦略 ◦ Server Component と Client

    Component がある ◦ データ取得をよりいい感じに書ける ◦ Waterfall の解決が元々のモチベーション ◦ SPA に、MPA の良いところを取り込む • プロダクション投入できる? ◦ Server Actions が stable になるまでは早いかも