React官方文件導讀 | 也許你不需要使用Effect | You Might Not Need an Effect

·

9 min read

React官方文件導讀 | 也許你不需要使用Effect | You Might Not Need an Effect

前言:

此篇文章為React官方的You Might Not Need an Effect 透過我自己的理解來做翻譯與筆記。

🌟 這篇文章可以幫助我們更了解Effect, State的時機來改善效能,以及避免新手常掉入無限render的陷阱。


我們都知道Effect 是在render完畢、commit完畢、DOM更新完畢後才會去執行Effect,所以Effect沒用好可能會造成不必要的re-render。

常見的不必要的Effects:

  1. 你不需要在Effects 中做資料轉換(例如處理資料後又setState回去)

  2. 一些可以在Event Handler可以處理的事情

Effects 是讓我們來做一些**“外部系統”**需要處理的事情在做使用的。


基於props 或state 來更新 useState | Updating state based on props or state

這個範例展示了我們常常需要將fristName結合lastName成一個新變數。 在render的過程中React本來就會做calculated的動作,我們不需要多用一個useState來保存fullName

function Form() {
  const [firstName, setFirstName] = useState('Taylor');
  const [lastName, setLastName] = useState('Swift');

  // 🔴 Avoid: redundant state and unnecessary Effect
  const [fullName, setFullName] = useState('');
  useEffect(() => {
    setFullName(firstName + ' ' + lastName);
  }, [firstName, lastName]);
  // ...
}
function Form() {
  const [firstName, setFirstName] = useState('Taylor');
  const [lastName, setLastName] = useState('Swift');
  // ✅ Good: calculated during rendering
  const fullName = firstName + ' ' + lastName;
  // ...
}

這個例子我們可以記住一個原則:

When something can be calculated from the existing props or state, don’t put it in state. Instead, calculate it during rendering. 當一個變數可以透過propsstate 來取得結果,不要再將它宣告成一個state。相反的你只需要在render時透過calculate來取得它。


在useEffect中做複雜的運算 | Caching expensive calculations

useEffect不是讓我們拿來做複雜的運算,這個例子展示一個TodoList常見的狀況,當todosfilter更新時可以立刻取得一個visibleTodos,我們可能會想到下面的寫法:

function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');

  // 🔴 Avoid: redundant state and unnecessary Effect
  const [visibleTodos, setVisibleTodos] = useState([]);
  useEffect(() => {
    setVisibleTodos(getFilteredTodos(todos, filter));
  }, [todos, filter]);

  // ...
}

這個情況與1的情況有點像,我們不需要多用setState來保存visibleTodos,只需要像下面程式碼一寫撰寫即可,因為我們在render時本來就能做calculated的動作,大部分的案例下面的寫法都能運行!

function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');
  // ✅ This is fine if getFilteredTodos() is not slow.
  const visibleTodos = getFilteredTodos(todos, filter);
  // ...
}

但也許某些案例時,getFilteredTodos()非常耗效能(也許你有很多todos?),在下面案例展示當你不希望一些其他像是newTodo等的變數改變造成重新計算getFilteredTodos(),可以用useMemo來包裝visibleTodos

import { useMemo, useState } from 'react';

function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');
  const visibleTodos = useMemo(() => {
    // ✅ Does not re-run unless todos or filter change
    return getFilteredTodos(todos, filter);
  }, [todos, filter]);
  // ...
}

或寫的更簡潔,像下面只有一行:

import { useMemo, useState } from 'react';

function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');
  // ✅ Does not re-run getFilteredTodos() unless todos or filter change
  const visibleTodos = useMemo(() => getFilteredTodos(todos, filter), [todos, filter]);
  // ...
}

像這樣透過useMemo,在每一個render時React都會透過比對todosfilter是否有不同,來決定是否重新計算visibleTodos


當props 改變時重置元件狀態 | Resetting all state when a prop changes

這個例子是一個ProfilePage元件,傳入有userId這個props。當傳入的userId有變動時我們需要重置元件中的comment,第一個直覺我們可能會想到這樣撰寫程式碼:

export default function ProfilePage({ userId }) {
  const [comment, setComment] = useState('');

  // 🔴 Avoid: Resetting state on prop change in an Effect
  useEffect(() => {
    setComment('');
  }, [userId]);
  // ...
}

