With the new Next.JS 13 App Router, arrived React Server Components. When following the best practices of this new version, we have to think server first but it can be easy to get tangled in server and client components when working with third-party libraries.
What is a React Server Component
A React Server Component is rendered server-side. The generated HTML is then sent to the browser to hydrate the updated part of your app.
PHP Developers will recognize this pattern!
There are a lot of benefits of using React Server Components:
- Drastically decrease the amount of Javascript sent to the browser
- React Server Components can be entirely cached
- Ability to fetch data directly from your component
The problem with React Server Components
If you try to use React Hooks (useState
, useEffects
, etc) or simply listen to an event (onClick
, onChange
). You will receive an error similar to this one:
You're importing a component that needs useState. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by default.
This is completely normal as the server is not able to listen to events happening in the browse.
To fix the issue you have to add 'use client';
on top of your file to tell Next.JS that everything inside the file is "client-side".
But watchout! Do not add 'use client';
everywhere as if it was a patch. The idea is to keep most of your components server side.
The problem with UI Libraries
You just installed your favorite UI library, and start using it:
// src/app/page.tsx
import { Button } from "@chakra-ui/react";
export default async function Page() {
const post = await fetchPost();
return <Button>{post.title}</Button>;
}
You received the same error You're importing a component that needs useState
but you do not want to change your page into a Client Component as you need to fetch data.
So you decide to create a "component-in-the-middle" that will tell Next.JS that the Button imported from @chakra-ui/react
is a Client Component:
// src/app/Button.tsx
"use client";
import { Button as ChakraButton } from "@chakra-ui/react";
export default function Button({ children }: { children?: React.ReactNode }) {
return <ChakraButton>{children}</ChakraButton>;
}
// src/app/page.tsx
import Button from "./Button";
export default async function Page() {
const post = await fetchPost();
return <Button>{post.title}</Button>;
}
It works! But for this case we only forwarded the children
, we want every props. Let's do it with a much simpler solution:
// src/app/Button.tsx
"use client";
import { Button as ChakraButton } from "@chakra-ui/react";
const Button = ChakraButton;
export default Button;
But it means that we have to do the same thing for every single component that we use, is there any better solution?
"use client";
export * from "@chakra-ui/react";
import { Button } from "./ChakraUI";
export default function Page() {
const post = await fetchPost();
return <Button>{post.title}</Button>;
}
What about "dot notation" like framer-motion
Unfortunately there is no magic solution currently.
You have to export each component:
"use client";
import { motion } from "framer-motion";
export const MotionDiv = motion.div;
export const MotionSpan = motion.span;
Conclusion
Properly using React Server Component can drastically increase your app performances but it is tricky to keep a good component tree without getting tangled between server and client.
Currently most of the libraries does not yet export using 'use client';
directive so you still have to add it ourself.
Top comments (0)