在過去的5 年裡,我一直在專業地使用 React。

在這篇文章中,我分享了我多年來學到的 101 個最佳提示和技巧。

準備好?讓我們潛入吧💪!

ℹ️注意事項:

  • 本指南假設您基本上熟悉 React 並理解術語propsstatecontext等。

  • 為了讓事情變得簡單,我嘗試在大多數範例中使用 Vanilla JS。如果您使用 TypeScript,則可以輕鬆調整程式碼。

  • 該程式碼尚未準備好用於生產。請自行決定使用。


{% 詳細目錄(點擊展開)↕️ %}

{% 結束詳細資料 %}


類別#1:元件組織🧹


1. 使用自閉合標籤來保持程式碼緊湊

// ❌ Bad: too verbose
<MyComponent></MyComponent>

// ✅ Good
<MyComponent/>

回到頂部⬆️


2. 優先使用fragments而不是 DOM 節點(例如 div、span 等)來對元素進行分組

在 React 中,每個元件必須傳回一個元素。不要將多個元素包裝在<div><span>中,而是使用<Fragment>來保持DOM整潔。

❌ 不好:使用div會使您的 DOM 變得混亂,並且可能需要更多 CSS 程式碼。

function Dashboard() {
  return (
    <div>
      <Header />
      <Main />
    </div>
  );
}

✅ 好: <Fragment>包裹元素而不影響 DOM 結構。

function Dashboard() {
  return (
    <Fragment>
      <Header />
      <Main />
    </Fragment>
  );
}

回到頂部⬆️


3.使用React片段簡寫<></> (除非你需要設定一個key)

❌ 不好:下面的程式碼不必要地冗長。

<Fragment>
   <FirstChild />
   <SecondChild />
</Fragment>

✅ 好:除非,你需要一個key<></>更簡潔。

<>
   <FirstChild />
   <SecondChild />
</>

// Using a `Fragment` here is required because of the key.
function List({ users }) {
  return (
    <div>
      {users.map((user) => (
        <Fragment key={user.id}>
          <span>{user.name}</span>
          <span>{user.occupation}</span>
        </Fragment>
      ))}
    </div>
  );
}

回到頂部⬆️


4. 喜歡分散道具而不是單獨存取每個道具

❌ 不好:下面的程式碼更難閱讀(尤其是大規模時)。

// We do `props…` all over the code.
function TodoList(props) {
  return (
    <div>
      {props.todos.map((todo) => (
        <div key={todo}>
          <button
            onClick={() => props.onSelectTodo(todo)}
            style={{
              backgroundColor: todo === props.selectedTodo ? "gold" : undefined,
            }}
          >
            <span>{todo}</span>
          </button>
        </div>
      ))}
    </div>
  );
}

✅ 好:下面的程式碼更簡潔。

function TodoList({ todos, selectedTodo, onSelectTodo }) {
  return (
    <div>
      {todos.map((todo) => (
        <div key={todo}>
          <button
            onClick={() => onSelectTodo(todo)}
            style={{
              backgroundColor: todo === selectedTodo ? "gold" : undefined,
            }}
          >
            <span>{todo}</span>
          </button>
        </div>
      ))}
    </div>
  );
}

回到頂部⬆️


5. 設定 props 的預設值時,在解構它們的同時進行

❌ 不好:您可能需要在多個位置定義預設值並引入新變數。

function Button({ onClick, text, small, colorScheme }) {
  let scheme = colorScheme || "light";
  let isSmall = small || false;
  return (
    <button
      onClick={onClick}
      style={{
        color: scheme === "dark" ? "white" : "black",
        fontSize: isSmall ? "12px" : "16px",
      }}
    >
      {text ?? "Click here"}
    </button>
  );
}

✅ 好:您可以在頂部的一處設定所有預設值。這使得其他人很容易找到他們。

function Button({
  onClick,
  text = "Click here",
  small = false,
  colorScheme = "light",
}) {
  return (
    <button
      onClick={onClick}
      style={{
        color: colorScheme === "dark" ? "white" : "black",
        fontSize: small ? "12px" : "16px",
      }}
    >
      {text}
    </button>
  );
}

回到頂部⬆️


6. 傳遞string型別 props 時去掉花括號。

// ❌ Bad: curly braces are not needed
<Button text={"Click me"} colorScheme={"dark"} />

// ✅ Good
<Button text="Click me" colorScheme="dark" />

回到頂部⬆️


7. 使用value && <Component {...props}/>之前確保value是布林值,以防止在螢幕上顯示意外的值。

❌ 不好:當清單為空時,螢幕上會印出0

export function ListWrapper({ items, selectedItem, setSelectedItem }) {
  return (
    <div className="list">
      {items.length && ( // `0` if the list is empty
        <List
          items={items}
          onSelectItem={setSelectedItem}
          selectedItem={selectedItem}
        />
      )}
    </div>
  );
}

:沒有物品時,螢幕上不會列印任何內容。

export function ListWrapper({ items, selectedItem, setSelectedItem }) {
  return (
    <div className="list">
      {items.length > 0 && (
        <List
          items={items}
          onSelectItem={setSelectedItem}
          selectedItem={selectedItem}
        />
      )}
    </div>
  );
}

回到頂部⬆️


8. 使用 IIFE(立即呼叫函數表達式)來保持程式碼整潔並避免延遲變數

