Advanced

Kyle Mo

 

Become a better React developer

React Concepts - (2)

State Of React

 

  • React Renderer
  • Streaming SSR
  • Suspense For Data Fetching
  • Concurrent Features
  • Server Components
  • React Compiler (AST)
  • Maybe Not React

React Renderer

Virtual DOM is overhead ?

  1. Diffing
  2. DOM Manipulation
const search = document.getElementById("search");
search.innerHTML = `<input class="search" type="text" value="foo">`;
// Change value to "bar"?
search.innerHTML = `<input class="search" type="text" value="bar">`;
const search = document.getElementById("search");
search.innerHTML = `<input class="search" type="text" value="foo">`;
// Change value to "bar"?
search.querySelector("input").value = "bar";

Virtual DOM 透過 Diffing 找出最佳方式,再進行最佳化 DOM 操作

如果本身就會寫效能好的 DOM 操作 ->

理論上效能會比 Virtual DOM 好,因為 Virtual DOM 多做了 Diffing,且記憶體用量比較大(需要 maintain 一個樹狀結構)

Virtual DOM is, by definition, slower than carefully crafted manual updates.

Virtual DOM (Fiber Tree) 的目的從來就不是效能,而是提升 Developer Experience:開發者不用管 DOM 怎麼更新、抽象化、元件化、Composition、任務優先級、相對其他框架寫法上更能運用 JavaScript 的能力...等等。

Svelte 沒有採用 Virtual DOM,它靠在編譯階段得知之後會有哪些

 DOM 操作,提前產生對應的程式碼,可以產生差不多甚至超越 Virtaul DOM 的效能。

所以 Virtual DOM 不好嗎?不見得,把 Diffing 邏輯抽出來,與平台解耦,讓跨平台成為可能。

React

React Native

React Three Fiber

React Reconcilier + ReactDOM

React Reconcilier + Native Renderer

React Reconcilier + Three.js Renderer

How to write a custom Renderer ?

Streaming SSR

Original SSR

  1. 在 server side 抓取想要的資料

  2. Server render 出 HTML

  3. 頁面的 HTML, CSS, JS 被送到 client side

  4. 使用者這時候可以看到畫面,但還不能進行互動

  5. React 進行 hydration,賦予 UI 互動能力

Hydration

Problems

  • You have to fetch everything before you can show anything

  • You have to load everything before you can hydrate anything

  • You have to hydrate everything before you can interact with anything

😨

Streaming HTML In React

<Layout>
  <NavBar />
  <Sidebar />
  <RightPane>
    <Post />
    <Suspense fallback={<Spinner />}>
      <Comments />
    </Suspense>
  </RightPane>
</Layout>

Note: for this to work, your data fetching solution needs to integrate with Suspense

Initial HTML

<main>
  <nav>
    <!--NavBar -->
    <a href="/">Home</a>
   </nav>
  <aside>
    <!-- Sidebar -->
    <a href="/profile">Profile</a>
  </aside>
  <article>
    <!-- Post -->
    <p>Hello world</p>
  </article>
  <section id="comments-spinner">
    <!-- Spinner -->
    <img width=400 src="spinner.gif" alt="Loading..." />
  </section>
</main>
<div hidden id="comments">
  <!-- Comments -->
  <p>First comment</p>
  <p>Second comment</p>
</div>
<script>
  // This implementation is slightly simplified
  document.getElementById('sections-spinner').replaceChildren(
    document.getElementById('comments')
  );
</script>

Data Ready

Solve What Problem ?

You have to fetch everything before you can show anything

Selective Hydration In React 18

import { lazy } from 'react';

const Comments = lazy(() => import('./Comments.js'));

// ...

<Suspense fallback={<Spinner />}>
  <Comments />
</Suspense>

Solve What Problem ?

You have to load everything before you can hydrate anything

Interacting with the page before all the components have hydrated

Even..

<Layout>
  <NavBar />
  <Suspense fallback={<Spinner />}>
    <Sidebar />
  </Suspense>
  <RightPane>
    <Post />
    <Suspense fallback={<Spinner />}>
      <Comments />
    </Suspense>
  </RightPane>
</Layout>

Solve What Problem ?

You have to hydrate everything before you can interact with anything

TLDR

  • You no longer have to wait for all the data to load on the server before sending HTML. Instead, you start sending HTML as soon as you have enough to show a shell of the app, and stream the rest of the HTML as it’s ready.

  • You no longer have to wait for all JavaScript to load to start hydrating. Instead, you can use code splitting together with server rendering. The server HTML will be preserved, and React will hydrate it when the associated code loads.

  • You no longer have to wait for all components to hydrate to start interacting with the page. Instead, you can rely on Selective Hydration to prioritize the components the user is interacting with, and hydrate them early.

And they are all relative about fiber architecture we talked about last time...

Suspense For Data Fetching

Fetch On Render

useEffect(() => {
  fetchSomething().then(result => setState(result));
}, []);

理論上效率、體驗最差,Render 後才去呼叫 API,一層一層的 Render,造成抓資料時的 Waterfall

Fetch Then Render

