面試的時候,常常會被問到這題。看似簡單,其實有一些進階注意事項,此篇與您分享。
以下是最基本做法。根據陣列內容,直接渲染成一個列表。
import { useState, useEffect } from "react";
const App = () => {
const [posts, setPosts] = useState([]);
const [currentPost, setCurrentPost] = useState(undefined);
useEffect(() => {
const initialize = async () => {
const res = await fetch("https://jsonplaceholder.typicode.com/posts");
const json = await res.json();
setPosts(json);
};
initialize();
}, []);
const onPostClick = (post) => {
setCurrentPost(post);
};
return (
<div>
{currentPost && <h1>{currentPost.title}</h1>}
<PostList posts={posts} onPostClick={onPostClick} />
</div>
);
};
const PostList = ({ posts, onPostClick }) => {
return (
<div>
{posts.map((post) => (
<Post post={post} onPostClick={onPostClick} />
))}
</div>
);
};
const Post = ({ post, onPostClick }) => {
const onClick = () => {
onPostClick(post);
};
return <div onClick={onClick}>{post.title}</div>;
};
export default App;
以下是四個可以改進的方向,以及背後的原因。
替每個元件提供一個 key 屬性,可以幫助後續 React 渲染時改善效能。要注意 key 屬性要是唯一值,不要直接用陣列索引當成 key。
{posts.map((post) => (
<Post key={post.id} post={post} onPostClick={onPostClick} />
))}
每次點擊列表項目時,都會重新渲染 PostList
和每個 Post
。
const Post = ({ post, onPostClick }) => {
console.log("post rendered");
const onClick = () => {
onPostClick(post);
};
return <div onClick={onClick}>{post}</div>;
};
可以使用 React 提供的 memo
功能來優化 PostList
元件。用 memo
包裝元件時,等於告訴 React:除非 props 改變,否則不要重新渲染這個元件。
import { useState, useEffect, memo } from "react";
const PostList = memo(({ posts, onPostClick }) => {
return (
<div>
{posts.map((post) => (
<Post post={post} onPostClick={onPostClick} />
))}
</div>
);
});
不過,實際跑下去,會發現還是重新渲染了。因為每次 currentPost 改變時,App 都會重新渲染。每次重新渲染都會重新建立 onPostClick 函數。當一個函數被重新建立時(即使函式內容一樣),都算是新的實體。所以技術上來說,props 確實發生了變化,所以 PostList
會重新渲染。
const fn1 = () => {};
const fn2 = () => {};
fn1 === fn2; // => false
這種狀況,就可以使用 useCallback hook 來告訴 React 不要重新建立函數。
const onPostClick = useCallback((post) => {
setCurrentPost(post);
}, []);
上面的例子中,使用 useCallback 沒問題,可以避免全部貼文被重新渲染。但要知道的是,並非什麼函式都適合包在 useCallback 裡面。
const Post = ({ post, onPostClick }) => {
const useCalllback(onClick = () => {
onPostClick(post);
}, []);
return <div onClick={onClick}>{post.title}</div>;
};
比方說,在 Post 元件使用 useCallback 就不太有意義。因為這個元件是輕量級的。應該只在有意義的情況下使用 useCallback(可以用 profiling 工具分析效能確認)。useCallback
有缺點:它增加了程式碼的複雜性。使用 useCallback 會在每次渲染時運行的額外程式碼。
目前的元件沒有在卸載時進行清理。例如,如果在收到 URL 回應結果之前就離開頁面怎麼辦?這時應該要取消請求。
useEffect(() => {
const initialize = async () => {
const res = await fetch("https://jsonplaceholder.typicode.com/posts");
const json = await res.json();
setPosts(json);
};
initialize();
}, []);
useEffect
可以分為兩部分:掛載時運行的程式碼、卸載時運行的程式碼:
useEffect(() => {
// When component mounts what code should I run?
return () => {
// When component unmounts what code should I run (clean up)?
};
}, []);
我們可以使用 AbortController ,並在清理時呼叫 controller.abort() 來取消請求。
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
const initialize = async () => {
try {
const res = await fetch("https://jsonplaceholder.typicode.com/posts", { signal });
const json = await res.json();
setPosts(json);
} catch (err) {
console.log(err);
}
};
initialize();
return () => {
controller.abort();
};
}, []);
以上範例太過簡單,沒有太多無障礙功能可以加入。一旦應用程式變複雜,絕對應該要開始考慮這點。
有個簡單的無障礙測試:可以單獨只使用鍵盤就操作一個應用程式嗎?
關於這點,快速解法是:把每個項目都轉成按鈕,用鍵盤時就可以在元件之間切換。
const Post = ({ post, onPostClick }) => {
const onClick = () => {
onPostClick(post);
};
return <button onClick={onClick}>{post}</button>;
};
在 React 中渲染列表,乍看之下,只是個簡單的面試問題。但其實可深可淺。下次面試時遇到這問題,可以思考一下此篇文章談到的各個面向。