React官方文件表示這是一個效率低下的做法,且會在第一次render時處理兩次comment = '';。React官方提供另一個寫法,將ProfilePage分割一個component <Profile>出來,並使用userId作為key來傳給<Profile>

export default function ProfilePage({ userId }) {
  return (
    <Profile
      userId={userId}
      key={userId}
    />
  );
}

function Profile({ userId }) {
  // ✅ This and any other state below will reset on key change automatically
  const [comment, setComment] = useState('');
  // ...
}

為什麼這樣能達成props改變時重置狀態? 在官方文件這段有一段粗體字:

By passing userId as a key to the Profile component, you’re asking React to treat two Profile components with different userId as two different components that should not share any state.

當使用userId 作為key值給Profile component使用,等於你像React要求當userId改變時Profile componet 即等於新的componet,新的cpmonent不會與舊component共享任何狀態。

再白話翻譯:React 提供的key屬性本身特性即為當key值改變,裡面的component 狀態會還原成初始化的狀態,來達成每個props 改變時都需要重置元件狀態的效果。


當props 改變同時修改state | Adjusting some state when a prop changes

這個範例與展示一個列表,當傳入的items改變時,我們希望原本已經選定好的Selection狀態能改成null,下面是常見的錯誤寫法:

function List({ items }) {
  const [isReverse, setIsReverse] = useState(false);
  const [selection, setSelection] = useState(null);

  // 🔴 Avoid: Adjusting state on prop change in an Effect
  useEffect(() => {
    setSelection(null);
  }, [items]);
  // ...
}

會造成的副作用與前幾個有點像,即第一次render時selection這個state會被賦予值兩次造成re-render。

React官方提供另一個解決方式,在<List>宣告一個放入props itemsstate,名為prevItemsprevItems 會先記錄最初始的props items。並且在每次render時我們都做一次比對props items 是否一致,不一致時才更新selection,可以避免前一個寫法造成的re-render。

function List({ items }) {
  const [isReverse, setIsReverse] = useState(false);
  const [selection, setSelection] = useState(null);

  // Better: Adjust the state while rendering
  const [prevItems, setPrevItems] = useState(items);
  if (items !== prevItems) {
    setPrevItems(items);
    setSelection(null);
  }
  // ...
}

React 官方推薦我們使用這種「儲存前一次render的資訊 Storing information from previous renders 」,來取代使用useEffect帶入參數調整state的方式。但事實上官方也知道這樣撰寫的pattern雖然比Effect高效,但也同時造成較難以閱讀與維護,大部分情況還是以參照「當props 改變時重置元件狀態」或 「常見的基於props 或state 來update useState」來做撰寫即可。

例如,我們不要儲存舊的items,我們改將selection這個state改成selectedId,並在render同時calculate selection

function List({ items }) {
  const [isReverse, setIsReverse] = useState(false);
  const [selectedId, setSelectedId] = useState(null);
  // ✅ Best: Calculate everything during rendering
  const selection = items.find(item => item.id === selectedId) ?? null;
  // ...
}

當使用者曾經selected,值會儲存在selectedId這個state,接著props items更新造成render時,我們比對selectIditems,如果有比對到值還可以保持使用者的選擇狀態。


將邏輯移動到Event Handle | Sharing logic between event handlers

這個案例我覺得比較像是思考該事件的邏輯應該放在哪、該怎麼拆,避免濫用Effect。

如以下案例是一個ProductPage,有handleByClick()handleCheckoutClick()兩個handler,和addToCartnavigateToshowNotification三個方法。 當點擊產品時會觸發addToCart(product)、與顯示showNotification提示已將產品加入購物車的Notification

function ProductPage({ product, addToCart }) {
  // 🔴 Avoid: Event-specific logic inside an Effect
  useEffect(() => {
    if (product.isInCart) {
      showNotification(`Added ${product.name} to the shopping cart!`);
    }
  }, [product]);

  function handleBuyClick() {
    addToCart(product);
  }

  function handleCheckoutClick() {
    addToCart(product);
    navigateTo('/checkout');
  }
  // ...
}

