Server Component và Client Component trong Nextjs 13

By Thái Nguyễn
Picture of the author
Published on
image alt

Server Component và Client Component là một số thuật ngữ mới xuất hiện trong React 18. Trong bài viết này, chúng ta sẽ cùng tìm hiểu Server Component và Client Component được sử dụng như thế nào trong nextjs 13, cách hoạt động cũng như những điểm mạnh của chúng.

Table Of Content

Server Component

Server components là component được fetch và render phía server. Chúng sẽ không tồn tại trong client-side bundle.

Ưu điểm của Server component là gì ?

  • Tận dụng được cở sở hạ tầng phía Server, gần với cơ sở dữ liệu hơn nên độ trễ cũng thấp hơn => tăng performance khi xử lý dữ liệu.

  • Việc render ở phía Server cũng làm giảm JS Bundle Size gửi về phía Client. => load page nhanh hơn.

By default: các component của nextjs 13 đều là Server Component


Client Component

Client Components cho phép bạn thêm các tương tác ( click vào button, nhập input, ... ) tại client-site. Trong nextjs, Client Components sẽ được pre-render tại phía Server, sau đó thực hiện quá trình Hydrate tại phía Client.

Hydrate: Trong Next.js, khi một trang được tải lần đầu, trình duyệt sẽ tải mã JavaScript cần thiết để render ứng dụng React trên máy Client. Khi mã JavaScript này được tải và thực thi, React sẽ tiến hành quá trình "hydration" (thấm nhuần) các thành phần đã được render trước đó trên phía Client.

"use client" directive

"use client" là cú pháp để đánh dấu component là Client Component.

Tất cả các component con được import trong 1 component đã sử dụng "use client" directive thì sẽ được xem là Client Component.

Ví dụ:

app/counter/page.tsx

"use client";

import { useState } from "react";

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div className="min-h-screen">
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

Sau khi được đánh dấu là Client Component, Page Counter sẽ được bundle và gửi về phía Client. Chúng ta có thể check trong webpack bundle:

demo client component

Trường hợp nào nên sử dụng Client Component và Server Component ?

Server Component:

  • Fetch data.
  • Truy cập backend resources (directly)
  • Giữ các sensitive data ở phía server (access tokens, API keys, etc)
  • Giữ các dependencies có size lớn phía server / giảm client-side JavaScript

Client Component:

  • Thêm tương tác và event listeners (onClick(), onChange(), etc)
  • Sử dụng State và Lifecycle Effects (useState(), useReducer(), useEffect(), etc)
  • Sư dụng browser-only APIs
  • Sử dụng custom hooks phụ thuộc vào state, effects, hoặc browser-only APIs
  • Sử dụng React Class components

Một số use case thường gặp

1. Nesting Server Components inside Client Components

Tất cả các component con được import trong 1 component đã sử dụng "use client" directive thì sẽ được xem là Client Component.

Để có thể sử dụng được Server Components trong Client Component chúng ta có thể truyền thông qua props:

// You can pass a Server Component as a child or prop of a
// Client Component.
import ExampleClientComponent from './example-client-component'
// Server Component.
import ExampleServerComponent from './example-server-component'

// Pages in Next.js are Server Components by default
export default function Page() {
  return (
    <ExampleClientComponent>
      <ExampleServerComponent />
    </ExampleClientComponent>
  )
}

Use case này giải quyết được việc sử dụng Context để lưu global state hay import các 3rd party providers.

Bạn có thể tạo 1 file chung gọi là GlobalProviders để render tất cả các providers. Sau đó import GlobalProviders vào trong RootLayout.

2. Làm sao để 1 Component chỉ cho phép sử dụng trong Server Component?

Chúng ta có thể sử dụng server-only package để thêm ràng buộc.

server-only package sẽ thông báo lỗi lúc Build-time nếu import component đó trong 1 Client Component.

Cách sử dụng server-only package:

  • install package
npm install server-only
  • import package
import 'server-only'
 
export async function getData() {
  const res = await fetch('https://external-service.com/data', {
    headers: {
      authorization: process.env.API_KEY,
    },
  })
 
  return res.json()
}
3. Shared components giữa Client và Server components

Để sử dụng được component chung thì chúng ta không được khai báo directive use client hay import server-only package để đánh dấu component đó là Client hay Server components.

Dưới đây là 1 ví dụ về một Common Component trong Nextjs 13:

Date component

//date component is a common shared component
import { parseISO, format } from 'date-fns';

export default function Date({ dateString, formatType = 'LLLL dd, yyyy' }) {
  const date = parseISO(dateString);
  return (
    <time className="text-sm text-gray-600" dateTime={dateString}>
      {format(date, formatType)}
    </time>
  );
}

Nếu Date component được import trong 1 Client Component thì nó sẽ nằm trong client-side bundle. Ngược lại, nếu được import trong 1 Server Component nó sẽ được render ở server-site và không nằm trong client-site bundle.

4. Làm cách nào để filter data trong Server Component bởi action từ phía người dùng?

VD: Có 1 màn hình hiển thị danh sách các movies, và 1 ô input search tên các bộ phim. Danh sách các bộ phim sẽ được filter theo ô input search khi người dùng nhập vào:

ảnh minh hoạ

Thông thường chúng ta sẽ sử dụng Route Handle hoặc Server Action của Nextjs 13 trong trường hợp này.

Tuy nhiên, nếu bạn vẫn muốn hiển thị dữ liệu sau khi filter thì có thể pass thông qua params URL như sau:

"use client";

import React from "react";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useRouter } from "next/navigation";

const SearchBar = () => {
  const navigate = useRouter();

  return (
    <div className="flex flex-row items-center justify-between">
      <Label>
        <h5>Movies</h5>
      </Label>
      <Input
        className="w-[500px]"
        onChange={(event) => {
          if (event?.target?.value) {
            navigate.push("/movies?search=" + event?.target?.value);
          } else {
            navigate.push("/movies");
          }
        }}
        type="text"
      />
    </div>
  );
};

export default SearchBar;

Sử dụng param URL để filter data trong Server Component:

const Movies = async ({
  searchParams,
}: {
  searchParams: { [key: string]: string | string[] | undefined };
}) => {
  const search =
    typeof searchParams.search === "string" ? searchParams?.search : undefined;

  const data = await getMovies(search);

  return (
    <div className="min-h-screen w-[800px] m-auto mt-10">
      <SearchBar />
      <div className="grid grid-cols-4 gap-8 mt-10">
        {data?.map((data: any, index: any) => {
          return (
            <div key={index} className="flex flex-col justify-start">
              <div className="h-[280px]">
                <Image
                  src={data?.thumbnail}
                  className="w-full max-h-[280px]"
                  width={data?.thumbnail_width ?? 100}
                  height={data?.thumbnail_height ?? 100}
                  alt="xxx"
                />
              </div>
              <p>{`${data?.title} - ${data?.year}`}</p>
            </div>
          );
        })}
      </div>
    </div>
  );
};

export default Movies;

Resources

https://nextjs.org/docs/app/building-your-application/rendering