function fetchProfileData() {
  return Promise.all([
    fetchUser(),
    fetchPosts()
  ]).then(([user, posts]) => {
    return {user, posts};
  })
}

// Kick off fetching as early as possible
const promise = fetchProfileData();

function ProfilePage() {
  const [user, setUser] = useState(null);
  const [posts, setPosts] = useState(null);

  useEffect(() => {
    promise.then(data => {
      setUser(data.user);
      setPosts(data.posts);
    });
  }, []);

  if (user === null) {
    return <p>Loading profile...</p>;
  }
  return (
    <>
      <h1>{user.name}</h1>
      <ProfileTimeline posts={posts} />
    </>
  );
}

// The child doesn't trigger fetching anymore
function ProfileTimeline({ posts }) {
  if (posts === null) {
    return <h2>Loading posts...</h2>;
  }
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.text}</li>
      ))}
    </ul>
  );
}

Using Relay or GraphQL

Render as You Fetch (Suspense)

function ProfilePage() {
  return (
    <Suspense fallback={<h1>Loading profile...</h1>}>
      <ProfileDetails />
    </Suspense>
  );
}

function ProfileDetails() {
  // Try to read user info, although it might not have loaded yet
  const user = resource.user.read();
  return <h1>{user.name}</h1>;
}

在渲染前儘早的開始抓資料,並立刻的開始渲染下一個 Chunk,這時資料若處於未 Ready 的狀態,會 throw Promise 並進入 Suspense 的狀態,等到 Promise Resolve 後,React 會進行 Retry(這時候資料已經 Ready 了)

Solve Race Condition In Effect

useEffect(() => {
  fetchUser(id).then(u => setUser(u));
}, [id]);
useEffect(() => {
    const abortController = new AbortController();

    const fetchData = async () => {
      dispatch(requestStarted());

      try {
        fetch(url, { signal: abortController.signal });

        // code omitted for brevity

        dispatch(requestSuccessful({ data }));
      } catch (e) {
        dispatch(requestFailed({ error: e.message }));
      }
    };

    fetchData();

    return () => {
      abortController.abort();
    };
  }, [url]);

Concurrent Features

Concurrent Features

Concurrent Mode

One is Suspense...and another is

Transition

React Transitions are (blocks of) state updates, that the developer instructs React to deprioritize and delay if something more important comes along.

CSS Transition

Visual States

startTransition For Low-priority updates

Pending State

And they are all relative about fiber architecture we talked about last time...

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

Suspense For Data Fetching ?

Server Components

React Compiler

React Forget

How to build a compiler ?

AST (Abstract Syntax Tree)

AST(抽象語法樹)是一種樹狀結構,用於表示程式碼的語法結構。當編譯器或解釋器讀取程式碼時,它首先將其解析成這樹狀結構,以便於後續的分析和處理。每個樹的節點都代表了程式碼中的一個結構元素,如變量、運算符或函數。利用AST,工具和技術可以更容易地進行程式碼轉換、優化和分析。

What Can AST Do ?

AST 長什麼樣子?

使用範例

為了兼容老舊瀏覽器,我們通常會使用 webpack 打包編譯我們的程式碼,將 ES6 語法降低版本,例如箭頭函數變成普通函數。將 const、let 聲明改成 var 等等,這都是透過 AST 來完成的,只不過實現的過程比較複雜,精緻。不過主要包含以下三步驟:

 

  1. JS 語法解析成 AST
  2. 修改 AST
  3. AST 轉成目標語法( ex: JS, Machine Code )

為什麼要理解 AST ?

雖然日常開發應用比較少會碰到 AST,但學會 AST 是打開開發各種工具機會的敲門磚,也讓我們有機會參與更底層工具的開發與研究。

(EX: Parser, Compiler, Bundler)

Maybe Not React ?

也許 React Forget 也救不了 React ? 

Subscription reactivity 訂閱式響應 vs. Comparison reactivity 比較式響應

function A() {
  const [text, setText] = useState(''');
  return <B text={text} />
}

function B({text}) {
  return <C text={text} />;
}

// ...
function Z({text}) {
  return <input value={text} />;
}

Imagine This...

無法做到 Fine-Grained Reactivity

比較式響應的問題

function Todo({todos, showDone, text}) {
  const filteredTodos = todos.map(todo => todo.done === showDone);
 
  return (
    <div>
      <ul>{filteredTodos.map(todo => <li>{todo.task}</li>)}<ul>
      <input value={text} />
    </div>
  )
}

即便加上 memoize,還是要做比較

(剛剛那個例子換成 context 也一樣)

什麼情況下可能會有問題?

  1. 在大型或有 Deeply Nested 元素的網頁

  2. 有長列表或表格

  3. 使用者輸入高頻率,如 text input 文字輸入、slider / range input 拖動輸入

Try These Frameworks

  • Qwik
  • Preact (signals)
  • Svelte (Compiler)
  • Solid
  • Vue 3 (Proxy Based Reactivity)

Conclusion

  • Know future progress of React
  • Know the limits of React
  • Know the design philosophy of React
  • Know the differences between React & other frameworks 
  • Don't think React is always the only solution

React - 02

By kylemo860617

React - 02

  • 217