這樣的邏輯有明顯的錯誤,甚至會造成bug。想像如果ProductPage每次都會讀取cookielocalStorge來讀取使用者曾加入購物車的商品,這樣會造成刷新頁面時如果product.isInCarttrue就會跳一次Notification

When you’re not sure whether some code should be in an Effect or in an event handler, ask yourself why this code needs to run. Use Effects only for code that should run because the component was displayed to the user

當你不確定一些code應該放在Effect還是event handler時,試問自己這些code是什麼時候需要觸發?UseEffect應該只用在當component display給使用者時所觸發使用。

在這個案例中,showNotification應該是只有在使用者按下按鈕將產品加入購物車時才需要觸發,而不是因為頁面顯示。我們可以將按下按鈕將產品加入購物車的邏輯拆出來:

function ProductPage({ product, addToCart }) {
  // ✅ Good: Event-specific logic is called from event handlers
  function buyProduct() {
    addToCart(product);
    showNotification(`Added ${product.name} to the shopping cart!`);
  }

  function handleBuyClick() {
    buyProduct();
  }

  function handleCheckoutClick() {
    buyProduct();
    navigateTo('/checkout');
  }
  // ...
}

這樣可以同時達成移除Effect,並且修正bug,也讓邏輯比較清楚。


發出POST 請求 | Sending a POST request

這個案例是一個表單,會發出兩種POST請求。一種是當component mounts必須發出分析事件,另一則是按下Submit按鈕觸發onSubmit時發出/api/register

function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');

  // ✅ Good: This logic should run because the component was displayed
  useEffect(() => {
    post('/analytics/event', { eventName: 'visit_form' });
  }, []);

  // 🔴 Avoid: Event-specific logic inside an Effect
  const [jsonToSubmit, setJsonToSubmit] = useState(null);
  useEffect(() => {
    if (jsonToSubmit !== null) {
      post('/api/register', jsonToSubmit);
    }
  }, [jsonToSubmit]);

  function handleSubmit(e) {
    e.preventDefault();
    setJsonToSubmit({ firstName, lastName });
  }
  // ...
}

這個案例正確與錯誤相對好懂,component mounts時發出分析事件使用Effect是正確的,除了在開發環境會有觸發兩次的問題,不過僅限開發環境所以沒關係。但將POST/api/register放入Effect不僅浪費效能,還會造成程式碼較難閱讀與維護的問題。

用「將邏輯移動到Event Handler」的邏輯來看 /api/register,這個POST request的觸發實際並不是因為component display而觸發,是因為submitButton被點擊而被觸發,所以較適合的寫法為將Event Handler邏輯集中:

function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');

  // ✅ Good: This logic runs because the component was displayed
  useEffect(() => {
    post('/analytics/event', { eventName: 'visit_form' });
  }, []);

  function handleSubmit(e) {
    e.preventDefault();
    // ✅ Good: Event-specific logic is in the event handler
    post('/api/register', { firstName, lastName });
  }
  // ...
}

串連的計算 | Chains of computations

當一個元件有許多相依的因素來計算各個變數,可能會聯想到這樣做:

