reactjsnext.jsmaterial-uinext.js13

How to render a server component from a client component in react 18 - nextjs 14 app router?


According to this RFC:

Client Components May not import Server Components or call server hooks/utilities, because those only work on the server.


I am working on a project using react 18.2 and nextjs v14 with app router.

I have this page which I am working on:

enter image description here

The page, is a client component, because almost everything in the page depends on client features. This is a form, so everything depends on the React useState() hook.

The actual code of the page.tsx looks as follows, it's a big component, I am using a components library called JoyUI (by mui).

'use client'

import React from 'react'
import { Grid } from '@mui/material'
import useBreakpoints from '@/hooks/useBreakpoints'
import PasswordInput from '@/components/PasswordInput'
import AvatarUploader from '@/components/AvatarUploader'
import { Stack, Input, FormLabel, Checkbox } from '@mui/joy'
import {
  EmailRounded as EmailIcon,
  PhoneRounded as PhoneIcon,
  PersonRounded as PersonIcon,
} from '@mui/icons-material'
import SelectCompany from './SelectCompany'

const NewModeratorPage = () => {
  const { isDownMd } = useBreakpoints()

  return (
    <Grid container spacing={2}>
      <Grid item xs={12} md={3}>
        <AvatarUploader placeholder={<PersonIcon sx={{ fontSize: 64 }} />} />
      </Grid>
      <Grid item xs={12} md={8}>
        <Stack spacing={1}>
          <FormLabel>Name</FormLabel>
          <Stack direction={isDownMd ? 'column' : 'row'} spacing={2}>
            <Input placeholder="First name" size="sm" fullWidth required />
            <Input placeholder="Last name" size="sm" fullWidth required />
          </Stack>
          <FormLabel>Credentials</FormLabel>
          <Stack direction={isDownMd ? 'column' : 'row'} spacing={2}>
            <Input
              placeholder="Email"
              startDecorator={<EmailIcon />}
              type="email"
              size="sm"
              fullWidth
              required
            />
            <Input placeholder="Phone number" startDecorator={<PhoneIcon />} size="sm" fullWidth />
          </Stack>
          <Stack direction={isDownMd ? 'column' : 'row'} spacing={2}>
            <PasswordInput size="sm" fullWidth required />
          </Stack>
          <Checkbox size="sm" label="Send password via email" defaultChecked />
          <SelectCompany />
        </Stack>
      </Grid>
    </Grid>
  )
}

export default NewModeratorPage

So, the <SelectCompany /> involves data fetching.

Which is a good indicator that this component should be a server component.

So no loading time for fetching the companies on the client side.

Here's the code of the <SelectCompany /> server component.

"use server"

import React, { PropsWithChildren } from 'react'
import { Select, Option, Avatar, Stack } from '@mui/joy'

export interface SelectCompanyProps extends PropsWithChildren {}

const SelectCompany = async ({ children, ...props }: SelectCompanyProps) => {
  const response = await fetch('/api/companies')
  const data = await response.json()
  console.log(data)

  return (
    <Select value={1}>
      <Option value={1}>
        <Stack direction="row" spacing={2}>
          <Avatar size="sm" />
          <React.Fragment>Company 1</React.Fragment>
        </Stack>
      </Option>
    </Select>
  )
}

export default SelectCompany

Firstly, I am getting this error in the browser:

Error: async/await is not yet supported in Client Components, only Server Components. This error is often caused by accidentally adding `'use client'` to a module that was originally written for the server.

And in the terminal (I am on Linux btw), I am getting this error:

Internal error: Error: Server Functions cannot be called during initial render. This would create a fetch waterfall. Try to use a Server Component to pass data to Client Components instead.

The reason of this error, as far as I can tell, is that it's not allowed for client components to import server components,

So what is the solution to my case? I want <SelectCompanies /> to be a server component.


Solution

  • There are a couple of supported and unsupported pattern from nextjs docs

    Unsupported

    'use client'
     
    // You cannot import a Server Component into a Client Component.
    import ServerComponent from './Server-Component'
     
    export default function ClientComponent({
      children,
    }: {
      children: React.ReactNode
    }) {
      const [count, setCount] = useState(0)
     
      return (
        <>
          <button onClick={() => setCount(count + 1)}>{count}</button>
     
          <ServerComponent />
        </>
      )
    }
    

    This is what you tried instead you can pass the server component as a child like this:

    Supported pattern

    'use client'
     
    import { useState } from 'react'
     
    export default function ClientComponent({
      children,
    }: {
      children: React.ReactNode
    }) {
      const [count, setCount] = useState(0)
     
      return (
        <>
          <button onClick={() => setCount(count + 1)}>{count}</button>
          {children}
        </>
      )
    }
    
    // This pattern works:
    // You can pass a Server Component as a child or prop of a
    // Client Component.
    import ClientComponent from './client-component'
    import ServerComponent from './server-component'
     
    // Pages in Next.js are Server Components by default
    export default function Page() {
      return (
        <ClientComponent>
          <ServerComponent />
        </ClientComponent>
      )
    }
    

    By passing them as children we are aware of all server components and can render/stream them.