:變數gradeSumgradeCount擾亂了元件的範圍。

function Grade() {
  let gradeSum = 0;
  let gradeCount = 0;
  grades.forEach((grade) => {
    gradeCount++;
    gradeSum += grade;
  });
  const averageGrade = gradeSum / gradeCount;
  return <>{averageGrade}</>;
}

:變數gradeSumgradeCount範圍在IIFE 內。

function Grade() {
  const averageGrade = (() => {
    let gradeSum = 0;
    let gradeCount = 0;
    grades.forEach((grade) => {
      gradeCount++;
      gradeSum += grade;
    });
    return gradeSum / gradeCount;
  })();
  return <>{averageGrade}</>;
}

💡 注意:你也可以在元件外部定義一個函數computeAverageGrade ,並在元件內部呼叫它。

回到頂部⬆️


9. 使用柯里化函數重複使用邏輯(並正確記憶回呼函數)

❌ 不好:更新欄位的邏輯非常重複。

function Form() {
  const [{ name, email }, setFormState] = useState({
    name: "",
    email: "",
  });

  return (
    <>
      <h1>Class Registration Form</h1>
      <form>
        <label>
          Name:{" "}
          <input
            type="text"
            value={name}
            onChange={(evt) =>
              setFormState((formState) => ({
                ...formState,
                name: evt.target.value,
              }))
            }
          />
        </label>
        <label>
          Email:{" "}
          <input
            type="email"
            value={email}
            onChange={(evt) =>
              setFormState((formState) => ({
                ...formState,
                email: evt.target.value,
              }))
            }
          />
        </label>
      </form>
    </>
  );
}

✅ 好:引入createFormValueChangeHandler ,為每個欄位傳回正確的處理程序。

注意:如果您開啟了 ESLint 規則jsx-no-bind,這個技巧尤其有用。您可以將柯里化函數包裝在useCallback和“Voilà!”中。

function Form() {
  const [{ name, email }, setFormState] = useState({
    name: "",
    email: "",
  });

  const createFormValueChangeHandler = (field) => {
    return (event) => {
      setFormState((formState) => ({
        ...formState,
        [field]: event.target.value,
      }));
    };
  };

  return (
    <>
      <h1>Class Registration Form</h1>
      <form>
        <label>
          Name:{" "}
          <input
            type="text"
            value={name}
            onChange={createFormValueChangeHandler("name")}
          />
        </label>
        <label>
          Email:{" "}
          <input
            type="email"
            value={email}
            onChange={createFormValueChangeHandler("email")}
          />
        </label>
      </form>
    </>
  );
}

回到頂部⬆️


10. 將不依賴元件 props/state 的資料移至外部,以獲得更清晰(且更有效率)的程式碼

❌ 不好: OPTIONSrenderOption不需要位於元件內部,因為它們不依賴任何 props 或 state。

此外,將它們保留在內部意味著每次元件渲染時我們都會獲得新的物件參考。如果我們將renderOption傳遞給包裹在memo中的子元件,它會破壞記憶。

function CoursesSelector() {
  const OPTIONS = ["Maths", "Literature", "History"];
  const renderOption = (option: string) => {
    return <option>{option}</option>;
  };

  return (
    <select>
      {OPTIONS.map((opt) => (
        <Fragment key={opt}>{renderOption(opt)}</Fragment>
      ))}
    </select>
  );
}

✅ 好:將它們移出元件以保持元件清潔和參考穩定。

const OPTIONS = ["Maths", "Literature", "History"];
const renderOption = (option: string) => {
  return <option>{option}</option>;
};

function CoursesSelector() {
  return (
    <select>
      {OPTIONS.map((opt) => (
        <Fragment key={opt}>{renderOption(opt)}</Fragment>
      ))}
    </select>
  );
}

💡 注意:在這個例子中,您可以透過使用內聯選項元素來進一步簡化。

const OPTIONS = ["Maths", "Literature", "History"];

function CoursesSelector() {
  return (
    <select>
      {OPTIONS.map((opt) => (
        <option key={opt}>{opt}</option>
      ))}
    </select>
  );
}

回到頂部⬆️


11. 儲存清單中選定的專案時,儲存專案 ID 而不是整個專案

❌ 不好:如果選擇了一個專案,但隨後它發生了變化(即,我們收到相同 ID 的全新物件參考),或者如果該專案不再出現在列表中,則selectedItem將保留過時的值或變更得不正確。

function ListWrapper({ items }) {
  // We are referencing the entire item
  const [selectedItem, setSelectedItem] = useState<Item | undefined>();

  return (
    <>
      {selectedItem != null && <div>{selectedItem.name}</div>}
      <List
        items={items}
        selectedItem={selectedItem}
        onSelectItem={setSelectedItem}
      />
    </>
  );
}

✅ 好:我們透過 ID 儲存所選專案(應該是穩定的)。這可以確保即使該專案從清單中刪除或其屬性之一發生更改,UI 也應該是正確的。

function ListWrapper({ items }) {
  const [selectedItemId, setSelectedItemId] = useState<number | undefined>();
  // We derive the selected item from the list
  const selectedItem = items.find((item) => item.id === selectedItemId);

  return (
    <>
      {selectedItem != null && <div>{selectedItem.name}</div>}
      <List
        items={items}
        selectedItemId={selectedItemId}
        onSelectItem={setSelectedItemId}
      />
    </>
  );
}