function Game() {
  const [card, setCard] = useState(null);
  const [goldCardCount, setGoldCardCount] = useState(0);
  const [round, setRound] = useState(1);
  const [isGameOver, setIsGameOver] = useState(false);

  // 🔴 Avoid: Chains of Effects that adjust the state solely to trigger each other
  useEffect(() => {
    if (card !== null && card.gold) {
      setGoldCardCount(c => c + 1);
    }
  }, [card]);

  useEffect(() => {
    if (goldCardCount > 3) {
      setRound(r => r + 1)
      setGoldCardCount(0);
    }
  }, [goldCardCount]);

  useEffect(() => {
    if (round > 5) {
      setIsGameOver(true);
    }
  }, [round]);

  useEffect(() => {
    alert('Good game!');
  }, [isGameOver]);

  function handlePlaceCard(nextCard) {
    if (isGameOver) {
      throw Error('Game already ended.');
    } else {
      setCard(nextCard);
    }
  }

  // ...

這樣的寫法會有兩種問題: 這會造成這個component(以及它的children)效能極差,最糟的狀況會是(setCard → render → setGoldCardCount → render → setRound → render → setIsGameOver → render),有三次不必要的re-render產生。

即使你的代碼負擔不大、並沒有太多的效能問題。隨著產品發展下去來你可能會遇到這個**“串連Chain”**不符合新需求的狀況。想像接下來有一個「回到上一步」的需求,需要透過將每個state更新為過去的值來實現,但將state更新為過去的值又會觸發這段串連,進而又再次改變已經修改好的state。

這兩個問題都展示出這樣串連Effect會讓我們代碼擴展性極差。 在這個案例中較好的解決方法

  1. 解析出能在render時calculate的變數。

  2. state調整至EventHandler中。

function Game() {
  const [card, setCard] = useState(null);
  const [goldCardCount, setGoldCardCount] = useState(0);
  const [round, setRound] = useState(1);

  // ✅ Calculate what you can during rendering
  const isGameOver = round > 5;

  function handlePlaceCard(nextCard) {
    if (isGameOver) {
      throw Error('Game already ended.');
    }

    // ✅ Calculate all the next state in the event handler
    setCard(nextCard);
    if (nextCard.gold) {
      if (goldCardCount <= 3) {
        setGoldCardCount(goldCardCount + 1);
      } else {
        setGoldCardCount(0);
        setRound(round + 1);
        if (round === 5) {
          alert('Good game!');
        }
      }
    }
  }

  // ...

app初始化 | Initializing the application

我們會希望在app初始化時運作一些邏輯,所以我們會將程式碼放在根元件的Effect中:

function App() {
  // 🔴 Avoid: Effects with logic that should only ever run once
  useEffect(() => {
    loadDataFromLocalStorage();
    checkAuthToken();
  }, []);
  // ...
}

這兩個初始化的function看起來沒問題,但有時我們也希望即使在開發模式也只需要執行一次初始化。例如:在確認token時可能因為在Strict mode執行兩次而造成token失效。 如果我們希望一段程式碼不僅在根元件掛載(mount)時運行一次,而是只在整個app應用加載(load)時運行一次即可,我們可以在根元件外新增一個變數來追蹤是否已執行過初始化:

let didInit = false;

function App() {
  useEffect(() => {
    if (!didInit) {
      didInit = true;
      // ✅ Only runs once per app load
      loadDataFromLocalStorage();
      checkAuthToken();
    }
  }, []);
  // ...
}

甚至在根元件外直接執行,這樣可以達成app render前完成初始化的動作:

if (typeof window !== 'undefined') { // Check if we're running in the browser.
   // ✅ Only runs once per app load
  checkAuthToken();
  loadDataFromLocalStorage();
}

function App() {
  // ...
}

只運行一次有關初始化的程式碼對效能與避免非預期錯誤是很重要的,官網也強調將App初始化的邏輯都建議放在根元件或整個App的入口,不要在其他的component濫用這種方法。


通知父元件更改狀態 | Notifying parent components about state changes

這個案例展示一個用來切換狀態的子元件<Toggle>,有一個isOnstate,可能為turefalse,使用者可能會用click或drag的方式改變isOn,當isOn的值改變時就必須通知父元件:

function Toggle({ onChange }) {
  const [isOn, setIsOn] = useState(false);

  // 🔴 Avoid: The onChange handler runs too late
  useEffect(() => {
    onChange(isOn);
  }, [isOn, onChange])

  function handleClick() {
    setIsOn(!isOn);
  }

  function handleDragEnd(e) {
    if (isCloserToRightEdge(e)) {
      setIsOn(true);
    } else {
      setIsOn(false);
    }
  }

  // ...
}

這個案例與前幾個案例差不多,都會造成元件re-render次,且邏輯不明確。isOn的值會改變是來自於使用者click或Drag觸發isOn的更新、接下來再觸發onChange來通知父元件state改變。我們可以仿造前幾個案例將更新事件集中處理:

function Toggle({ onChange }) {
  const [isOn, setIsOn] = useState(false);

  function updateToggle(nextIsOn) {
    // ✅ Good: Perform all updates during the event that caused them
    setIsOn(nextIsOn);
    onChange(nextIsOn);
  }

  function handleClick() {
    updateToggle(!isOn);
  }

  function handleDragEnd(e) {
    if (isCloserToRightEdge(e)) {
      updateToggle(true);
    } else {
      updateToggle(false);
    }
  }

  // ...
}

透過這個方式,並且React會將來自不同組件的change批次做處理,讓我們每次改變都只需要render一次。 再觀察這個案例可以發現,我們觸發isOn的改變會同時改變兩個state(子元件與父元件的state),那我們可以改由父元件傳入isOnstate

// ✅ Also good: the component is fully controlled by its parent
function Toggle({ isOn, onChange }) {
  function handleClick() {
    onChange(!isOn);
  }

  function handleDragEnd(e) {
    if (isCloserToRightEdge(e)) {
      onChange(true);
    } else {
      onChange(false);
    }
  }

  // ...
}

這樣更符合React的“Lifting state up” ,共享state來減少子元件需要管理與同步變數的問題,只需要專心處理事件邏輯即可!


子元件傳值給父元件 | Passing data to the parent

這個案例展示當我們在子元件fetch API後,將結果回傳給父元件:

function Parent() {
  const [data, setData] = useState(null);
  // ...
  return <Child onFetched={setData} />;
}

function Child({ onFetched }) {
  const data = useSomeAPI();
  // 🔴 Avoid: Passing data to the parent in an Effect
  useEffect(() => {
    if (data) {
      onFetched(data);
    }
  }, [onFetched, data]);
  // ...
}

React官方希望我們遵循由父元件傳遞資料給子元件的資料流動,方便我們追蹤資料流向。如果子元件在Effect中改變了父元件的state會讓後續維護變得難以追蹤。如果父元件與子元件都需要同一組資料,我們應該保持由父元件來執行fetch API再將資料傳給子元件:

function Parent() {
  const data = useSomeAPI();
  // ...
  // ✅ Good: Passing data down to the child
  return <Child data={data} />;
}

function Child({ data }) {
  // ...
}

這樣修改能更保持數據可追蹤性,且更高效能!


從第三方library或瀏覽器拿取資料 | Subscribing to an external store

有時我們資料來源會是React本身以外的地方,例如第三方library或瀏覽器本身(built-in browser API)的資料。有些時候React沒辦法監聽這些數據是否有變動,所以常會出現使用Effect來進行手動監聽並state的動作:

function useOnlineStatus() {
  // Not ideal: Manual store subscription in an Effect
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    function updateState() {
      setIsOnline(navigator.onLine);
    }

    updateState();

    window.addEventListener('online', updateState);
    window.addEventListener('offline', updateState);
    return () => {
      window.removeEventListener('online', updateState);
      window.removeEventListener('offline', updateState);
    };
  }, []);
  return isOnline;
}

