標籤: 前端開發

  • 只有資深的 React 工程師才知道的事情

    React 對初學者來說可能會有些棘手。然而,理解一些底層原理或技巧可以讓成為高級 React 工程師變得更容易。

    1. useEffect 的清理回調函數在每次渲染時都會執行

    大多數人認為它只在組件卸載時執行,但這並不正確。

    在每次渲染時,來自前一次渲染的清理回調會在下一個效果執行之前執行

    讓我們來看一個例子:

    function SomeComponent() {
      const [count, setCount] = useState(0)
    
      useEffect(() => {
        console.log('The current count is ', count)
        return () => {
          console.log('The previous count is ', count)
        }
      })
    
      return <button onClick={() => setCount(count + 1)}>Click me</button>
    }

    這會記錄以下內容:

    // Component mounts
    The current count is 0
    
    // Click
    The previous count is 0
    The current count is 1
    
    // Click
    The previous count is 1
    The current count is 2
    
    // Component unmounts
    The previous count is 2

    這對於創建「取消訂閱然後立即重新訂閱」的模式非常有用,這也是 useEffect 唯一應該被使用的情況。

    當然,如果我們添加了依賴數組,這些東西只會在依賴項改變時被調用。

    2. useEffect 是一個低階工具,應該僅在類似庫的程式碼中使用

    初級 React 開發者經常在不必要的情況下使用 useEffect,這可能會使程式碼更加複雜,產生閃爍或微妙的錯誤。

    最常見的情況是同步不同的 useStates,而實際上你只需要一個 useState:

    function MyComponent() {
      const [text, setText] = useState("Lorem ipsum dolor sit amet")
    
      // You don't need to do this !!!
      const [trimmedText, setTrimmedText] = useState("Lorem ip...")
    
      useEffect(() => {
        setTrimmedText(text.slice(0, 8) + '...')
      }, [text])
    }
    
    
    function MyBetterComponent() {
      const [text, setText] = useState("Lorem ipsum dolor sit amet")
    
      // Do this instead:
      // (each time text changes, the component will re-render so trimmedText
      //  will be up-to-date)
      const trimmedText = text.slice(0, 8) + '...'
    }

    3. 使用 key 屬性來重置內部狀態

    當元素上的 key prop 發生變化時,該元素的渲染不會被解釋為更新,而是被視為卸載並重新掛載一個具有全新狀態的全新組件實例。

    function Layout({ currentItem }) {
      /* When currentItem changes, we want any useState inside <EditForm/>
         to be reset to a new initial value corresponding to the new item */
      return (
        <EditForm
          item={currentItem}
          key={currentItem.id}
        />
      )
    }

    4. 不要將伺服器狀態放在 useState 中

    伺服器狀態大致上是你的資料庫在頁面加載時存在於前端記憶體中的一個快照。

    通常由伺服器狀態管理器管理,例如 react-query 或 Apollo。

    如果你將其中任何一部分放入 `useState`,當查詢刷新或發生變異時,`useState` 的內容將不會更新。

    5. ReactElement 與 ReactNode

    ReactElement 僅代表一個標記片段,而 ReactNode 可以是任何 React 可以渲染的東西,例如 ReactElement,但也包括 stringnumberbooleanarraynullundefined 等。

    // this is a ReactElement
    const a = <div/>
    
    // these are ReactNodes
    1
    "hello"
    <div/>
    [2, <span/>]
    null

    始終將children屬性類型設為ReactNode,這樣你就不會限制該組件可以放入什麼樣的子元素。

    JSX.Element 是 TypeScript 的一個內部功能(並非由 React 庫定義),主要針對庫開發者。除此之外,它等同於 ReactElement

  • 不要這樣使用 TypeScript 類型。改用 Map Pattern 吧。

    不要這樣使用 TypeScript 類型。改用 Map Pattern 吧。

    介紹

    在處理一個實際專案時,我遇到了一個特定的 TypeScript 實現,它雖然功能正常,但缺乏靈活性。在這篇部落格中,我將帶你了解我遇到的問題,以及如何通過使用Map 模式來改進設計,使其更具動態性。

    這個問題

    我遇到了這個 TypeScript 類型:

    // FinalResponse.ts
    import { Reaction } from './Reaction'
    
    export type FinalResponse = {
      totalScore: number
      headingsPenalty: number
      sentencesPenalty: number
      charactersPenalty: number
      wordsPenalty: number
      headings: string[]
      sentences: string[]
      words: string[]
      links: { href: string; text: string }[]
      exceeded: {
        exceededSentences: string[]
        repeatedWords: { word: string; count: number }[]
      }
      reactions: {
        likes: Reaction
        unicorns: Reaction
        explodingHeads: Reaction
        raisedHands: Reaction
        fire: Reaction
      }
    }

    此外,這個 Reaction 類型已被定義:

    // Reaction.ts
    export type Reaction = {
      count: number
      percentage: number
    }

    而這在一個函數中是這樣使用的:

    // calculator.ts
    export const calculateScore = (
      headings: string[],
      sentences: string[],
      words: string[],
      totalPostCharactersCount: number,
      links: { href: string; text: string }[],
      reactions: {
        likes: Reaction
        unicorns: Reaction
        explodingHeads: Reaction
        raisedHands: Reaction
        fire: Reaction
      },
    ): FinalResponse => {
      // Score calculation logic...
    }

    這種方法的問題

    現在,想像一下開發者需要添加新反應(例如,愛心、鼓掌等)的情境。
    鑑於目前的設置,他們必須:

    • 修改 FinalResponse.ts 檔案以新增反應類型。
    • 如有必要,請更新 Reaction.ts 類型。
    • 修改 calculateScore 函數以包含新的反應。
    • 可能需要更新依賴此結構的應用程式的其他部分。

    因此,他們不是在一個地方添加新的反應,而是最終在三個或更多文件中進行更改,這增加了錯誤和冗餘的可能性。這種方法是緊密耦合的

    解決方案

    我提出了一個更乾淨的解決方案,通過引入一個更靈活且可重用的結構。

    // FinalResponse.ts
    import { Reaction } from './Reaction'
    
    export type ReactionMap = Record<string, Reaction>
    
    export type FinalResponse = {
      totalScore: number
      headingsPenalty: number
      sentencesPenalty: number
      charactersPenalty: number
      wordsPenalty: number
      headings: string[]
      sentences: string[]
      words: string[]
      links: { href: string; text: string }[]
      exceeded: {
        exceededSentences: string[]
        repeatedWords: { word: string; count: number }[]
      }
      reactions: ReactionMap
    }

    解釋:

    • ReactionMap: 這個類型使用了 Record<string, Reaction>,這意味著任何字串都可以作為鍵,而值將始終是 Reaction 類型。
    • FinalResponse: 現在,FinalResponse 中的 reactions 欄位是 ReactionMap 類型,讓你可以動態添加任何反應,而無需修改多個文件。

    乾淨的程式碼

    在 calculator.ts 文件中,函數現在看起來像這樣:

    // calculator.ts
    export const calculateScore = (
      headings: string[],
      sentences: string[],
      words: string[],
      totalPostCharactersCount: number,
      links: { href: string; text: string }[],
      reactions: ReactionMap,
    ): FinalResponse => {
      // Score calculation logic...
    }

    但等等!我們需要一些控制

    雖然新的解決方案提供了靈活性,但它也帶來了添加未經檢查反應的風險,這意味著任何人都可能將任何字串作為反應添加。我們絕對不希望這樣。

    為了解決這個問題,我們可以對允許的反應實施更嚴格的控制。

    更安全的解決方案

    這是更新後的版本,我們將反應限制在預先定義的一組允許值中:

    // FinalResponse.ts
    import { Reaction } from './Reaction'
    
    type AllowedReactions =
      | 'likes'
      | 'unicorns'
      | 'explodingHeads'
      | 'raisedHands'
      | 'fire'
    
    export type ReactionMap = {
      [key in AllowedReactions]: Reaction
    }
    
    export type FinalResponse = {
      totalScore: number
      headingsPenalty: number
      sentencesPenalty: number
      charactersPenalty: number
      wordsPenalty: number
      headings: string[]
      sentences: string[]
      words: string[]
      links: { href: string; text: string }[]
      exceeded: {
        exceededSentences: string[]
        repeatedWords: { word: string; count: number }[]
      }
      reactions: ReactionMap
    }

    視覺化呈現

    結論

    這種方法在靈活性與控制之間取得了平衡:

    • 靈活性:你可以通過僅修改AllowedReactions類型來輕鬆添加新的反應。
    • 控制: 使用聯合類型確保只能使用允許的反應,防止添加無效或不需要的反應的風險。

    這段程式碼遵循了開放/封閉原則 (OCP),透過擴展來新增功能,而無需修改現有程式碼。

    使用這種模式,我們可以輕鬆擴展反應列表,而無需修改太多文件,同時仍然嚴格控制可以添加的內容。