回到頂部⬆️


12. 如果您在做某事之前經常檢查 prop 的值,請引入一個新元件

❌ 不好:因為所有user == null檢查,程式碼變得混亂。

在這裡,由於hooks 的規則,我們不能提前返回。

function Posts({ user }) {
  // Due to the rules of hooks, `posts` and `handlePostSelect` must be declared before the `if` statement.
  const posts = useMemo(() => {
    if (user == null) {
      return [];
    }
    return getUserPosts(user.id);
  }, [user]);

  const handlePostSelect = useCallback(
    (postId) => {
      if (user == null) {
        return;
      }
      // TODO: Do something
    },
    [user]
  );

  if (user == null) {
    return null;
  }

  return (
    <div>
      {posts.map((post) => (
        <button key={post.id} onClick={() => handlePostSelect(post.id)}>
          {post.title}
        </button>
      ))}
    </div>
  );
}

✅ 好:我們引入了一個新元件UserPosts ,它接受定義的使用者並且更加乾淨。

function Posts({ user }) {
  if (user == null) {
    return null;
  }

  return <UserPosts user={user} />;
}

function UserPosts({ user }) {
  const posts = useMemo(() => getUserPosts(user.id), [user.id]);

  const handlePostSelect = useCallback(
    (postId) => {
      // TODO: Do something
    },
    [user]
  );

  return (
    <div>
      {posts.map((post) => (
        <button key={post.id} onClick={() => handlePostSelect(post.id)}>
          {post.title}
        </button>
      ))}
    </div>
  );
}

回到頂部⬆️


13. 使用 CSS :empty偽類別隱藏沒有子元素的元素

在下面的範例中👇,包裝器接受子專案並在它們周圍加入紅色邊框。

function PostWrapper({ children }) {
  return <div className="posts-wrapper">{children}</div>;
}
.posts-wrapper {
  border: solid 1px red;
}

❌ 問題:即使子項為空(即等於nullundefined等),邊框在螢幕上仍可見。

紅色邊框

✅ 解決方案:使用:empty CSS 偽類來確保包裝器為空時不顯示。

.posts-wrapper:empty {
  display: none;
}

回到頂部⬆️


14. 將所有狀態和上下文分組在元件頂部

當所有狀態和上下文都位於頂部時,很容易發現什麼可以觸發元件重新渲染。

❌ 不好:狀態和上下文分散,難以追蹤。

function App() {
  const [email, setEmail] = useState("");
  const onEmailChange = (event) => {
    setEmail(event.target.value);
  };
  const [password, setPassword] = useState("");
  const onPasswordChange = (event) => {
    setPassword(event.target.value);
  };
  const theme = useContext(ThemeContext);

  return (
    <div className={`App ${theme}`}>
      <h1>Welcome</h1>
      <p>
        Email: <input type="email" value={email} onChange={onEmailChange} />
      </p>
      <p>
        Password:{" "}
        <input type="password" value={password} onChange={onPasswordChange} />
      </p>
    </div>
  );
}

✅ 好:所有狀態和上下文都分組在頂部,使其易於發現。

function App() {
  const theme = useContext(ThemeContext);
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const onEmailChange = (event) => {
    setEmail(event.target.value);
  };
  const onPasswordChange = (event) => {
    setPassword(event.target.value);
  };

  return (
    <div className={`App ${theme}`}>
      <h1>Welcome</h1>
      <p>
        Email: <input type="email" value={email} onChange={onEmailChange} />
      </p>
      <p>
        Password:{" "}
        <input type="password" value={password} onChange={onPasswordChange} />
      </p>
    </div>
  );
}

回到頂部⬆️


類別#2:有效的設計模式和技術🛠️


15. 利用children props 實現更簡潔的程式碼(以及效能優勢)

使用children道具有幾個好處:

  • 好處#1:您可以透過將 prop 直接傳遞給子元件而不是透過父元件路由它們來避免 prop 鑽取。

  • 好處#2:您的程式碼更具可擴展性,因為您可以輕鬆修改子元件而無需更改父元件。

  • 好處#3:您可以使用此技巧來避免重新渲染「慢」元件(請參閱下面的範例👇)。

❌ 不好:每當Dashboard渲染時, MyVerySlowComponent就會渲染,每次當前時間更新時都會發生這種情況。您可以在下圖中看到它,我在其中使用了React Developer Tool 的分析器

function App() {
  // Some other logic…
  return (
    <Dashboard />
  );
}

function Dashboard() {
  const [currentTime, setCurrentTime] = useState(new Date());
  useEffect(() => {
    const intervalId = setInterval(() => {
      setCurrentTime(new Date());
    }, 1_000);
    return () => clearInterval(intervalId);
  }, []);

  return (
    <>
      <h1>{currentTime.toTimeString()}</h1>
      <MyVerySlowComponent />  {/* Renders whenever `Dashboard` renders */}
    </>
  );
}

<

圖>

每當儀表板呈現時, MyVerySlowComponent都會呈現

✅ 好: Dashboard渲染時MyVerySlowComponent不會渲染。

function App() {
  return (
    <Dashboard >
      <MyVerySlowComponent />
    </Dashboard>
  );
}

