Skip to content

Commit

Permalink
add when-not-to-use-use-client-and-use-server
Browse files Browse the repository at this point in the history
Signed-off-by: Vu Van Dung <[email protected]>
  • Loading branch information
joulev committed Dec 18, 2023
1 parent d755b99 commit 53e93df
Show file tree
Hide file tree
Showing 10 changed files with 387 additions and 25 deletions.
7 changes: 6 additions & 1 deletion mdx-components.ts → mdx-components.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import type { MDXComponents } from "mdx/types";

import * as customComponents from "~/components/blogs";
import { Tweet } from "~/components/ui/tweet";

export function useMDXComponents(components: MDXComponents): MDXComponents {
return { ...components, ...customComponents };
return {
...components,
...customComponents,
Tweet: ({ id }: { id: string }) => <Tweet id={id} className="my-8" />,
};
}
4 changes: 2 additions & 2 deletions src/app/(public)/blogs/(posts)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ export default function Layout({ children }: LayoutProps) {
</div>
</div>
<div className="flex flex-col divide-y divide-separator blog-lg:flex-row blog-lg:divide-x blog-lg:divide-y-0">
<article className="prose max-w-none px-[--p] py-12 [--p:24px] blog-lg:[--p:48px] [&>*]:mx-auto [&>*]:max-w-prose">
<article className="prose max-w-none px-[--p] py-12 text-[#ffffffba] [--p:24px] blog-lg:[--p:48px] [&>*]:mx-auto [&>*]:max-w-prose">
{children}
<div>
<LinkButton href="/blogs" className="mt-6 no-underline">
<LinkButton href="/blogs" className="mt-6 text-text-primary no-underline">
<ChevronLeft /> Back to blogs
</LinkButton>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { makeMetadata } from "~/lib/blogs/utils";

export const metadata = makeMetadata("when-not-to-use-use-client-and-use-server");

## TL;DR

1. You should only use `"use client"` for client components that you intend to import to server components directly. For other client components, it's best to not use this directive.

2. Use `"use server"` to mark [server actions](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations) only. Do NOT use it to mark server components.

---

Since the release of the [`app` router](https://nextjs.org/docs/app) in Next.js 13, and then the release of [server actions](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations), React users have been able to use two brand new directives: `"use client"` and `"use server"`.

It's hard to "under-use" these directives because whenever they are needed, they are _required_. So there are no cases where you should use them if not using them would still work. The reverse is not true, though – I've seen a lot of cases where these two directives are _overused_: people are using them where they should not be used.

This article will explain my take, as a [Next.js contributor](https://github.com/vercel/next.js/commits?author=joulev) and a [community helper on the Next.js Discord server](https://nextjs-forum.com), on when you should, and should not, use these shiny new directives.

## `"use client"`

### What is it?

React recently announced a new feature called [server components](https://react.dev/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023#react-server-components), and React developers as of now can use it in a number of frameworks, most notably the Next.js App Router.

Server components are React components that only run on the server, and as far as the client-side code is concerned, these components are just static HTML. As a result, server components cannot be interactive, and they cannot use any client-side APIs. That means for 99% of apps, there need to be a way to define components that run on the client and behave like normal (old) React components. That's where `"use client"` comes in. It marks components, called _client components_, that should also be run and be interactive on the client.

Basically, it is a network boundary where the server-side code ends and the client-side code begins.

### When should you use it?

When you have to. So if you use a framework that doesn't support React server components, then you can simply forget about this directive. But if you use, say, the Next.js App Router, then use this directive whenever you need a part of a page (could be a small button, but could also be a big WYSIWYG text editor) to be interactive.

It's hard to go wrong on this front, because if you need to use this directive, you have to use it else there will be compilation errors.

### When should you _not_ use it?

This is where it gets interesting. To put it simply, **you should only use `"use client"` for client components that you intend to import to server components as well. For other client components, it's best to not use this directive.** Read on to know why.

First, we need to remember when a component is considered a client component. It is [when it has `"use client"` at the top, or when it is imported to another client component](https://nextjs-faq.com/when-client-component-in-app-router). Hence, if you already have a client component, things _imported_ to it are already client components and you don't need `"use client"` – so we know the rule above is safe to use.

But is it a good idea?

Since client components marked with `"use client"` _can_ be imported directly to server components, I'd say these components should have the props as valid "entry points" for the network boundary: props should be serialisable and all functions passed to these props should be exclusively [server actions](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations). Consider this example:

```tsx
export function Foo({ onClick }: { onClick: () => void }) {
return (
<button type="button" onClick={onClick}>
Click Me
</button>
);
}
```

This component requires `onClick` which is a client-side event callback, so it cannot be imported directly to a server component – only other client components should consume it. But if you mark it with `"use client"`, that would notate that it is actually possible to directly import it to a server component (since you have `"use client"` anyway, regardless of where you import this function it is always a client component). That's not good.

`Foo` is not a _valid entry point_ between the server-side parts and the client-side parts of your app. Hence it should not be marked with `"use client"`.

If you set up the [TypeScript plugin](https://nextjs.org/docs/app/building-your-application/configuring/typescript#typescript-plugin) correctly, the function `Foo` above will actually give you a warning if you add `"use client"` to the top:

> Props must be serializable for components in the `"use client"` entry file, `onClick` is invalid.
which basically explains my point above.

## `"use server"`

### What is it?

To quote the [React documentation](https://react.dev/reference/react/use-server),

> `"use server"` marks server-side functions that can be called from client-side code.
### When should you use it?

Once again, it's hard to go wrong here. Server actions (marked by the `"use server"` directive) are basically a fancy way for you to do type-safe API calls that integrate tightly with the React router. So if you want to make an API call, where you want to run a certain function on the server (e.g. a database query, a call to a secret API with a secret API key, etc.) while having that function _importable_ to client-side logic as a regular `async` function, then time to use `"use server"`.

### When should you _not_ use it?

Contrary to what some people might think, `"use client"` and `"use server"` are not the opposite of each other. `"use server"` is not the directive to mark server components. In fact, the two directives are almost not related to each other at all.

**Use `"use server"` to mark server actions only. Do NOT use it to mark server components.**

First (and more obvious) reason is that, a `"use server"` file can only export asynchronous functions (because server actions must be asynchronous). So if you have this

```tsx
"use server";

export default async function Page() {
return <div>Hello world</div>;
}

export const revalidate = 10;
```

you will get the following error:

```
× Only async functions are allowed to be exported in a "use server" file.
╭─[/Users/joulev/dev/www/debug/app/page.tsx:4:1]
4 │ return <div>Hello world</div>;
5 │ }
6 │
7 │ export const revalidate = 10;
· ─────────────────────────────
╰────
```

But it's worse than just this and is actually a security vulnerability.

Each server function (marked with `"use server"`) is given a server-side address that the client can call to trigger the function. For example, in this code

```tsx
<button
onClick={async () => {
await updateUser(); // a server function
}}
>
Update
</button>
```

what actually happens on the client-side is that there is a `POST` request to the same URL with the _server address_ of `updateUser` added to the request payload. The server then reads that address and triggers the function located at that address. So basically, all server functions have specific addresses that anyone can use to tell your server to trigger the action.

So say you work at YouTube and want to display the dislike count. You make a server component for that. You only want the video creator to view the component, but since you already implemented authentication in the dashboard page, you decide you don't need to implement authentication in this component. Logically, since only the creator can view the dashboard page, that would mean the dislike count component is only viewable by the creator right?

But if you mark that component with `"use server"`, it will then actually become a server action and will have a server address. Then, once an attacker somehow knows this address, they can run the component and get the (protected) content (in this case, the dislike count) without being the video creator.

While I would fully support that YouTube somehow bring the dislike count back, if you make this vulnerability you would probably get fired.

To conclude, I think it's worth it to repeat the rule: **Use `"use server"` to mark server actions only. Do NOT use it to mark server components.**

## The two directives with very bad names

As you can see, the opposite nature of the "server" and "client" words here have made the two directives very confusing. Far too many people are confused that the two directives are the exact opposite of each other, but it turns out they are not even related to each other. `"use client"` components [getting run and prerendered on the _server_](https://github.com/reactwg/server-components/discussions/4) also doesn't help with this confusion.

Theo puts it nicely:

<Tweet id="1736177027433316701" />

But well, things are already set in stone. It's very hard to rename them now. So I guess we just have to live with these confusing names. And not use them where they should not be used.
9 changes: 8 additions & 1 deletion src/app/(public)/blogs/meta.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
export const meta = [
export const meta: { slug: string; title: string; description: string; postedDate: string }[] = [
{
slug: "handling-svg-icon-path-opacity-overlap",
title: "Handling SVG Icon Path Opacity Overlap",
description:
"How to handle SVG icons with semi-transparent colours having path opacity overlapping in intersections points",
postedDate: "2023-12-12",
},
{
slug: "when-not-to-use-use-client-and-use-server",
title: 'When NOT to Use "use\u00A0client" and "use\u00A0server"',
description:
"It's surprisingly common to see people overusing these directives. This article explains when I would use or NOT use them.",
postedDate: "2023-12-18",
},
];
44 changes: 23 additions & 21 deletions src/app/(public)/blogs/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,28 +18,30 @@ export default function Page() {
</div>
</div>
<div className="flex w-full flex-col divide-y divide-separator">
{meta.map(post => {
const publishedTime = new Date(post.postedDate);
return (
<PostCard slug={post.slug} key={post.slug}>
<div className="mx-auto flex w-full max-w-xl flex-col gap-3 p-6">
<h2 className="text-xl font-bold md:text-2xl">
<Balancer>{post.title}</Balancer>
</h2>
<p className="text-text-secondary">{post.description}</p>
<div className="text-sm text-text-tertiary">
Posted{" "}
<time
dateTime={publishedTime.toISOString()}
title={publishedTime.toISOString()}
>
{formatTime(publishedTime)}
</time>
{meta
.sort((a, b) => new Date(b.postedDate).valueOf() - new Date(a.postedDate).valueOf())
.map(post => {
const publishedTime = new Date(post.postedDate);
return (
<PostCard slug={post.slug} key={post.slug}>
<div className="mx-auto flex w-full max-w-xl flex-col gap-3 p-6">
<h2 className="text-lg font-bold md:text-xl">
<Balancer>{post.title}</Balancer>
</h2>
<p className="text-text-secondary">{post.description}</p>
<div className="text-sm text-text-tertiary">
Posted{" "}
<time
dateTime={publishedTime.toISOString()}
title={publishedTime.toISOString()}
>
{formatTime(publishedTime)}
</time>
</div>
</div>
</div>
</PostCard>
);
})}
</PostCard>
);
})}
</div>
</Card>
</main>
Expand Down
14 changes: 14 additions & 0 deletions src/app/(public)/glui/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ import {
SidebarSectionItems,
} from "~/components/ui/sidebar";
import { Slider } from "~/components/ui/slider";
import { Tweet } from "~/components/ui/tweet";
import { cn } from "~/lib/cn";

import {
Expand Down Expand Up @@ -550,6 +551,18 @@ function SliderShowcase() {
);
}

function TweetShowcase() {
return (
<>
<hr />
<section className="flex flex-col gap-6">
<h2 className="text-xl font-bold">Tweet</h2>
<Tweet id="1736177027433316701" />
</section>
</>
);
}

export default function Page() {
return (
<main className="container flex max-w-screen-md flex-col gap-9">
Expand Down Expand Up @@ -594,6 +607,7 @@ export default function Page() {
<SidebarShowcase />
<SliderShowcase />
<TextAreaShowcase />
<TweetShowcase />
</main>
);
}
Expand Down
16 changes: 16 additions & 0 deletions src/components/blogs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,19 @@ export function pre(props: React.ComponentPropsWithoutRef<"pre">) {
</ScrollArea>
);
}

export function code({ className, ...props }: React.ComponentPropsWithoutRef<"code">) {
return (
<code
{...props}
className={cn(
"rounded-[0.5em] bg-bg-darker p-1 [font-weight:inherit] before:content-[''] after:content-[''] [pre_&]:rounded-none [pre_&]:bg-transparent [pre_&]:p-0",
className,
)}
/>
);
}

export function hr(props: React.ComponentPropsWithoutRef<"hr">) {
return <hr {...props} className="!-mx-[--p] !max-w-none" />;
}
17 changes: 17 additions & 0 deletions src/components/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,11 @@ export const GitHub = createIcon(
<path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85m0 0v4m0-4c-4.51 2-5-2-7-2" />,
);

export const Heart = createIcon(
"Heart",
<path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7 7-7Z" />,
);

export const Link = createIcon(
"Link",
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />,
Expand Down Expand Up @@ -164,6 +169,11 @@ export const MessageSquare = createIcon(
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v10Z" />,
);

export const MessageCircle = createIcon(
"MessageCircle",
<path d="M7.9 20A9 9 0 1 0 4 16.1L2 22l5.9-2Z" />,
);

export const Move3d = createIcon(
"Move3d",
<path d="M5 3v16M5 3 2 6m3-3 3 3M5 19h16M5 19l6-6m10 6-3-3m3 3-3 3" />,
Expand Down Expand Up @@ -208,6 +218,8 @@ export const Repeat = createIcon(
<path d="m17 2 4 4m0 0-4 4m4-4H7a4 4 0 0 0-4 4v1m4 11-4-4m0 0 4-4m-4 4h14a4 4 0 0 0 4-4v-1" />,
);

export const Reply = createIcon("Reply", <path d="m9 17-5-5m0 0 5-5m-5 5h12a4 4 0 0 1 4 4v2" />);

export const RotateCcw = createIcon(
"RotateCcw",
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8m0 0V3m0 5h5" />,
Expand Down Expand Up @@ -243,6 +255,11 @@ export const Trash = createIcon(
<path d="M3 6h18m-2 0v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6m3 0V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />,
);

export const Twitter = createIcon(
"Twitter",
<path d="M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2Z" />,
);

export const Tv2 = createIcon(
"Tv2",
<path d="M7 21h10M4 3h16a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2Z" />,
Expand Down
Loading

0 comments on commit 53e93df

Please sign in to comment.