function ChatIndicator() {
  const isOnline = useOnlineStatus();
  // ...
}

以上案例展示了手動監聽外部資料(瀏覽器的navigator.onLine API),因為最初渲染時這隻API並不在Serve上,所以在資料回來前都會initial HTML都會將這個值渲染成ture。 這個做法並非不可行,甚至很常見,但React有提供useSyncExternalStore這個Hook,專門處理監聽第三方資料的問題:

function subscribe(callback) {
  window.addEventListener('online', callback);
  window.addEventListener('offline', callback);
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}

function useOnlineStatus() {
  // ✅ Good: Subscribing to an external store with a built-in Hook
  return useSyncExternalStore(
    subscribe, // React won't resubscribe for as long as you pass the same function
    () => navigator.onLine, // How to get the value on the client
    () => true // How to get the value on the server
  );
}

function ChatIndicator() {
  const isOnline = useOnlineStatus();
  // ...
}

useSyncExternalStore 本身是一隻協助我們訂閱外部store的hook,初階用法可以詳閱useSyncExternalStore 官方介紹 Subscribing to a browser API的章節。

獲取數據 | Fetching Data

最大部分我們都會用useEffect 來發出API請求得到數據,想像在製作一個有Input提供使用者輸入搜尋內容,且會實時發出API請求並且setResult的頁面:

function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  const [page, setPage] = useState(1);

  useEffect(() => {
    // 🔴 Avoid: Fetching without cleanup logic
    fetchResults(query, page).then(json => {
      setResults(json);
    });
  }, [query, page]);

  function handleNextPageClick() {
    setPage(page + 1);
  }
  // ...
}