function Dashboard({ children }) {
  const [currentTime, setCurrentTime] = useState(new Date());
  useEffect(() => {
    const intervalId = setInterval(() => {
      setCurrentTime(new Date());
    }, 1_000);
    return () => clearInterval(intervalId);
  }, []);

  return (
    <>
      <h1>{currentTime.toTimeString()}</h1>
      {children}
    </>
  );
}

<

圖>

MyVerySlowComponent不再渲染

回到頂部⬆️


16. 使用compound components建構可組合程式碼

將複合元件視為樂高積木。

您將它們組合在一起以建立自訂的 UI。這些元件在建立庫時工作得非常好,從而產生富有表現力和高度可擴展的程式碼。

您可以在此處進一步探索此模式👉複合模式

來自reach.ui的範例Menu、MenuButton、MenuList、MenuLink是複合元件)

<Menu>
  <MenuButton>
    Actions <span aria-hidden>▾</span>
  </MenuButton>
  <MenuList>
    <MenuItem onSelect={() => alert("Download")}>Download</MenuItem>
    <MenuItem onSelect={() => alert("Copy")}>Create a Copy</MenuItem>
    <MenuLink as="a" href="https://reacttraining.com/workshops/">
    Attend a Workshop
    </MenuLink>
  </MenuList>
</Menu>

回到頂部⬆️


17. 使用render functionscomponent functions props 使程式碼更具可擴充性

假設我們想要顯示各種列表,例如訊息、個人資料或帖子,並且每個列表都應該是可排序的。

為了實現這一點,我們引入了一個List元件以供重複使用。我們可以透過兩種方法來解決這個問題:

❌ 不好:選項 1

List處理每個專案的渲染以及它們的排序方式。這是有問題的,因為它違反了開閉原則。每當新增新的專案類型時,都會修改此程式碼。

✅ 好:選項 2

List接受渲染函數或元件函數,僅在需要時呼叫它們。

您可以在下面的沙箱中找到一個範例:

🏖 沙盒

回到頂部⬆️


18. 處理不同情況時,使用value === case && <Component />以避免保留舊狀態

❌問題:在下面的沙箱中,在PostsSnippets之間切換時計數器不會重置。發生這種情況是因為在渲染相同元件時,其狀態在類型變更時保持不變。

🏖 沙盒

✅ 解決方案:根據selectedType渲染一個元件或在type改變時使用按鍵強制重設。

function App() {
  const [selectedType, setSelectedType] = useState<ResourceType>("posts");
  return (
    <>
      <Navbar selectedType={selectedType} onSelectType={setSelectedType} />
      {selectedType === "posts" && <Resource type="posts" />}
      {selectedType === "snippets" && <Resource type="snippets" />}
    </>
  );
}

// We use the `selectedType` as a key
function App() {
  const [selectedType, setSelectedType] = useState<ResourceType>("posts");
  return (
    <>
      <Navbar selectedType={selectedType} onSelectType={setSelectedType} />
      <Resource type={selectedType} key={selectedType} />
    </>
  );
}

回到頂部⬆️


19. 始終使用錯誤邊界

預設情況下,如果你的應用程式在渲染過程中遇到錯誤,整個 UI 就會崩潰💥。

為了防止這種情況,請使用錯誤邊界來:

  • 即使發生錯誤,也能保持應用程式的某些部分正常運作。

  • 顯示使用者友善的錯誤訊息並可選擇追蹤錯誤。

💡提示:您可以使用react-error-boundary庫。

回到頂部⬆️


類別#3:按鍵和參考 🗝️


20.使用crypto.randomUUIDMath.random產生金鑰

map()呼叫中的 JSX 元素始終需要鍵。

假設您的元素還沒有鍵。在這種情況下,您可以使用crypto.randomUUIDMath.randomuuid函式庫產生唯一 ID。

注意:請注意,舊版瀏覽器中未定義crypto.randomUUID

function App({ fruits }) {
  const fruitsWithIds = fruits.map((fruit) => ({
    value: fruit,
    id: crypto.randomUUID(), // or uuid.v4(), Math.random()
  }));
  return <List items={fruitsWithIds} />;
}

function List({ items }) {
  return (
    <ul>
      {items.map(({ value, id }) => {
        return <li key={id}>{value}</li>;
      })}
    </ul>
  );
}

回到頂部⬆️


21. 確保您的清單專案 ID 穩定(即它們在渲染之間不會改變)

密鑰/ID 應盡可能穩定。

否則,React 可能會無用地重新渲染某些元件,或者選擇將不再有效,如下例所示。

❌ 不好: selectedQuoteId會在App渲染時發生變化,因此永遠不會有有效的選擇。

function App() {
  const [quotes, setQuotes] = useState([]);
  const [selectedQuoteId, setSelectedQuoteId] = useState(undefined);

  // Fetch quotes
  useEffect(() => {
    const loadQuotes = () =>
      fetchQuotes().then((result) => {
        setQuotes(result);
      });
    loadQuotes();
  }, []);

  // Add ids: this is bad!!!
  const quotesWithIds = quotes.map((quote) => ({
    value: quote,
    id: crypto.randomUUID(),
  }));

  return (
    <List
      items={quotesWithIds}
      selectedItemId={selectedQuoteId}
      onSelectItem={setSelectedQuoteId}
    />
  );
}

✅ 好:當我們收到報價時會新增IDs

