Essence
In the context of React Server Component, components we have been used are called Client Components. There are some rules to use these components. I picked up important ones.
- May not use state, rendering lifecycle, browser-only APIs in Server Components
- May not import Server Components in Client Components
These are not all rules, but they tell us important essence of this React new API. First, Server Components are evaluated on a server, as its name suggests. If you use Next.js standalone mode, you run server with node server.js command. Yes, the environment is node, not browser. So you can't use browser-only APIs.
Second, React is a UI library. It shows value when your users interact with your UI. Can they interact with your server? No. They interact with your UI to input text, click button to send it, and so on. Such a state management and interactivity are handled by web browser. Server Components evaluated on a server must be sent to web browser. So state and rendering lifecycle are not allowed in Server Components.
Third, Client Components exists in web browser and manages state and handles interaction. What happens if you update state inside client components and pass it to Server Components in it? Server Components can access to database and backend microservices because it runs on a server. But, in this situation, Server Components are already delivered to web browser. Web browser or server, there is contradiction. In my understanding, this is why Server Components cannot be imported in Client Components. This doesn't mean you can't use Server Components inside Client Components. Check it out in next chapter.
Organize UI
We have three kinds of components to organize UI, Server Components, Client Components, and Native Components - like div, h1 and p. Each component has its own role. Server Components can access data in backend, Client Components will manage state and handle interaction and Native Components is native.... It's important to know how to split these components and organize them.
Server/Client Components are ultimately transformed to Native Components. We think about two components, Server Components and Client Components. In my thought, using Server Components as possible is better. If you need state, interaction or browser-only APIs, use Client Components. We can reduce bundle size and requests to display your data.
Assume we can split components into Server Components and Client Components. Next we think about how to organize them. I wrote in previous chapter, Server Components cannot be imported in Client Components. Don't worry this doesn't mean you can't use Server Components inside Client Components.
1<ServerComponent1>2 <ClientComponent>3 <ServerComponent2>4 </ServerComponent2>5 </ClientComponent>6</ServerComponent1>
You can write like this. ServerComponent2 is used in ClientComponent, and ClientComponent is inside ServerComponent1.
Quoting from RFC, "Server and Client components can be nested and interleaved in the tree at any level.".
This design is good. When you make new component, you can decide whether it is Server Component or Client Component without thinking about entire application architecture. After splitting components, you just combine them.
In more detail this RFC also says "However, a Server Component may pass another Server Component as a child to a Client Component".
You cannot import Server Components in Client Components, but you can pass Server Components as a child to a Client Component in Server Components.
Following this rule, we should avoid using Client Components for entire page, page.tsx in Next.js approuter, I think.
Server/Client Boundary - Make Types Flexible for Client Components
RFC also says
You only need to add this directive to the Client Components that you import from Server Components. If you don't add the directive, by default all components will be treated as Shared — imports from the Server are treated as Server Components, and imports from the Client are treated as Client Components
The important part of this is Client Components may be imported from Server Components and Client Components. In the case of importing from Server Components, the props are serialized..
Think about following situation.
1export class Person {2 constructor(public name: string) {}34 greet() {5 return `Hello, my name is ${this.name}`6 }7}89export function Client0({person}: {person: Person}) {10 return <div>{person.greet()}</div>11}
Client0 component takes Person instance as a prop and shows his greeting.
First, we use it inside Client Component.
1"use client"23export function Client1() {4 const person = new Person('John')5 return <div><Client0 person={person}/></div>6}
This works correctly. Next, we use it inside Server Component.
1export function Server1() {2 const person = new Person('John')3 return <div><Client0 person={person}/></div>4}
This throws an error. In my environment, error message says TypeError: person.greet is not a function. This is because person is serialized in Server Component. Client0 takes person prop as a pure object, greet() method is stripped away.
To avoid this, I introduce two methods.
- Stop passing class object as props to Client Components from Server Components
- Make types of props flexible for Client Components
The former is simple, but you have to be very careful, (some of the objects can be serialized and deserialized). The latter one says broaden types of props as following.
1// Update type of props2export function Client0({person}: {person: Person | {name: string}}) {3 return (4 <div>5 {person instanceof Person ? person.greet() : person.name}6 </div>7 )8}
In this situation, you must validate passed person variable whether it is Person instance or not to access to greet() method. If you forget to validate it, you will see a compile error. This is safer than runtime error.
Be careful especially when you use some objects related to Date.