你可能不需要使用Effect

29 阅读8分钟

Effect是使用来使组件与外部系统同步,比如非React组件、网络或浏览器dom。如不涉及外部系统,比如,你想在某个状态改变时候更新另一个状态,则不需要Effect


移除不需要的Effect

不要使用Effect来转换数据来进行渲染。 例如,你想在列表显示之前进行过滤。可能会写一个Effect进行监听list,然后setFilteredList。这是不必要的,可以直接在组件顶层执行filter来过滤数据,这样组件在每次渲染时候都会进行过滤列表。如果过滤逻辑很复杂,则使用useMemo

不要使用Effect处理用户事件。 例如,点击一个按钮会将物品加入列表,不要监听列表的变化去发送请求,而是在按钮点击函数内进行处理事件发送请求。

示例

当数据依赖于props或者state

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]);
  // ...
}

可以在组件渲染期间生成fullName

function Form() {
  const [firstName, setFirstName] = useState('Taylor');
  const [lastName, setLastName] = useState('Swift');
  // ✅ Good: calculated during rendering
  const fullName = firstName + ' ' + lastName;
  // ...
}

缓存昂贵的计算

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]);

  // ...
}

可以在渲染期间进行过滤

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

如果计算消耗过多性能,可以使用useMemo监听数据变化,进行复杂计算

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]);
  // ...
}

如何判断合适需要使用useMemo

使用 console.time ``console.timeEnd进行两种方式的代码运行时间计算。

重置所有的state当一个props变化时候

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

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

使用userId作为组件的key,当userId变化时候,key更新,组件会被重置。

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变化

当items发生变化时候,你要更新当前组件选中的选项为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]);
  // ...
}

上述方式,在items变化时候,React会先使用之前的selection进行渲染,然后更新dom并运行Effect,最后执行setSelection(null)将选中的值置为null,这样组件会进行一次不必要的重新渲染。

相反,应在渲染期间直接调整状态

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会丢弃jsx并执行重新渲染。React 还没有渲染 List children 或更新 DOM,所以这让 List children 跳过渲染陈旧的选择值。注意写判断条件避免react进入重新渲染的死循环。

当然,无论怎么做,基于props进行调整当前组件的状态会让数据流难以管理。你可以直接存储选中的id,而非选中的选项。

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;
  // ...
}

存储组件选中的id作为组件内部的状态,这样组件的状态不需要依赖于props。渲染的内容可以直接通过id进行筛选。

在事件处理之间共享逻辑

假设您有一个带有两个按钮(购买和结帐)的产品页面,这两个按钮都可以让您购买该产品。 您希望在用户将产品放入购物车时显示通知。 在两个按钮的点击处理程序中调用 showNotification() 感觉很重复,因此您可能想将此逻辑放在 Effect 中:

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');
  }
  // ...
}

如果页面会记录当前的购物车的商品,那么每次页面刷新时候也会触发showNotification,这是bug。

当不确定一个事件应该放在effect中还是事件处理函数中的时候。要判断一下这个代码为什么要执行,Effect仅仅使用在当组件已经渲染完成。

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');
  }
  // ...
}

监听状态变化发送请求

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 });
  }
  // ...
}

发送请求的原因不是来自现在表单渲染的数据,只是想在特定的时间发送请求,这个时刻是用户点击提交按钮,那么发送请求应该放在按钮点击的时间处理函数中

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 });
  }
  // ...
}

当选择将一些逻辑放在事件处理函数还是Effect中时候。要从用户的角度判断它是什么逻辑。如果逻辑是由用户的操作引起的,那么逻辑应该放在事件处理函数中,如果逻辑是用户在屏幕上看到的组件引起的,那么使用Effect

计算链

有时您可能会想链接 Effects,每个 Effects 都根据其他状态调整一个状态:

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);
    }
  }

  // ...

多次使用Effect会使组件的逻辑越来越复杂,而且会使代码运行速度变慢。上述代码还有不必要的state引起组件不必要的重新渲染。

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!');
        }
      }
    }
  }

  // ...

在组件的渲染期间进行判断计算,在事件处理函数中取下一次state进行计算。

初始化

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

在生产环境中是没问题的,但是在开发环境中,useEffect中的代码会执行两次,可能会导致错误,而且代码在不同环境中的效果应该是相同的。

可以添加一个顶级变量(组件外变量)来判断组件是否运行过。

let didInit = false;

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

当组件被引入的时候,顶层代码将会执行一次,即使组件没有渲染。

通知父组件组件状态变化

假设您正在编写一个带有内部 isOn 状态的 Toggle 组件,该状态可以是 true 或 false。 有几种不同的方式来切换它(通过单击或拖动)。 您希望在 Toggle 内部状态发生变化时通知父组件,因此您公开一个 onChange 事件并从 Effect 中调用它:

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);
    }
  }

  // ...
}

上述方式过程:点击或者拖动结束,执行setIsOn,组件进行重新渲染,组件重新渲染完成之后执行useEffect,通知父组件更新状态,开始另一个渲染过程。

不应该使用Effect进行状态同步,应在事件处理中一次完成两个组件的状态更新。

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);
    }
  }

  // ...
}

更进一步,从父组件接收isOn

// ✅ 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);
    }
  }

  // ...
}

每当您尝试使两个不同的状态变量保持同步时,请尝试提升状态!

传递数据给父组件

子组件发送请求,然后将请求到的数据传递给父组件

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 中,数据从父组件流向它们的子组件。 当您在屏幕上看到错误时,您可以沿着组件链向上追踪信息的来源,直到您找到哪个组件传递了错误的 prop 或具有错误的状态。 当子组件在 Effects 中更新其父组件的状态时,数据流变得很难追踪。 由于子组件和父组件都需要相同的数据,因此让父组件获取该数据,然后将其传递给子组件:

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

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

订阅外部数据

有时候,组件可能需要订阅状态,这些状态不是来自React组件,他们可能来自第三方或者来自浏览器。它们会在React不知道的情况下更新,因此需要订阅他们

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();
  // ...
}

React专门构建的有hooks来订阅外部数据

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();
  // ...
}
  • 如果您可以在渲染期间计算某些东西,则不需要 Effect。
  • 要缓存昂贵的计算,请添加 useMemo 而不是 useEffect。
  • 要重置整个组件树的状态,请将不同的key传递给它。
  • 要重置特定位的状态以响应道具更改,请在渲染期间设置它。
  • 因为组件渲染而运行的代码应该在 Effects 中,其余的应该在事件中。
  • 如果您需要更新多个组件的状态,最好在单个事件期间执行。
  • 每当您尝试同步不同组件中的状态变量时,请考虑提升状态。
  • 您可以使用 Effects 获取数据,但您需要实施清理以避免竞争条件。

** 注:文章内容完全来自react官网