function App() {
  const [quotes, setQuotes] = useState([]);
  const [selectedQuoteId, setSelectedQuoteId] = useState(undefined);

  // Fetch quotes and save with ID
  useEffect(() => {
    const loadQuotes = () =>
      fetchQuotes().then((result) => {
        // We add the `ids` as soon as we get the results
        setQuotes(
          result.map((quote) => ({
            value: quote,
            id: crypto.randomUUID(),
          }))
        );
      });
    loadQuotes();
  }, []);

  return (
    <List
      items={quotes}
      selectedItemId={selectedQuoteId}
      onSelectItem={setSelectedQuoteId}
    />
  );
}

回到頂部⬆️


22.策略性地使用key屬性來觸發元件重新渲染

想要強制元件從頭開始重新渲染?只需更改其key即可。

在下面的範例中,我們使用此技巧在切換到新分頁時重設錯誤邊界。

🏖 沙盒

回到頂部⬆️


23. 使用ref callback function來執行監視 ref 大小變化和管理多個節點元素等任務。

您是否知道可以將函數而不是 ref 物件傳遞給ref屬性?

它的工作原理如下:

  • 當 DOM 節點新增到螢幕上時,React 以 DOM 節點作為參數呼叫函數。

  • 當 DOM 節點被刪除時,React 使用null呼叫函數。

在下面的例子中,我們使用這個技巧來跳過useEffect

❌之前:使用useEffect聚焦輸入

function App() {
  const ref = useRef();

  useEffect(() => {
    ref.current?.focus();
  }, []);

  return <input ref={ref} type="text" />;
}

✅ 之後:一旦有輸入,我們就會立即關注。

function App() {
  const ref = useCallback((inputNode) => {
    inputNode?.focus();
  }, []);

  return <input ref={ref} type="text" />;
}

回到頂部⬆️


類別#4:組織 React 程式碼🧩


24. 將 React 元件與其資產(例如樣式、圖像等)並置

始終將每個 React 元件與相關資產一起保存,例如樣式和圖像。

  • 這使得當不再需要該元件時可以更輕鬆地刪除它們。

  • 它還簡化了程式碼導航,因為您需要的一切都集中在一個地方。

元件資料夾結構

回到頂部⬆️


25.限制元件檔案大小

包含大量元件和匯出的大檔案可能會令人困惑。

另外,隨著更多東西的加入,它們往往會變得更大。

因此,以合理的檔案大小為目標,並在有意義時將元件拆分為單獨的檔案。

回到頂部⬆️


26.限制函數元件檔案中傳回語句的數量

功能元件中的多個return語句使得很難看到元件傳回的內容。

對於我們可以搜尋render術語的類別元件來說,這不是問題。

一個方便的技巧是盡可能使用不含大括號的箭頭函數(VSCode 有一個針對此的運算😀)。

❌ 缺點:更難發現元件回傳語句

function Dashboard({ posts, searchTerm, onPostSelect }) {
  const filteredPosts = posts.filter((post) => {
    return post.title.includes(searchTerm);
  });
  const createPostSelectHandler = (post) => {
    return () => {
      onPostSelect(post.id);
    };
  };
  return (
    <>
      <h1>Posts</h1>
      <ul>
        {filteredPosts.map((post) => {
          return (
            <li key={post.id} onClick={createPostSelectHandler(post)}>
              {post.title}
            </li>
          );
        })}
      </ul>
    </>
  );
}

✅ 好:元件有一個回傳語句

function Dashboard({ posts, searchTerm, onPostSelect, selectedPostId }) {
  const filteredPosts = posts.filter((post) => post.title.includes(searchTerm));
  const createPostSelectHandler = (post) => () => {
    onPostSelect(post.id);
  };
  return (
    <>
      <h1>Posts</h1>
      <ul>
        {filteredPosts.map((post) => (
          <li
            key={post.id}
            onClick={createPostSelectHandler(post)}
            style={{ color: post.id === selectedPostId ? "red" : "black" }}
          >
            {post.title}
          </li>
        ))}
      </ul>
    </>
  );
}

回到頂部⬆️


27. 優先選擇命名導出而不是預設導出

我到處都看到預設導出,這讓我很難過🥲。

讓我們比較一下這兩種方法:

/// `Dashboard` is exported as the default component
export function Dashboard(props) {
 /// TODO
}

/// `Dashboard` export is named
export function Dashboard(props) {
 /// TODO
}

我們現在像這樣導入元件:

/// Default export
import Dashboard from "/path/to/Dashboard"

/// Named export
import { Dashboard } from "/path/to/Dashboard"

這些都是預設導出的問題:

  • 如果重新命名元件,IDE 不會自動重新命名匯出。

例如,如果Dashboard重新命名為Console ,我們將得到以下結果:

/// In the default export case, the name is not changed
import Dashboard from "/path/to/Console"

/// In the named export case, the name is changed
import { Console } from "/path/to/Console"
  • 使用預設匯出很難查看從文件中匯出的內容。

例如,在命名導入的情況下,一旦我輸入import { } from "/path/to/file" ,當我將遊標放在括號內時,我就會得到自動補全。

  • 預設導出很難重新導出。

例如,如果我想從索引index重新匯出Dashboard元件,我必須這樣做:

export { default as Dashboard } from "/path/to/Dashboard"

使用命名導出,解決方案更加簡單。

export { Dashboard } from "/path/to/Dashboard"

所以,請預設為命名導出🙏。

💡 注意:即使您使用React惰性,您仍然可以使用命名導出。請參閱此處的範例。

