🥑

Next.js App Router でのRecoilの使い方

2023/09/08に公開

TL;DR

Composition(componentをpropsとして渡す)Patternを使用します。
※このパターンは元々Reactで再レンダリングを防ぐために使われてたパターンだそうです。何もNext.jsだけでの用語ではないようです。
https://www.youtube.com/watch?v=7sgBhmLjVws

以下のように component(children)をpropsとして渡すことで、このprops(children)に渡されるserver componentはちゃんとserverでレンダリングされるようになります。
RecoilProviderはuse clientディレクティブが使用されているのでclient componentです。なのでclientでレンダリングされる。composition patternを使用することでレンダリングをそれぞれ独立させることができるようになるようです。※ここらへんの実際の挙動を観測していないのでhidration(client componentもserverで描画HTML->ブラウザで静的なHTMLとして表示->Hidration(jsがロードされたタイミングで動的なアプリケーションに復元)->動的なReactアプリケーションとして動作)とかは普通に起こっていると思うのでclientはclientでしかレンダリングされない!とは断言できないのですが、このパターンによってserver componentの分のjavascriptがサーバーからクライアント(ブラウザ)に送られるjs bundleに含まれることは防げるそうです。

app/recoilProvider.tsx
'use client'

import { ReactNode } from "react";
import { RecoilRoot } from "recoil";

function RecoilProvider({ children }: { children: ReactNode }) {
  return (
    <RecoilRoot>{children}</RecoilRoot>
  );
}

export default RecoilProvider;
app/layout.tsx
import { ToastContainer } from 'react-toastify'
import Header from './_components/layouts/Header'
import Footer from './_components/layouts/Footer'
import RecoilProvider from './recoilProvider'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="ja">
      <body>
        <RecoilProvider>
          <Header />
          <ToastContainer position="top-right" autoClose={3000} />
          {/* template.tsx */}
	     {children}
          {/* / template.tsx */}
          <TheFooter />
        </RecoilProvider>
      </body>
    </html>
  )
}

どんなことを考えていたのか

https://nextjs.org/docs/app/building-your-application/upgrading/app-router-migration#step-2-creating-a-root-layout

app routerで以下を作成しました

  • app/recoilProvider.tsx
  • app/template.tsx
  • app/layout.tsx

プロジェクトのstate管理(クライアント,browserのメモリー上に保存される情報)にrecoilを使用していたので、それぞれのページの子コンポーネント(CC)からstateにアクセスするためにはツリーのトップレベル(app/layout.tsx、それぞれのページに遷移しても初期化されない場所で)をRecoilRootで囲う必要性がありました。しかしRecoilは先ほども言ったようにbrowserのメモリー上に情報を保存する仕組み、つまりクライアント側でしか動作しません、よって 'use client'をしたClient ComponentにしかRecoilRootを配置することしかできません。ツリーの一番上で use clientしたらその下が全てjs bundleに含まれてしまうのか!! それじゃ意味ないじゃないか!と最初は思いました。しかしそうはなりませんでした。 以下のようにapp/recoilProvider.tsx,app/layout.tsx,app/template.tsxこの3つのファイルはcomponent(children: React.ReactNode)をpropsとして受け取っています(Composition Pattern)。このComposition Pattern(componentをpropsとして渡すこと)によって server componentとclient componentのレンダリングが切り離され、独立してレンダリングすることができます。そしてクライアントでそれらを構成してるらしいです。その時にslots(みぞ?みたいな)にserver componentが描画されたもの(HTML)を当てはめてる感じだと認識しております。
間違ってたら補足していただけると嬉しいです:bow:
初期描画を早くしてユーザーの待ち時間を減らしてユーザー体験(UX)をよくするために、最初のページロードの際はclient componetもserver componentもserver側で事前にレンダリングされてるらしいです。ちょっとこの辺の挙動についてはこれから動かしながら確認していく予定です。まとまり次第記事にしてみたいと思います。

https://nextjs.org/docs/getting-started/react-essentials#composing-client-and-server-components

※なんかserver側では server componentに出くわしたらレンダリングしてclient componentに出くわしたらレンダリングをスキップしてるとも書かれてたりします。筆者の読解力が足りないこともありこのあたりの挙動については今後動かしながら確認していく所存です。

app/recoilProvider.tsx
'use client'

