Kyle Mo

@React 小聚

Exploring

React Server Components

From Next.js 13

Kyle Mo

Software Engineer (FE, JS Infra)

Agenda

Basic Introduction Of React Server Components

How Next.js v13 Integrate Server Components ?

Recap & Conclusions

App Directory, Server & Client Components, Data Fetching, Streaming & Suspense, Limits, Future Roadmap...

Basic INTRODUCTION OF REACT SERVER COMPONENTS

Server Components (.server.js(/ts/jsx/tsx))

Client Components (.client.js(/ts/jsx/tsx))

Share Components (.js(/ts/jsx/tsx))

Types Of Components

Hybrid Rendering

Server Component

Client Component

How The Hybrid Rendering Work ?

  1. Render Root
  2. Request For Server Components
  3. React Runtime Rendering
function Root() {
    const [data, setData] = useState({});
    
    // 向伺服器發送請求
    const componentResponse = useServerResponse(data);
    return (
        <DataContext.Provider value={[data, setData]}> 
            componentResponse.render();
        </DataContext.Provider>
    );
}

Request For Server Components

Server Component Root

Server

Component 1

Client

Component 1

Server

Component 2

Client

Component 2

Request For Server Components

M1:{"id":"./src/SearchField.client.js","chunks":["client5"],"name":""}
M2:{"id":"./src/EditButton.client.js","chunks":["client1"],"name":""}
S3:"react.suspense"
M4:{"id":"./src/SidebarNote.client.js","chunks":["client6"],"name":""}
J0:["$","div",null,{"className":"main","children":[["$","section",null,{"className":"col sidebar","children":[["$","section",null,{"className":"sidebar-header","children":[["$","img",null,{"className":"logo","src":"logo.svg","width":"22px","height":"20px","alt":"","role":"presentation"}],["$","strong",null,{"children":"React Notes"}]]}],["$","section",null,{"className":"sidebar-menu","role":"menubar","children":[["$","@1",null,{}],["$","@2",null,{"noteId":null,"children":"New"}]]}],["$","nav",null,{"children":["$","$3",null,{"fallback":["$","div",null,{"children":["$","ul",null,{"className":"notes-list skeleton-container","children":[["$","li",null,{"className":"v-stack","children":["$","div",null,{"className":"sidebar-note-list-item skeleton","style":{"height":"5em"}}]}],["$","li",null,{"className":"v-stack","children":["$","div",null,{"className":"sidebar-note-list-item skeleton","style":{"height":"5em"}}]}],["$","li",null,{"className":"v-stack","children":["$","div",null,{"className":"sidebar-note-list-item skeleton","style":{"height":"5em"}}]}]]}]}],"children":["$","ul",null,{"className":"notes-list","children":[["$","li","0",{"children":["$","@4",null,{"id":0,"title":"Meeting Notes","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"This is an example note. It contains Markdown!"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"Meeting Notes"}],["$","small",null,{"children":"10:04 PM"}]]}]}]}],["$","li","1",{"children":["$","@4",null,{"id":1,"title":"Make a thing","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"It's very easy to make some words bold and other words italic with Markdown. You can even link to React's..."}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"Make a thing"}],["$","small",null,{"children":"10:04 PM"}]]}]}]}],["$","li","2",{"children":["$","@4",null,{"id":2,"title":"A note with a very long title because sometimes you need more words","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"You can write all kinds of amazing notes in this app! These note live on the server in the notes..."}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"A note with a very long title because sometimes you need more words"}],["$","small",null,{"children":"10:04 PM"}]]}]}]}],["$","li","3",{"children":["$","@4",null,{"id":3,"title":"I wrote this note today","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"It was an excellent note."}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"I wrote this note today"}],["$","small",null,{"children":"10:04 PM"}]]}]}]}]]}]}]}]]}],["$","section","null",{"className":"col note-viewer","children":["$","$3",null,{"fallback":["$","div",null,{"className":"note skeleton-container","role":"progressbar","aria-busy":"true","children":[["$","div",null,{"className":"note-header","children":[["$","div",null,{"className":"note-title skeleton","style":{"height":"3rem","width":"65%","marginInline":"12px 1em"}}],["$","div",null,{"className":"skeleton skeleton--button","style":{"width":"8em","height":"2.5em"}}]]}],["$","div",null,{"className":"note-preview","children":[["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}],["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}],["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}],["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}],["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}]]}]]}],"children":["$","div",null,{"className":"note--empty-state","children":["$","span",null,{"className":"note-text--empty-state","children":"Click a note on the left to view something! 🥺"}]}]}]}]]}]
module.exports = {
    tag: 'Server Root',
    props: {...},
    children: [
        { tag: "Client Component1", props: {...}: children: [] },
        { tag: "Server Component1", props: {...}: children: [
            { tag: "Server Component2", props: {...}: children: [] },
            { tag: "Server Component3", props: {...}: children: [] },
        ]}
    ]
}

React Runtime Rendering

React Runtime

M1

Client

J0

Server

Render

Request JS Bundle

Response Text (Streaming)

Browser

Client Root

Server

App

Server

Component 1

Client

Component 1

React Runtime

Render

Response Text

(Streaming)

Advantages Of React Server Components

Reduce Bundle Size

Server Side Capabilities

Automatic Code Splitting

import ClientComponent1 from './ClientComponent1';

function ServerComponent() {
    return (
        <div>
            // Client Component 會自動被 Code Splitting
            <ClientComponent1 />
        </div>
    )
}

Server Components VS SSR

SSR Navigation

RSC Navigation

How Next.js v13 Integrate Server Components ?

Page Directory -> App Directory

Colocation

File Conventions

App Directory also bring...

Layouts

Server Components

Streaming

Support for Data Fetching

Easily share UI between routes while preserving state and avoiding expensive re-renders.

Making server-first the default for the most dynamic applications.

Display instant loading states and stream in units of UI as they are rendered.

async Server Components and extended fetch API enables component-level fetching.

Server & Client Components

Server Component

Client Component

When to use which ?

Limits & Recommendations

Client Components can't import Server Components directly

Props need to be serializable

Server-Only & Client-Only

Moving Client Components to the Leaves

Data Fetching In Server Components

The fetch() API

Why Fetch Data In Server Components

Component-Level Data Fetching & Caching

Parallel & Sequential Data Fetching

Static & Dynamic Data Fetching

The fetch() API

async function getData() {
  const res = await fetch('https://api.example.com/...');
  // The return value is *not* serialized
  // You can return Date, Map, Set, etc.

  // Recommendation: handle errors
  if (!res.ok) {
    // This will activate the closest `error.js` Error Boundary
    throw new Error('Failed to fetch data');
  }

  return res.json();
}

export default async function Page() {
  const data = await getData();

  return <main></main>;
}

Automatic Request Deduplication

Provide a richer options object so that each request can separately set caching and revalidating rules

 

Data Fetching In Server Components

對於 backend data resources 有直接的存取權,例如 database 或是 file system

在 Server Side 可以避免一些敏滿資訊洩漏到 Client Side,例如 access token, API key...

讓資料的抓取與渲染在同一個環境中進行,以減少 client side 與 server side 間 back-and-forth 的溝通,可以盡量降低 client side 的工作量

減少 client-server 間的 waterfall 請求

How about data fetching in Client Components ?

import { use } from 'react';

export function Data({ url }) {
	const data = use(fetch(url).thrn(res => res.json()))
    
    return <pre>{JSON.stringify(data, null, 2)}</pre>
}

Components-Level Data Fetching

function Page({ data }) {
  // Render data...
}

// This gets called on every request
export async function getServerSideProps() {
  // Fetch data from external API
  const res = await fetch(`https://.../data`)
  const data = await res.json()

  // Pass data to the page via props
  return { props: { data } }
}

export default Page
async function getData() {
  const res = await fetch('https://api.example.com/...');
  // The return value is *not* serialized
  // You can return Date, Map, Set, etc.

  // Recommendation: handle errors
  if (!res.ok) {
    // This will activate the closest `error.js` Error Boundary
    throw new Error('Failed to fetch data');
  }

  return res.json();
}

export default async function Page() {
  const data = await getData();

  return <main></main>;
}

// child component...
export default async function Child() {
	const data = await getData();
    
    return <p>child</p>
}

Caching

Cache Function (RFC)

The fetch() API is wrapped with cache

How about GraphQL POST endpoint ?

Parallel & Sequential Data Fetching

Parallel Data Fetching

Parallel Data Fetching (Better UX)

export default async function Page({ params: { username } }) {
  // Initiate both requests in parallel
  const artistData = getArtist(username);
  const albumData = getArtistAlbums(username);

  // Wait for the artist's promise to resolve first
  const artist = await artistData;

  return (
    <>
      <h1>{artist.name}</h1>
      {/* Send the artist information first,
      and wrap albums in a suspense boundary */}
      <Suspense fallback={<div>Loading...</div>}>
        <Albums promise={albumData} />
      </Suspense>
    </>
  );
}

// Albums Component
async function Albums({ promise }) {
  // Wait for the albums promise to resolve
  const albums = await promise;

  return (
    <ul>
      {albums.map((album) => (
        <li key={album.id}>{album.name}</li>
      ))}
    </ul>
  );
}

Sequential Data Fetching

Static & Dynamic Data Fetching

fetch() API - Static By Default

減少 Server 與 DB 的負擔

降低頁面的 Loading Time

Per Page

Per Request

Streaming & Suspense

Without Streaming & Suspense

  1. First, all data for a given page is fetched on the server.
  2. The server then renders the HTML for the page.
  3. The HTML, CSS, and JavaScript for the page are sent to the client.
  4. A non-interactive user interface is shown using the generated HTML, and CSS.
  5. Finally, React hydrates the user interface to make it interactive.

Streaming

Suspense

Limits

CSS-In-JS (ref)

Third Party Libraries

Mutation (ref)

Hard to know is Server or Client Components

Mutation

import Todo from './todo';

interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

async function getTodos() {
  const res = await fetch('https://api.example.com/todos');
  const todos: Todo[] = await res.json();
  return todos;
}

export default async function Page() {
  const todos = await getTodos();
  return (
    <ul>
      {todos.map((todo) => (
        <Todo key={todo.id} {...todo} />
      ))}
    </ul>
  );
}

Mutation

"use client";

import { useRouter } from 'next/navigation';
import { useState, useTransition } from 'react';

interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

export default function Todo(todo: Todo) {
  const router = useRouter();
  const [isPending, startTransition] = useTransition();
  const [isFetching, setIsFetching] = useState(false);

  // Create inline loading UI
  const isMutating = isFetching || isPending;

  async function handleChange() {
    setIsFetching(true);
    // Mutate external data source
    await fetch(`https://api.example.com/todo/${todo.id}`, {
      method: 'PUT',
      body: JSON.stringify({ completed: !todo.completed }),
    });
    setIsFetching(false);

    startTransition(() => {
      // Refresh the current route and fetch new data from the server without
      // losing client-side browser or React state.
      router.refresh();
    });
  }

  return (
    <li style={{ opacity: !isMutating ? 1 : 0.7 }}>
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={handleChange}
        disabled={isPending}
      />
      {todo.title}
    </li>
  );
}

Roadmap

Conclusion

Conclusion

Thank You

Made with Slides.com