回到頂部⬆️


類別#5:高效率的狀態管理🚦


28.永遠不要為可以從其他狀態或道具衍生出來的值建立狀態

更多的狀態=更多的麻煩。

每個狀態都可以觸發重新渲染,並使重置狀態變得非常麻煩。

因此,如果可以從狀態或道具派生出值,請跳過新增狀態。

❌ 不好: filteredPosts不需要處於狀態。

function App({ posts }) {
  const [filters, setFilters] = useState();
  const [filteredPosts, setFilteredPosts] = useState([]);

  useEffect(
    () => {
      setFilteredPosts(filterPosts(posts, filters));
    },
    [posts, filters]
  );

  return (
    <Dashboard>
      <Filters filters={filters} onFiltersChange={setFilters} />
      {filteredPosts.length > 0 && <Posts posts={filteredPosts} />}
    </Dashboard>
  );
}

✅ 好: filteredPosts源自於postsfilters.

function App({ posts }) {
  const [filters, setFilters] = useState({});
  const filteredPosts = filterPosts(posts, filters)

  return (
    <Dashboard>
      <Filters filters={filters} onFiltersChange={setFilters} />
      {filteredPosts.length > 0 && <Posts posts={filteredPosts} />}
    </Dashboard>
  );
}

回到頂部⬆️


29. 將狀態保持在必要的最低級別,以盡量減少重新渲染

每當元件內部的狀態發生變化時,React 都會重新渲染該元件及其所有子元件(子元件包裹在memo中是一個例外)。

即使這些孩子不使用更改後的狀態,也會發生這種情況。為了最大程度地減少重新渲染,請盡可能將狀態在元件樹中向下移動。

❌ 不好:sortOrder改變時, LeftSidebarRightSidebar都會重新渲染。

function App() {
  const [sortOrder, setSortOrder] = useState("popular");
  return (
    <div className="App">
      <LeftSidebar />
      <Main sortOrder={sortOrder} setSortOrder={setSortOrder} />
      <RightSidebar />
    </div>
  );
}

function Main({ sortOrder, setSortOrder }) {
  return (
    <div>
      <Button
        onClick={() => setSortOrder("popular")}
        active={sortOrder === "popular"}
      >
        Popular
      </Button>
      <Button
        onClick={() => setSortOrder("latest")}
        active={sortOrder === "latest"}
      >
        Latest
      </Button>
    </div>
  );
}

✅ 好: sortOrder更改只會影響Main

function App() {
  return (
    <div className="App">
      <LeftSidebar />
      <Main />
      <RightSidebar />
    </div>
  );
}

function Main() {
  const [sortOrder, setSortOrder] = useState("popular");
  return (
    <div>
      <Button
        onClick={() => setSortOrder("popular")}
        active={sortOrder === "popular"}
      >
        Popular
      </Button>
      <Button
        onClick={() => setSortOrder("latest")}
        active={sortOrder === "latest"}
      >
        Latest
      </Button>
    </div>
  );
}

回到頂部⬆️


30.明確初始狀態和目前狀態的區別

❌ 不好:不清楚 sortOrder 只是初始值,這可能會導致狀態管理的混亂或錯誤。

function Main({ sortOrder }) {
  const [internalSortOrder, setInternalSortOrder] = useState(sortOrder);
  return (
    <div>
      <Button
        onClick={() => setInternalSortOrder("popular")}
        active={internalSortOrder === "popular"}
      >
        Popular
      </Button>
      <Button
        onClick={() => setInternalSortOrder("latest")}
        active={internalSortOrder === "latest"}
      >
        Latest
      </Button>
    </div>
  );
}

✅ 好:命名清楚地表示什麼是初始狀態,什麼是目前狀態。

function Main({ initialSortOrder }) {
  const [sortOrder, setSortOrder] = useState(initialSortOrder);
  return (
    <div>
      <Button
        onClick={() => setSortOrder("popular")}
        active={sortOrder === "popular"}
      >
        Popular
      </Button>
      <Button
        onClick={() => setSortOrder("latest")}
        active={sortOrder === "latest"}
      >
        Latest
      </Button>
    </div>
  );
}

回到頂部⬆️


31. 根據先前的狀態更新狀態,尤其是使用useCallback進行記憶時

React 允許您將 updater 函數從useState傳遞給set函數。

此更新器函數使用目前狀態來計算下一個狀態。

每當我需要根據先前的狀態更新狀態時,我都會使用此行為,尤其是在useCallback.事實上,這種方法不需要將狀態作為鉤子依賴項之一。

❌ 不好:每當todos改變時, handleAddTodohandleRemoveTodo也會改變。

function App() {
  const [todos, setToDos] = useState([]);
  const handleAddTodo = useCallback(
    (todo) => {
      setToDos([...todos, todo]);
    },
    [todos]
  );

  const handleRemoveTodo = useCallback(
    (id) => {
      setToDos(todos.filter((todo) => todo.id !== id));
    },
    [todos]
  );

  return (
    <div className="App">
      <TodoInput onAddTodo={handleAddTodo} />
      <TodoList todos={todos} onRemoveTodo={handleRemoveTodo} />
    </div>
  );
}

✅ 好:即使todos發生變化, handleAddTodohandleRemoveTodo也保持不變。

