Image

Next.js Combining Server and Client Components

Here is a pattern for how one might want to structure server and client components that are closely related.

From: https://www.mux.com/blog/what-are-react-server-components#did-someone-say-advanced-patterns

Let’s say we’re making a  component. We might want the syntax highlighting to stay on the server so we don’t have to ship that large library, but we might also want some client functionality so that the user can switch between multiple code examples. First, we break the component into two halves: CodeBlock.server.js and CodeBlock.client.js. The former imports the latter. (The names could be anything; we use .server and .client just to keep things straight.)

components/CodeBlock/CodeBlock.server.js

// filename: components/CodeBlock/CodeBlock.server.js
import Highlight from 'expensive-library'
import ClientCodeBlock from './CodeBlock.client.js'
import { example0, example1, example2 } from './examples.js'

function ServerCodeBlock() {
  return (
    <ClientCodeBlock
      // because we're passing these as props, they remain server-only
      renderedExamples={[
        <Highlight code={example0.code} language={example0.language} />,
        <Highlight code={example1.code} language={example1.language} />,
        <Highlight code={example2.code} language={example2.language} />
      ]}
    >
  )
}

export default ServerCodeBlock

components/CodeBlock/CodeBlock.client.js

"use client"
import { useState } from 'react'

function ClientCodeBlock({ renderedExamples }) {
  // because we need to react to state and onClick listeners,
  // this must be a Client Component
  const [currentExample, setCurrentExample] = useState(1)
  
  return (
    <>
      <button onClick={() => setCurrentExample(0)}>Example 1</button>
      <button onClick={() => setCurrentExample(1)}>Example 2</button>
      <button onClick={() => setCurrentExample(2)}>Example 3</button>
      { renderedExamples[currentExample] }
    </>
  )
}

export default ClientCodeBlock

Now that we have those two components, let’s make them easy to consume with a delightful file structure. Let’s put those two files in a folder called CodeBlock and add an index.js file that looks like this below. Now, any consumer can import CodeBlock from ‘components/CodeBlock.js’ and the Client and Server Components remain transparent.

components/CodeBlock/index.js

export { default } from './CodeBlock.server.js'

The second way a component gets shipped to the client is if it’s imported by a Client Component. In other words, if you mark a component with “use client”, not only will that component be shipped to the client, but all the components it imports will also be shipped to the client.

Any component that’s imported in a client component becomes also a client component. Except server components.

Nextjs-server-client-component-dependency.png

Using Server Components and Passing Down Data to Client

video/page.jsx

/**
  * All we're doing here is fetching data on the server,
  * and passing that data to the Client component.
  */
import VideoPageClient from './page.client.jsx'

// this used to be getServerSideProps
async function fetchData() {
  const res = await fetch('https://api.example.com')
  return await res.json()
}

export default async function FetchData() {
  const data = await fetchData()
  {/* We moved our page's contents into this Client Component */}
  const <VideoPageClient data={data} />
}

export default Page

video/page.client.jsx

/**
  * Our whole app, except for the data fetching, can live here.
  */
"use client"

export default function App({ data }) {
  <>
    <Player videoId={data.videoId} />
    <Title content={data.title} />
  </>
}

Nextjs-server-component-prop-drill.png

Tips for Optimized Patterns

From: https://www.mux.com/blog/what-are-react-server-components

For example, when we migrated our docs site to RSCs, we leaned on two patterns to unlock deeper gains. The first was wrapping key Server Components in Suspense to enable streaming of slow data fetches (as demonstrated earlier). Our whole app is statically generated except for the changelog sidebar, which comes from a CMS. By wrapping that sidebar in Suspense, the rest of the app doesn’t have to wait for the CMS fetch to resolve. Beyond that, we leveraged Next.js 13’s loading.js convention, which uses Suspense/streaming under the hood.

The second optimization we applied was creatively rearranging Client and Server Components to ensure that large libraries, like our syntax highlighting, Prism, stayed on the server.

Mixing Server and Client Components

For a client component, a server component can only ever be passed as a child or as a prop (but NOT imported).

import ClientComponent from './ClientComponent.js'
import ServerComponentB from './ServerComponentB.js'

/** 
  * The first way to mix Client and Server Components
  * is to pass a Server Component to a Client Component
  * as a child.
  */
function ServerComponentA() {
  return (
    <ClientComponent>
      <ServerComponentB />
    </ClientComponent>
  )
}

/** 
  * The second way to mix Client and Server Components
  * is to pass a Server Component to a Client Component
  * as a prop.
  */
function ServerPage() {
  return (
    <ClientComponent
      content={<ServerComponentB />}
    />
  )
}

© Filip Niklas 2024. All poetry rights reserved. Permission is hereby granted to freely copy and use notes about programming and any code.