DEV Community

Cover image for 10 things about component composition in nextjs
Anjan Shomodder
Anjan Shomodder

Posted on

10 things about component composition in nextjs

In this blog, I will talk about 10 things you should know about component composition in Next.js. You will learn:

  1. Different composition patterns with Server and client components
  2. Pattens that don't work
  3. How to use third-party packages that use browser api
  4. When to use server and client components
  5. How to share data between components
  6. Tips on performance

What is component composition in Nextjs?

In simple words, it is just a way to combine server and client components in Next.js.

React tree and obvious pattern

Few things to know:

  1. React uses a tree structure to render components like the actual DOM. See the image below.
  2. All the components in nextjs are now server components.
  3. You can make a client component by adding use client directive at the top of the file.
  4. It is obvious that you can nest server components inside server components and client components inside client components. React server and client component tree

Let's learn the 10 things you should know about component composition in Next.js.

1. Client component inside server component

Image description

You can use client components inside server components without any problem. Check the code below.

'use client'
// client component
import { useState } from 'react' // client side module

export default function ClientComponent() {
    const [count, setCount] = useState(0)
    return (
        <div>
            <button onClick={() => setCount(count + 1)}>Increment</button>
            <h1>Client Component</h1>
            <p>Count: {count}</p>
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode
// server component
import fs from 'fs' // server side module
import ClientComponent from './clientComponent'

export default function ServerComponent() {
    return (
        <div>
            <h1>Server Component</h1>
            <ClientComponent />
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

I have video explaining all these points. Check it out.

2. All components are client components

You might have a lot of client components and adding use client directive to all of them is a bit of a hassle. But if all the children components of a client component that has use client directive, will be treated as a client component. Check the code and image below.
Image description

'use client'

// client component
import ClientComponentChild from './clientComponentChild'

export default function ClientComponent() {
    return (
        <div>
            <h1>Client Component</h1>
            <ClientComponentChild />
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode
// client component child
// No need to add `use client` directive
import { useState } from 'react'

export default function ClientComponentChild() {
    return (
        <div>
            <h1>Client Component Child</h1>
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

3. No server component inside the client component

You simply can't use server components inside client components. It will throw an error. Check the code below.

Image description

// client component
import ServerComponent from './serverComponent' // Error

export default function ClientComponent() {
    return (
        <div>
            <h1>Client Component</h1>
            <ServerComponent />
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode
// server component
import fs from 'fs' // server side module

export default function ServerComponent() {
    return (
        <div>
            <h1>Server Component</h1>
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

You will see the error below.

Image description

It is trying to import the fs module on the client side but it is not available on the client side.

4. Pass server component as a prop to the client component

Even though you can't use server components inside client components, you can pass server components as a prop to client components.

This is useful when you want to have some kind of Provider like React Context or Redux Provider. You want to wrap the app by the provider. You can do that in the main layout file. But context or any kind of state is not available on server components.

So, You have two options:

  1. Make the main layout file a client component.
  2. Or you can pass the server component as a prop to the client component and wrap the app with the provider. Check the code below.
'use client' // client component
// Provider.jsx

import React, { createContext, useState } from 'react'

// Create a context
const AppContext = createContext()

// Create a provider component
const AppProvider = ({ children }) => {
    // Define state or any other data you want to share
    const [user, setUser] = useState(null)

    // Define any functions or methods you want to expose
    const loginUser = userData => {
        setUser(userData)
    }

    const logoutUser = () => {
        setUser(null)
    }

    // Define the context value
    const contextValue = {
        user,
        loginUser,
        logoutUser,
    }

    // Return the provider with its value and wrapped children
    return (
        <AppContext.Provider value={contextValue}>{children}</AppContext.Provider>
    )
}

export default AppProvider
Enter fullscreen mode Exit fullscreen mode
// layout.jsx
import AppProvider from './Provider'

export default function RootLayout({ children }) {
    return (
        <html lang='en'>
            <body className={inter.className}>
                {/* Client component as parent of server component */}
                <AppProvider>{children}</AppProvider>
            </body>
        </html>
    )
}
Enter fullscreen mode Exit fullscreen mode

Even though AppProvider is a client component, and it is wrapping the entire app, all of your components will be still server components by default. However you can't consume the context in server components. Because the server component doesn't support any state.

5. keep the server-only code to server components

Sometimes, you write code for server only. Like fetching data from the database, reading files, etc. You should keep those codes in server components. But accidentally you might use those codes in client components. For example:

export async function getData() {
    const url = 'https://jsonplaceholder.typicode.com/todos/1'

    const res = await fetch(url, {
        headers: {
            authorization: process.env.API_KEY,
        },
    })

    return res.json()
}
Enter fullscreen mode Exit fullscreen mode

This getData function can be used both on the server and client side. But it should be used only on the server side because you are accessing a secret environment variable. To prevent this, you can use a package called server-only. You just need to import the package at the top of the file.

import 'server-only'

export async function getData() {
    const url = 'https://jsonplaceholder.typicode.com/todos/1'

    const res = await fetch(url, {
        headers: {
            authorization: process.env.API_KEY,
        },
    })

    return res.json()
}
Enter fullscreen mode Exit fullscreen mode

6. Pass server data to the client component

Sometimes you need to pass some data from the server to client component. For instance, some data that you have fetched from the database and you want the client component to have that data. You can pass the data as props. However, the data should be Serializable.

You can pass string, number, boolean, array, object, etc. Check the documentation for full list.
You can't pass functions(that are not server action), jsx etc.

// server component

export default async function ServerComponent() {
    const data = await getData() // getting some data but could be anything

    return (
        <div>
            <h1>Server Component</h1>
            <ClientComponent
                data={data} // Works
                callback={() => console.log('hello')} // Won't work
            />
            />
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

7. Share data between server components using a single request

Suppose you are fetching some data on one server component and you want to use that data in another server component. You don't need to send multiple requests. If you use the fetch function then react will automatically cache the data. So, only one request will be sent.

async function ServerComponent1() {
    // Fetching and caching the data
    const data = await fetch('https://api.example.com/data')
    return (
        <div>
            <h1>Server Component 1</h1>
            <ServerComponent2 data={data} />
        </div>
    )
}

async function ServerComponent2() {
    // Use the cached data
    const data = await fetch('https://api.example.com/data')
    return (
        <div>
            <h1>Server Component 2</h1>
            <p>{data}</p>
        </div>
    )
}

export default function App() {
    return (
        <div>
            <ServerComponent1 />
            <ServerComponent2 /> {/* Won't send another request */}
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

8. No browser API in server components

You can't use browser API in server components. You can't use window, document, localStorage etc. It will throw an error. Check the code below.

// server component
export default function ServerComponent() {
    return (
        <div>
            <h1>Server Component</h1>
            <p>{window.location.href}</p> // Error
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

9. Third-party packages that use browser API in server components

Since server components are new a lot of third-party packages might not work with server components. Because they haven't added 'use client' directive to their code. So, you will get an error. Check the code below.

// server component
// Some third party component that uses useState
import { ExComponent } from 'some-package'

export default function ServerComponent() {
    return (
        <div>
            <h1>Server Component</h1>
            <ExComponent /> {/* Error */}
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

If you use it within a client component and then use it on the server component then it will work.

'use client'
// client component
import { ExComponent } from 'some-package'

export default function ClientComponent() {
    return (
        <div>
            <h1>Client Component</h1>
            <ExComponent /> {/* Works */}
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

Or you can wrap the third-party component with a client component like below.

'use client'
import { ExComponent } from 'some-package'

export default ExComponent
Enter fullscreen mode Exit fullscreen mode

10. You should move the client component down the tree

Server components are cheap and faster to send to the browser. Since they don't have javascript. And client components are the opposite.
Like I mentioned before, once you make a client component, all of its children will be treated as client components. Since they will have javascript, it will have a bigger bundle size. So, if you put the client components at the top of the tree, it will increase bundle size.

Good:

Image description

Bad(but still depends on the situation):

Image description

That's it. I hope you learned something new. If you have any questions, feel free to ask in the comment section below.
Please subscribe to my YouTube, it really motivates me to create more content.

Top comments (2)

Collapse
 
borzoomv profile image
Borzoo Moazami

Learnt a lot, thank you πŸ™πŸ»

Collapse
 
thatanjan profile image
Anjan Shomodder

I'm glad to hear that you found the information helpful.