function App() {
  const [todos, setToDos] = useState([]);
  const handleAddTodo = useCallback((todo) => {
    setToDos((prevTodos) => [...prevTodos, todo]);
  }, []);

  const handleRemoveTodo = useCallback((id) => {
    setToDos((prevTodos) => prevTodos.filter((todo) => todo.id !== id));
  }, []);

  return (
    <div className="App">
      <TodoInput onAddTodo={handleAddTodo} />
      <TodoList todos={todos} onRemoveTodo={handleRemoveTodo} />
    </div>
  );
}

回到頂部⬆️


32. 使用useState中的函數進行延遲初始化和效能提升,因為它們只被呼叫一次。

在 useState 中使用函數可確保初始狀態僅計算一次。

這可以提高效能,特別是當初始狀態源自於「昂貴」操作(例如從本地儲存讀取)時。

❌ 不好:每次元件渲染時我們都會從本地儲存讀取主題

const THEME_LOCAL_STORAGE_KEY = "101-react-tips-theme";

function PageWrapper({ children }) {
  const [theme, setTheme] = useState(
    localStorage.getItem(THEME_LOCAL_STORAGE_KEY) || "dark"
  );

  const handleThemeChange = (theme) => {
    setTheme(theme);
    localStorage.setItem(THEME_LOCAL_STORAGE_KEY, theme);
  };

  return (
    <div
      className="page-wrapper"
      style={{ background: theme === "dark" ? "black" : "white" }}
    >
      <div className="header">
        <button onClick={() => handleThemeChange("dark")}>Dark</button>
        <button onClick={() => handleThemeChange("light")}>Light</button>
      </div>
      <div>{children}</div>
    </div>
  );
}

✅ 好:我們僅在元件安裝時從本地存儲中讀取。

function PageWrapper({ children }) {
  const [theme, setTheme] = useState(
    () => localStorage.getItem(THEME_LOCAL_STORAGE_KEY) || "dark"
  );

  const handleThemeChange = (theme) => {
    setTheme(theme);
    localStorage.setItem(THEME_LOCAL_STORAGE_KEY, theme);
  };

  return (
    <div
      className="page-wrapper"
      style={{ background: theme === "dark" ? "black" : "white" }}
    >
      <div className="header">
        <button onClick={() => handleThemeChange("dark")}>Dark</button>
        <button onClick={() => handleThemeChange("light")}>Light</button>
      </div>
      <div>{children}</div>
    </div>
  );
}

回到頂部⬆️


33. 使用反應上下文來實現廣泛需要的靜態狀態,以防止道具鑽探

每當我有一些資料時,我都會使用 React 上下文:

  • 在多個地方都需要(例如,主題、當前使用者等)

  • 大部分是靜態的或唯讀的(即使用者不能/不經常更改資料)

這種方法有助於避免道具鑽探(即,透過元件層次結構的多個層向下傳遞資料或狀態)。

請參閱下面沙箱中的範例👇。

🏖 沙盒

回到頂部⬆️


34. React Context:將上下文分為經常更改的部分和不經常更改的部分,以提高應用程式效能

React 上下文的一個挑戰是,每當上下文資料發生變化時,所有使用上下文的元件都會重新渲染,即使它們不使用上下文中發生變化的部分🤦‍♀️。

一個辦法?使用單獨的上下文。

在下面的範例中,我們建立了兩個上下文:一個用於操作(恆定),另一個用於狀態(可以更改)。

🏖 沙盒

回到頂部⬆️


35. React Context:當值計算不簡單時引入Provider元件

❌ 缺點: App內有太多邏輯來管理主題。

export function App() {
  const [theme, setTheme] = useState(() =>
    window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
  );
  useEffect(() => {
    const darkThemeMq = window.matchMedia("(prefers-color-scheme: dark)");
    const listener = (event) => {
      setTheme(event.matches ? "dark" : "light");
    };
    darkThemeMq.addEventListener("change", listener);
    return () => darkThemeMq.removeEventListener("change", listener);
  }, []);
  const [selectedPostId, setSelectedPostId] = useState(undefined);
  const onPostSelect = (postId) => {
    // TODO: some logging
    setSelectedPostId(postId);
  };

  return (
    <div className="App">
      <ThemeContext.Provider value={theme}>
        <Dashboard
          posts={posts}
          onPostSelect={onPostSelect}
          selectedPostId={selectedPostId}
        />
      </ThemeContext.Provider>
    </div>
  );
}

✅ 好:主題邏輯封裝在ThemeProvider

export function App() {
  const [selectedPostId, setSelectedPostId] = useState(undefined);
  const onPostSelect = (postId) => {
    // TODO: some logging
    setSelectedPostId(postId);
  };

  return (
    <div className="App">
      <ThemeProvider>
        <Dashboard
          posts={posts}
          onPostSelect={onPostSelect}
          selectedPostId={selectedPostId}
        />
      </ThemeProvider>
    </div>
  );
}

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState(() =>
    window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
  );
  useEffect(() => {
    const darkThemeMq = window.matchMedia("(prefers-color-scheme: dark)");
    const listener = (event) => {
      setTheme(event.matches ? "dark" : "light");
    };
    darkThemeMq.addEventListener("change", listener);
    return () => darkThemeMq.removeEventListener("change", listener);
  }, []);

  return (
    <ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>
  );
}

回到頂部⬆️


36.考慮使用useReducer鉤子作為輕量級狀態管理解決方案

