阿川私房教材:學程式,拿 offer!

63 個專案實戰,直接上手!
無需補習,按步驟打造你的面試作品。

立即解鎖你的轉職秘笈

面試的時候,常常會被問到這題。看似簡單,其實有一些進階注意事項,此篇與您分享。

基本做法

以下是最基本做法。根據陣列內容,直接渲染成一個列表。

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;

改進方法

以下是四個可以改進的方向,以及背後的原因。

1. 指定 key

替每個元件提供一個 key 屬性,可以幫助後續 React 渲染時改善效能。要注意 key 屬性要是唯一值,不要直接用陣列索引當成 key。

{posts.map((post) => (
  <Post key={post.id} post={post} onPostClick={onPostClick} />
))}

2. 優化渲染

每次點擊列表項目時,都會重新渲染 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 會在每次渲染時運行的額外程式碼。

3. 元件卸載時清理乾淨

目前的元件沒有在卸載時進行清理。例如,如果在收到 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();
  };
}, []);

4. 增加無障礙輔助功能

以上範例太過簡單,沒有太多無障礙功能可以加入。一旦應用程式變複雜,絕對應該要開始考慮這點。

有個簡單的無障礙測試:可以單獨只使用鍵盤就操作一個應用程式嗎?

關於這點,快速解法是:把每個項目都轉成按鈕,用鍵盤時就可以在元件之間切換。

const Post = ({ post, onPostClick }) => {
  const onClick = () => {
    onPostClick(post);
  };

  return <button onClick={onClick}>{post}</button>;
};

結論

在 React 中渲染列表,乍看之下,只是個簡單的面試問題。但其實可深可淺。下次面試時遇到這問題,可以思考一下此篇文章談到的各個面向。


共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。

阿川私房教材:學程式,拿 offer!

63 個專案實戰,直接上手!
無需補習,按步驟打造你的面試作品。

立即解鎖你的轉職秘笈