In this blog, I will talk about 10 things you should know about component composition in Next.js. You will learn:
- Different composition patterns with Server and client components
- Pattens that don't work
- How to use third-party packages that use browser api
- When to use server and client components
- How to share data between components
- 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:
- React uses a tree structure to render components like the actual DOM. See the image below.
- All the components in nextjs are now server components.
- You can make a client component by adding
use client
directive at the top of the file. - It is obvious that you can nest server components inside server components and client components inside client components.
Let's learn the 10 things you should know about component composition in Next.js.
1. Client component inside server component
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>
)
}
// 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>
)
}
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.
'use client'
// client component
import ClientComponentChild from './clientComponentChild'
export default function ClientComponent() {
return (
<div>
<h1>Client Component</h1>
<ClientComponentChild />
</div>
)
}
// 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>
)
}
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.
// client component
import ServerComponent from './serverComponent' // Error
export default function ClientComponent() {
return (
<div>
<h1>Client Component</h1>
<ServerComponent />
</div>
)
}
// server component
import fs from 'fs' // server side module
export default function ServerComponent() {
return (
<div>
<h1>Server Component</h1>
</div>
)
}
You will see the error below.
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:
- Make the main layout file a client component.
- 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
// 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>
)
}
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()
}
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()
}
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>
)
}
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>
)
}
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>
)
}
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>
)
}
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>
)
}
Or you can wrap the third-party component with a client component like below.
'use client'
import { ExComponent } from 'some-package'
export default ExComponent
- 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:
Bad(but still depends on the situation):
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)
Learnt a lot, thank you 🙏🏻
I'm glad to hear that you found the information helpful.