每當我的狀態或複雜狀態中有太多值並且不想依賴外部程式庫時,我就會使用useReducer

當與更廣泛的狀態管理需求的上下文相結合時,它尤其有效。

範例:請參閱#Tip 34

回到頂部⬆️


37. 使用useImmeruseImmerReducer簡化狀態更新

對於useStateuseReducer這樣的鉤子,狀態必須是不可變的(即所有變更都需要建立一個新狀態而不是修改目前狀態)。

這通常很難實現。

這就是useImmeruseImmerReducer提供更簡單的替代方案的地方。它們允許您編寫自動轉換為不可變更新的“可變”程式碼。

❌ 不好:我們必須仔細確保我們正在建立一個新的狀態物件。

export function App() {
  const [{ email, password }, setState] = useState({
    email: "",
    password: "",
  });
  const onEmailChange = (event) => {
    setState((prevState) => ({ ...prevState, email: event.target.value }));
  };
  const onPasswordChange = (event) => {
    setState((prevState) => ({ ...prevState, password: event.target.value }));
  };

  return (
    <div className="App">
      <h1>Welcome</h1>
      <p>
        Email: <input type="email" value={email} onChange={onEmailChange} />
      </p>
      <p>
        Password:{" "}
        <input type="password" value={password} onChange={onPasswordChange} />
      </p>
    </div>
  );
}

✅ 好:我們可以直接修改draftState

import { useImmer } from "use-immer";

export function App() {
  const [{ email, password }, setState] = useImmer({
    email: "",
    password: "",
  });
  const onEmailChange = (event) => {
    setState((draftState) => {
      draftState.email = event.target.value;
    });
  };
  const onPasswordChange = (event) => {
    setState((draftState) => {
      draftState.password = event.target.value;
    });
  };

  /// Rest of logic
}

回到頂部⬆️


38. 使用 Redux(或其他狀態管理解決方案)來在多個元件中存取複雜的用戶端狀態

每當出現以下情況時我都會轉向Redux

  • 我有一個複雜的 FE 應用程式,其中包含許多共享的客戶端狀態(例如儀表板應用程式)

  • 我希望用戶能夠回到過去並恢復更改

  • 我不希望我的元件像 React 上下文那樣不必要地重新渲染

  • 我有太多的情況開始失控

為了獲得簡化的體驗,我建議使用redux-tooltkit

💡 注意:您也可以考慮 Redux 的其他替代方案,例如ZustandRecoil

回到頂部⬆️


39. Redux:使用 Redux DevTools 來除錯你的狀態

Redux DevTools 瀏覽器擴充功能是除錯 Redux 專案的實用工具。

它允許您即時視覺化您的狀態和操作,在刷新過程中保持狀態持久性等等。

要了解其用途,請觀看這段精彩的YouTube 影片

回到頂部⬆️


類別 #6:React 程式碼最佳化 🚀


40. 使用memo防止不必要的重新渲染

當處理渲染成本高昂且其父元件頻繁更新的元件時,將它們包裝在備忘錄中可能會改變遊戲規則。

memo確保元件僅在其 props 變更時重新渲染,而不僅僅是因為其父元件重新渲染。

在下面的範例中,我透過useGetDashboardData從伺服器取得一些資料。如果posts沒有更改,則將ExpensiveList包裝在memo中將阻止它在資料其他部分更新時重新渲染。

export function App() {
  const { profileInfo, posts } = useGetDashboardData();
  return (
    <div className="App">
      <h1>Dashboard</h1>
      <Profile data={profileInfo} />
      <ExpensiveList posts={posts} />
    </div>
  );
}

const ExpensiveList = memo(
  ({ posts }) => {
    /// Rest of implementation
  }
);

💡:一旦React 編譯器變得穩定,這個技巧可能就不再重要了。

回到頂部⬆️


41. 指定一個帶有memo的相等函數來指示 React 如何比較 props。

預設情況下, memo使用Object.is將每個 prop 與其先前的值進行比較。

但是,對於更複雜或特定的場景,指定自訂相等函數可能比預設比較或重新渲染更有效。

例👇

const ExpensiveList = memo(
  ({ posts }) => {
    return <div>{JSON.stringify(posts)}</div>;
  },
  (prevProps, nextProps) => {
    // Only re-render if the last post or the list size changes
    const prevLastPost = prevProps.posts[prevProps.posts.length - 1];
    const nextLastPost = nextProps.posts[nextProps.posts.length - 1];
    return (
      prevLastPost.id === nextLastPost.id &&
      prevProps.posts.length === nextProps.posts.length
    );
  }
)

回到頂部⬆️


42. 聲明記憶元件時,優先使用命名函數而不是箭頭函數

定義記憶元件時,使用命名函數而不是箭頭函數可以提高 React DevTools 中的清晰度。

箭頭函數通常會產生像_c2這樣的通用名稱,這使得偵錯和分析變得更加困難。

❌ 不好:對記憶元件使用箭頭函數會導致 React DevTools 中的名稱資訊量較少。

const ExpensiveList = memo(
  ({ posts }) => {
    /// Rest of implementation
  }
);

<

圖>

ExpenseList名稱不可見

✅ 好:元件的名稱將在 DevTools 中可見。


const ExpensiveList = memo(
  function ExpensiveListFn({ posts }) {
    /// Rest of implementation
  }
)

共有 0 則留言