import { ReactNode } from "react";
import { RecoilRoot } from "recoil";

function RecoilProvider({ children }: { children: ReactNode }) {
  return (
    <RecoilRoot>{children}</RecoilRoot>
  );
}

export default RecoilProvider;
app/template.tsx
'use client'
import GoogleAnalytics from './googleAnalytics'

function Template({ children }: { children: React.ReactNode }) {

  return (
    <>
      <GoogleAnalytics />
      {children}
    </>
  )
}

export default Template
app/layout.tsx
import { ToastContainer } from 'react-toastify'
import Header from './_components/layouts/Header'
import Footer from './_components/layouts/Footer'
import RecoilProvider from './recoilProvider'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="ja">
      <body>
        <RecoilProvider>
          <Header />
          <ToastContainer position="top-right" autoClose={3000} />
          {/* template.tsx */}
	     {children}
          {/* / template.tsx */}
          <TheFooter />
        </RecoilProvider>
      </body>
    </html>
  )
}

なんかlayoutは、layoutより下の階層の異なるページ(遷移しても)でstateとかシェアされて、
templateは遷移ごとに新しいインスタンスを作るらしいです。

だからRecoilRootとかページを横断して保持して欲しいものはapp/layout.tsxに書いて、
GoogleAnalyticsとかページごと(urlを記録してどのページにどのくらいの人が訪れたか確認したい)のものはapp/template.tsxに書きました

https://nextjs.org/docs/app/building-your-application/routing/pages-and-layouts#templates

Composition Patternはapp routerのlayoutとpageに使われている仕組みでClient ComponentとServer componentのレンダリングを独立させるのによく使われるらしいです。
このパターンを分かりやすく説明してくれてる動画あるので貼っときます。

https://www.youtube.com/watch?v=D7DC9pEaLFo

app/googleAnalytics.tsxのサンプルコードも貼っときます。
ここで重要だと思うことは2つくらいです

  • Scriptのstrategy="afterInteractive"で外部のjsをhidrationが起こった後に読み込む

https://nextjs.org/docs/app/building-your-application/optimizing/scripts#strategy

  • useSearchParamsを使用しているGAScriptをSuspenseで囲む

https://nextjs.org/docs/app/api-reference/functions/use-search-params#behavior

app/googleAnalytics.tsx
'use client'

import { usePathname, useSearchParams } from 'next/navigation'
import Script from 'next/script'
import { Suspense, useEffect } from 'react'


function GAScript() {
  const GA_TRACKING_ID = process.env.NEXT_PUBLIC_GA_ID
  const loggingPageView = (url: string) => {
    if (!GA_TRACKING_ID) return
    window.gtag('config', GA_TRACKING_ID, {
      page_path: url,
    })
  }
  const pathname = usePathname()
  const searchParams = useSearchParams()
  useEffect(() => {
    const url = searchParams ? pathname + searchParams.toString() : pathname
    url && loggingPageView(url)
  }, [pathname, searchParams])

  return (
    <>
      <Script
        strategy="afterInteractive"
        src={`https://www.googletagmanager.com/gtag/js?id=${GA_TRACKING_ID}`}
      />
      <Script
        id="gtag-init"
        strategy="afterInteractive"
        dangerouslySetInnerHTML={{
          __html: `
        window.dataLayer = window.dataLayer || [];
        function gtag(){dataLayer.push(arguments);}
        gtag('js', new Date());
        gtag('config', '${GA_TRACKING_ID}', {
          page_path: window.location.pathname,
        });
      `,
        }}
      />
    </>
  )
}

export default function GoogleAnalytics() {
  return (
    <>
      {GA_TRACKING_ID && (
        <Suspense>
          <GAScript />
        </Suspense>
      )}
    </>
  )
}
	

アイキャッチ絵文字のアボカドの種がserver componentで
外側がrecoilProviderみたいなイメージを持ってます!🥑
アボカドをくり抜くとボールが入りそうな穴が開くと思うのですが、
そこがNext.jsドキュメントに出てくるslots(溝、みぞ)のようなもので
そこに描画済みのHTML(🥑の種)がハマるみたいなイメージを持ってるのですが、
ちゃんと挙動を確認しないとなんとも言えないのでちゃんと地道に確認していきたいと思います

ではまた明日!

Discussion