Kyle Mo
Become a better React developer
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 Reconcilier + ReactDOM
React Reconcilier + Native Renderer
React Reconcilier + Three.js Renderer
在 server side 抓取想要的資料
Server render 出 HTML
頁面的 HTML, CSS, JS 被送到 client side
使用者這時候可以看到畫面,但還不能進行互動
React 進行 hydration,賦予 UI 互動能力
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
<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
<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>
You have to fetch everything before you can show anything
import { lazy } from 'react';
const Comments = lazy(() => import('./Comments.js'));
// ...
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
You have to load everything before you can hydrate anything
<Layout>
<NavBar />
<Suspense fallback={<Spinner />}>
<Sidebar />
</Suspense>
<RightPane>
<Post />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</RightPane>
</Layout>
You have to hydrate everything before you can interact with anything
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.
useEffect(() => {
fetchSomething().then(result => setState(result));
}, []);
理論上效率、體驗最差,Render 後才去呼叫 API,一層一層的 Render,造成抓資料時的 Waterfall
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
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 了)
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]);
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
Server Components (.server.js(/ts/jsx/tsx))
Client Components (.client.js(/ts/jsx/tsx))
Share Components (.js(/ts/jsx/tsx))
Server Component
Client Component
function Root() {
const [data, setData] = useState({});
// 向伺服器發送請求
const componentResponse = useServerResponse(data);
return (
<DataContext.Provider value={[data, setData]}>
componentResponse.render();
</DataContext.Provider>
);
}
Server Component Root
Server
Component 1
Client
Component 1
Server
Component 2
Client
Component 2
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
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)
Reduce Bundle Size
Server Side Capabilities
Automatic Code Splitting
import ClientComponent1 from './ClientComponent1';
function ServerComponent() {
return (
<div>
// Client Component 會自動被 Code Splitting
<ClientComponent1 />
</div>
)
}
SSR Navigation
RSC Navigation
AST(抽象語法樹)是一種樹狀結構,用於表示程式碼的語法結構。當編譯器或解釋器讀取程式碼時,它首先將其解析成這樹狀結構,以便於後續的分析和處理。每個樹的節點都代表了程式碼中的一個結構元素,如變量、運算符或函數。利用AST,工具和技術可以更容易地進行程式碼轉換、優化和分析。
為了兼容老舊瀏覽器,我們通常會使用 webpack 打包編譯我們的程式碼,將 ES6 語法降低版本,例如箭頭函數變成普通函數。將 const、let 聲明改成 var 等等,這都是透過 AST 來完成的,只不過實現的過程比較複雜,精緻。不過主要包含以下三步驟:
雖然日常開發應用比較少會碰到 AST,但學會 AST 是打開開發各種工具機會的敲門磚,也讓我們有機會參與更底層工具的開發與研究。
(EX: Parser, Compiler, Bundler)
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} />;
}
無法做到 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 也一樣)
在大型或有 Deeply Nested 元素的網頁
有長列表或表格
使用者輸入高頻率,如 text input 文字輸入、slider / range input 拖動輸入