這個案例不需要像前幾個案例一樣,將邏輯移動到EventHandler中。
你可能會覺得矛盾,因為要觸發fetchResults不就是因為使用者有輸入(typing)或換頁(NextPageClick)這個動作嗎?
回歸這個input 與頁面顯示的關聯,我們最主要希望當這個component顯示時,保持results這個state的data 與querypage保持同步。
然後這樣會產生一個bug,想像當使用者快速輸入"hello"query會快速接收"h""he""hell""hello"等變化。等於發送數個API請求,接著這些API請求可能會產生"he"回傳的速度比"hello"還要晚回來,接著會發生query"hello",但畫面顯示"he"的搜尋結果。

這又稱“race condition”:發出兩個不同的API request 互相賽跑,但responce出現的速度不如預期。

想修正“race condition”的錯誤,應該加入一個cleanup function去忽略
舊的responses:

function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  const [page, setPage] = useState(1);
  useEffect(() => {
    let ignore = false;
    fetchResults(query, page).then(json => {
      if (!ignore) {
        setResults(json);
      }
    });
    return () => {
      ignore = true;
    };
  }, [query, page]);

  function handleNextPageClick() {
    setPage(page + 1);
  }
  // ...
}

這樣可以確保當Effect連續觸發fetches data,我們只使用最後一個請求所得到的結果。
在Effect fetch data不單要思考“race condition”的問題。有些情況我們會希望catching responses (也許在希望使用者點擊上一頁按鈕時能保持原本狀態)、或處理Serve-side render,以及避免網路瀑布network waterfalls(讓子元件不需等待任何父組件就執行fetch data)

這些問題適用於所有UI libray,這也是為什麼每個frameworks 都會提供比Effect 更多的fetch data的解決方案(例如Next.js, Remix, Gatsby, Expo (for native apps) …)

但不是每個專案的規模都需要framework,如果我們希望使用Effect 來fetch data並且考慮到上述問題的優化,可以參考下方案例:

function SearchResults({ query }) {
  const [page, setPage] = useState(1);
  const params = new URLSearchParams({ query, page });
  // ⬇️ 將results 使用自定義的hook回傳
  const results = useData(`/api/search?${params}`);

  function handleNextPageClick() {
    setPage(page + 1);
  }
  // ...
}

function useData(url) {
  const [data, setData] = useState(null);
  useEffect(() => {
    let ignore = false;
    fetch(url)
      .then(response => response.json())
      .then(json => {
        if (!ignore) {
          setData(json);
        }
      });
    return () => {
      ignore = true;
    };
  }, [url]);
  return data;
}

這個案例展示了自定義一個useData的自定義hook,可以自行添加error handle或追蹤結果是否已經回傳(也可以在React ecosystem尋找很多種解決方案),雖然這種解決方案不如使用framework來得有效率,但將data fetch 移至自定義的hook能幫助我們更有效率的獲取data以及其他動作。

總結來說,當我們認為必須撰寫Effect時,都再思考一次我們是否有其他替代性方案?(如上方自定義的Hook或useSyncExternalStore來替代Effect),逐步在component減少Effect的數量會發現後續維護越來越容易。

🌱 複習重點

  • 如果有一個變數是因為props改變而需要重新calculate,我們不需要用state定義它,僅需要宣告一個變數靠重新渲染時重新計算即可。

  • 如果有一個邏輯我們不知道是否要放在Effect,只需要問自己這個邏輯觸發的時機,如果不是因為componet display而觸發,就將邏輯集中在Event Handler。

  • 嘗試由父元件來分享state給子元件,不需要父子元件分別管理不同state而需煩惱同步邏輯。

  • 想要重置component的 state,嘗試pass將不同的key即可。

  • 當props改變時希望重置部分state,只需要在render時直接重置。

  • 當我們需要同時更新多個component的state,最好在單個event一次解決。

  • 需要在不同component同步不同state時,請考慮lifting state up(將資料流保持父傳子)

  • 在Effect fetch data時,務必注意“race condition”的問題。

參考文章:

You Might Not Need an Effect

pjchender-[React] You Might Not Need an Effect: Part 1