一份完整的useEffect指南(2)

本文太長鸳玩,謹(jǐn)慎閱讀。
本文原文:一份完整的useEffect指南
原文檔: A Complete Guide to useEffect

提示: 本文是接著上面的一篇闡述的米碰,別問我為啥分開寫虐译,見圖知意

image.png

開始正文

不要因?yàn)橐蕾嚩鲋e

在依賴關(guān)系上撒謊會(huì)帶來不好的后果。從直覺上看, 這是有道理的, 但我看到幾乎每個(gè)人都嘗試使用useEffect與類中的心理模型試圖欺騙規(guī)則厢拭。(一開始我也這么做了!)

function SearchResults() {
  async function fetchData() {
    // ...
  }

  useEffect(() => {
    fetchData();
  }, []); // Is this okay? Not always -- and there's a better way to write it.

  // ...
}

(Hook FAQ解釋了應(yīng)該怎么做供鸠。我們將在下面回到這個(gè)例子。)

“但我只想在mount時(shí)運(yùn)行它寨闹!”,你會(huì)這么說√現(xiàn)在塑娇,請記住:如果你指定deps写妥,組件內(nèi)部的效果所使用的所有 值都 必須 在那里。** 包括props扎筒,狀態(tài)嗜桌,函數(shù) - 組件中的任何內(nèi)容浮定。

有時(shí)桦卒,當(dāng)你這樣做時(shí),它會(huì)導(dǎo)致問題迎吵。例如,你可能會(huì)看到一個(gè)無限重取循環(huán)蔫巩,或者一個(gè)socket被重新創(chuàng)建得太頻繁圆仔。 該問題的解決方案不是刪除依賴項(xiàng)。 我們很快就會(huì)看到這些解決方案歪沃。

但在我們跳到解決方案之前,讓我們更好地理解問題液走。

當(dāng)依賴關(guān)系欺騙時(shí)會(huì)發(fā)生什么

如果deps(依賴項(xiàng))包含了效果使用的所有值腻窒,React知道什么時(shí)候重新運(yùn)行它:

useEffect(() => {
  document.title = 'Hello, ' + name;
}, [name]); // 關(guān)注這個(gè)依賴項(xiàng)
image

(依賴關(guān)系是不同的瓦哎,所以我們重新運(yùn)行效果割岛。)

但是,如果我們?yōu)榇诵Ч付?code>[]惠爽,則新效果函數(shù)將不會(huì)運(yùn)行:

useEffect(() => {
  document.title = 'Hello, ' + name;
}, []); // Wrong: name is missing in deps

image

(依賴項(xiàng)是相同的婚肆,所以我們跳過這個(gè)效果。)

在這種情況下赞咙,問題可能顯而易見攀操。但是,在其他情況下健芭,當(dāng)類解決方案從你的記憶中“跳出”時(shí),這種直覺可能會(huì)欺騙你痒留。

例如匾效,假設(shè)我們正在編寫一個(gè)每秒遞增一次的計(jì)數(shù)器。對于類魔策,我們的直覺是:“設(shè)置間隔(interval)一次闯袒,銷毀間隔一次”。這是一個(gè)我們?nèi)绾巫龅?a target="_blank" rel="nofollow">例子堕仔。當(dāng)我們在心理上將此代碼翻譯為useEffect時(shí),我們本能地將[]添加到deps中恼五≡致“我想讓它跑一次”睬罗,對嗎?

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []); //你的關(guān)注點(diǎn)在這里

  return <h1>{count}</h1>;
}

但是花盐,此示例僅遞增一次柒昏。哎呀职祷。

如果你的心理模型是“依賴關(guān)系讓我指定何時(shí)我想重新觸發(fā)效果”,這個(gè)例子可能會(huì)給你一個(gè)存在的危機(jī)。你想要觸發(fā)它一次昔字,因?yàn)樗且粋€(gè)間隔 - 所以它為什么會(huì)引起問題?

但是夹攒,如果你知道依賴項(xiàng)是我們的提示咏尝,用于對效果在渲染范圍中使用的所有內(nèi)容作出反應(yīng),那么這樣做是有意義的允懂。它使用count,但我們使用[]撒謊說它沒有依賴生百。它會(huì)坑道我們,這只是時(shí)間問題!

在第一次渲染中蜡坊,count0蠢甲。因此,第一個(gè)渲染效果中的setCount(count + 1)表示setCount(0 + 1)曼追。因?yàn)?code>[] deps我們永遠(yuǎn)不會(huì)重新運(yùn)行效果,所以它會(huì)每秒調(diào)用setCount(0 + 1)

// First render, state is 0
function Counter() {
  // ...
  useEffect(
    // Effect from first render
    () => {
      const id = setInterval(() => {
+       setCount(0 + 1); // Always setCount(1)
      }, 1000);
      return () => clearInterval(id);
    },
+   [] // Never re-runs
  );
  // ...
}

// Every next render, state is 1
function Counter() {
  // ...
  useEffect(
+   // This effect is always ignored because
+   // we lied to React about empty deps.
    () => {
      const id = setInterval(() => {
        setCount(1 + 1);
      }, 1000);
      return () => clearInterval(id);
    },
    []
  );
  // ...
}

我們?nèi)鲋e說我們的效果不依賴于組件內(nèi)部的值晶伦,而實(shí)際上它依賴于組件內(nèi)部的值!

我們的效果使用count - 組件內(nèi)部的值(但在效果之外):

+const count = // ...

useEffect(() => {
  const id = setInterval(() => {
+   setCount(count + 1);
  }, 1000);
  return () => clearInterval(id);
}, []);

因此,將[]指定為依賴項(xiàng)將產(chǎn)生一個(gè)錯(cuò)誤泌参。React將比較依賴項(xiàng)及舍,并跳過更新此效果:

image

(依賴性是相同的,所以我們跳過這個(gè)效果。)

像這樣的問題很難想到歼郭。因此病曾,我鼓勵(lì)你將其作為一項(xiàng)硬性規(guī)則鲫竞,始終誠實(shí)地遵循效果依賴關(guān)系,并將其全部(效果所用到的)指定僵井。(如果你想在團(tuán)隊(duì)中強(qiáng)制執(zhí)行此操作,我們會(huì)提供一個(gè)[lint規(guī)則](lint rule)渊季。)

誠實(shí)對待依賴關(guān)系的兩種方法

有兩種策略可以誠實(shí)地對待依賴關(guān)系荷并。通常應(yīng)該從第一個(gè)開始,然后根據(jù)需要應(yīng)用第二個(gè)谈息。

第一種策略是修復(fù)依賴關(guān)系數(shù)組,以包含在效果中使用的組件內(nèi)的所有 值犁珠。** 我們將count作為dep(依賴)包括:

useEffect(() => {
  const id = setInterval(() => {
    setCount(count + 1);
  }, 1000);
  return () => clearInterval(id);
}, [count]);

這使依賴項(xiàng)數(shù)組正確余素。它可能不太理想威根,但這是我們需要解決的第一個(gè)問題。現(xiàn)在改變count將重新運(yùn)行效果,每一個(gè)下一個(gè)間隔引用count從其渲染在setCount(count + 1):

// First render, state is 0
function Counter() {
  // ...
  useEffect(
    // Effect from first render
    () => {
      const id = setInterval(() => {
+       setCount(0 + 1); // setCount(count + 1)
      }, 1000);
      return () => clearInterval(id);
    },
+   [0] // [count]
  );
  // ...
}

// Second render, state is 1
function Counter() {
  // ...
  useEffect(
    // Effect from second render
    () => {
      const id = setInterval(() => {
+       setCount(1 + 1); // setCount(count + 1)
      }, 1000);
      return () => clearInterval(id);
    },
+   [1] // [count]
  );
  // ...
}

這樣可以解決問題,但只要count發(fā)生變化棺榔,我們的間隔就會(huì)被清除并再次設(shè)置。這可能是不受歡迎的:

image

(依賴關(guān)系是不同的,所以我們重新運(yùn)行效果设塔。)


第二種策略是更改我們的effect代碼,這樣它就不需要比我們想要的值頻繁的更改图柏。 我們不想在依賴關(guān)系上撒謊——我們只是想改變我們的效果序六,使依賴關(guān)系更少

讓我們看一些刪除依賴項(xiàng)的常用技巧蚤吹。

使效果自給自足

我們希望擺脫效果中的count依賴性例诀。

useEffect(() => {
  const id = setInterval(() => {
    setCount(count + 1);
  }, 1000);
  return () => clearInterval(id);
}, [count]);

要做到這一點(diǎn),我們需要問自己:我們使用count用來做什么距辆? 好像我們只將它用于setCount調(diào)用爆土。在這種情況下盅抚,我們實(shí)際上在作用域里根本不需要count。當(dāng)我們想要根據(jù)以前的狀態(tài)更新狀態(tài)時(shí),我們可以使用setState函數(shù)更新形式

useEffect(() => {
  const id = setInterval(() => {
    // 你的關(guān)注點(diǎn)在這里
    setCount(c => c + 1);
  }, 1000);
  return () => clearInterval(id);
}, []);

我喜歡將這些情況視為“錯(cuò)誤(假的)依賴”矩动。是的享言,count是一個(gè)必要的依賴項(xiàng),因?yàn)槲覀冊谛Ч芯帉懥?code>setCount(count + 1)侦讨。但是需忿,我們真正的需要count只是去將其轉(zhuǎn)換為count + 1并將其“發(fā)回”給React澈魄。但是React已經(jīng)知道了 當(dāng)前的count掰读。我們需要告訴React的是增加這個(gè)狀態(tài)——不管它現(xiàn)在是什么狀態(tài)赫模。

這正是setCount(c => c + 1)的作用摧玫。你可以將其看作是向React“發(fā)送一條指令”來對狀態(tài)應(yīng)該如何變化做出反應(yīng)。這種“更新形式”在其他情況下也有幫助,例如批量多次更新時(shí)芥牌。

請注意,我們實(shí)際上已經(jīng)完成了刪除依賴項(xiàng)的工作算灸。我們沒有欺騙他榨婆。我們的效果不再從渲染作用域讀取counter值:**

image

(依賴項(xiàng)是相同的,所以我們跳過這個(gè)效果挺物。)

你可以在這里試試。

即使此效果僅運(yùn)行一次骂澄,屬于第一個(gè)渲染的間隔回調(diào)完全能夠在每次間隔觸發(fā)時(shí)發(fā)送c => c + 1更新指令账千。它不再需要知道當(dāng)前的計(jì)數(shù)器狀態(tài)咧最。 React已經(jīng)知道了。

功能更新和Google文檔

還記得我們?nèi)绾握務(wù)撏绞切Ч男睦砟P蛦幔客降囊粋€(gè)有趣方面是,你經(jīng)常希望保持系統(tǒng)之間的“消息”與其狀態(tài)無關(guān)瞒窒。例如,在Google Docs中編輯文檔實(shí)際上并不會(huì)將整個(gè)頁面發(fā)送到服務(wù)器政勃。那將是非常低效的。相反持际,它會(huì)發(fā)送用戶嘗試執(zhí)行操作的表示。

雖然我們的用例是不同的闷祥,但是類似的哲學(xué)適用于效果。它只有助于將最少的必要信息從效果內(nèi)部發(fā)送到組件潮饱。setCount(c => c + 1)這樣的更新形式傳遞的信息嚴(yán)格少于setCount(count + 1),因?yàn)樗鼪]有被當(dāng)前計(jì)數(shù)(count)“污染”您朽。它只表示動(dòng)作(“遞增”)哗总。在React中思考涉及到找到最小的狀態(tài)几颜。這是相同的原則,但對于更新讯屈。

編碼意圖(而不是結(jié)果)類似于谷歌文檔解決協(xié)作編輯的方式蛋哭。雖然這是延伸的類比,但功能更新在React中也扮演著類似的角色涮母。它們確保來自多個(gè)源的更新(事件處理程序谆趾、效果訂閱等)能夠以可預(yù)測的方式正確地應(yīng)用于批處理中。

但是叛本,即使是setCount(c => c + 1)也不是那么好沪蓬。 它看起來有點(diǎn)奇怪,它能做的非常有限来候。例如跷叉,如果我們有兩個(gè)狀態(tài)變量,它們的值相互依賴峭拘,或者如果我們需要根據(jù)prop來計(jì)算下一個(gè)狀態(tài),這對我們沒有幫助。幸運(yùn)的是,setCount(c => c + 1)有一個(gè)更強(qiáng)大的姐妹模式。它的名稱是useReducer介蛉。

將更新與操作解耦

讓我們修改前面的例子佳恬,讓它有兩個(gè)狀態(tài)變量:countstep倾剿。我們的間隔將使count增加step輸入的值:

function Counter() {
  const [count, setCount] = useState(0);
  const [step, setStep] = useState(1);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + step);
    }, 1000);
    return () => clearInterval(id);
  }, [step]);

  return (
    <>
      <h1>{count}</h1>
      <input value={step} onChange={e => setStep(Number(e.target.value))} />
    </>
  );
}

(這里是一個(gè)例子)

請注意,我們沒有欺騙坯癣。 由于我開始在效果中使用step蚜点,我將它添加到依賴項(xiàng)中。這就是代碼正確運(yùn)行的原因藻丢。

此示例中的當(dāng)前行為是更改step重新啟動(dòng)間隔 - 因?yàn)樗瞧渲幸粋€(gè)依賴項(xiàng)斋否。在許多情況下旦委,這正是你想要的罢低!拆除一個(gè)效果并重新設(shè)置它沒有錯(cuò),除非我們有很好的理由遣铝,否則我們不應(yīng)該避免這樣做瘫絮。

但是,假設(shè)我們希望間隔時(shí)鐘不會(huì)在step更改時(shí)重置填硕。我們?nèi)绾螐男Ч袆h除step依賴麦萤?

設(shè)置狀態(tài)變量時(shí),取決于另一個(gè)狀態(tài)變量的當(dāng)前值扁眯,你可能希望嘗試使用useReducer替換它們壮莹。

當(dāng)你發(fā)現(xiàn)自己正在編寫setSomething(something =>…)時(shí),是時(shí)候考慮使用reducer了姻檀。reducer允許你 將表示組件中發(fā)生的“操作”與狀態(tài)更新的響應(yīng)方式解耦命满。

在我們的效果中,讓我們將step依賴關(guān)系替換為dispatch依賴關(guān)系:

const [state, dispatch] = useReducer(reducer, initialState);
const { count, step } = state;

useEffect(() => {
  const id = setInterval(() => {
    dispatch({ type: 'tick' }); // Instead of setCount(c => c + step);
  }, 1000);
  return () => clearInterval(id);
}, [dispatch]);

(查看這里例子)

你可能會(huì)問我:“這怎么會(huì)更好绣版?”答案是 React保證dispatch函數(shù)在整個(gè)組件生命周期內(nèi)保持不變胶台。所以上面的例子不需要重新訂閱間隔。

我們解決了問題杂抽!

(你可以省略deps中的dispatch诈唬,setStateuseRef容器值,因?yàn)镽eact保證它們是靜態(tài)的缩麸。但指定它們也沒有壞處铸磅。)

它不是在效果中讀取狀態(tài),而是發(fā)送一個(gè)動(dòng)作來編碼 發(fā)生了什么 的信息杭朱。這允許我們的效果保持與step狀態(tài)解耦阅仔。我們的效果并不關(guān)心我們?nèi)绾胃聽顟B(tài),它只是告訴我們發(fā)生了什么弧械。reducer集中了更新邏輯:

const initialState = {
  count: 0,
  step: 1,
};

function reducer(state, action) {
  const { count, step } = state;
  if (action.type === 'tick') {
    return { count: count + step, step };
  } else if (action.type === 'step') {
    return { count, step: action.step };
  } else {
    throw new Error();
  }
}

(如果你之前錯(cuò)過了霎槐,那么這是一個(gè)演示)

為什么useReducer是Hooks的欺騙模式

我們已經(jīng)看到當(dāng)效果需要根據(jù)以前的狀態(tài)或另一個(gè)狀態(tài)變量設(shè)置狀態(tài)時(shí)如何刪除依賴關(guān)系。但是如果我們需要props來計(jì)算下一個(gè)狀態(tài)呢梦谜? 例如丘跌,我們的API可能是<Counter step = {1} />。當(dāng)然唁桩,在這種情況下闭树,我們不能避免將props.step指定為依賴項(xiàng)?

事實(shí)上荒澡,我們可以报辱!我們可以將reducer本身 放在我們的組件中來讀取props:

function Counter({ step }) {
  const [count, dispatch] = useReducer(reducer, 0);

  function reducer(state, action) {
    if (action.type === 'tick') {
      // 你的關(guān)注點(diǎn)應(yīng)該在這里
      return state + step;
    } else {
      throw new Error();
    }
  }

  useEffect(() => {
    const id = setInterval(() => {
      dispatch({ type: 'tick' });
    }, 1000);
    return () => clearInterval(id);
  }, [dispatch]);

  return <h1>{count}</h1>;
}

這個(gè)模式禁用了一些優(yōu)化,所以盡量不要在任何地方都使用它单山,但是如果需要碍现,你可以完全從reducer訪問props幅疼。(這里是一個(gè)例子)

即使在這種情況下,重新渲染之間仍然保證dispatch標(biāo)識(shí)是穩(wěn)定的昼接。 因此爽篷,如果需要,你可以從效果deps中省略它慢睡。它不會(huì)導(dǎo)致重新運(yùn)行效果逐工。

你可能想知道:這怎么可能有效呢?當(dāng)從屬于另一個(gè)渲染效果的內(nèi)部調(diào)用時(shí)漂辐,reducer如何“知道”props?答案是泪喊,當(dāng)你dispatch時(shí),React會(huì)記住該操作 - 但它會(huì)在下一次渲染期間調(diào)用你的reducer髓涯。在那時(shí)袒啼,新的props將在作用域內(nèi),你不會(huì)在一個(gè)效果內(nèi)纬纪。

這就是為什么我喜歡將useReducer視為Hooks的“作弊(欺騙)模式”蚓再。它讓我將更新邏輯從描述所發(fā)生的事情中解耦出來。反過來育八,這有助于我從我的效果中刪除不必要的依賴項(xiàng),并避免更頻繁地重新運(yùn)行它們赦邻。

移動(dòng)函數(shù)到效果內(nèi)

一個(gè)常見的錯(cuò)誤是認(rèn)為函數(shù)不應(yīng)該是依賴關(guān)系髓棋。例如,這似乎可以工作:

function SearchResults() {
  const [data, setData] = useState({ hits: [] });

  async function fetchData() {
    const result = await axios(
      'https://hn.algolia.com/api/v1/search?query=react',
    );
    setData(result.data);
  }

  useEffect(() => {
    fetchData();
  }, []); // Is this okay?
  // ...

(這個(gè)例子改編自Robin Wieruch的一篇精彩文章 - 看看吧惶洲!)

需要明確的是按声,此代碼確實(shí)有效。但是簡單地忽略局部函數(shù)的問題是恬吕,當(dāng)組件增長時(shí)签则,很難判斷我們是否在處理所有的情況!

想象一下,我們的代碼是這樣拆分的铐料,每個(gè)函數(shù)都要大(很長渐裂,不易維護(hù))五倍:

function SearchResults() {
  // Imagine this function is long
  function getFetchUrl() {
    return 'https://hn.algolia.com/api/v1/search?query=react';
  }

  // Imagine this function is also long
  async function fetchData() {
    const result = await axios(getFetchUrl());
    setData(result.data);
  }

  useEffect(() => {
    fetchData();
  }, []);

  // ...
}

現(xiàn)在讓我們說我們稍后在其中一個(gè)函數(shù)中使用一些狀態(tài)或prop:

function SearchResults() {
  const [query, setQuery] = useState('react');

  // Imagine this function is also long
  function getFetchUrl() {
+   return 'https://hn.algolia.com/api/v1/search?query=' + query;
  }

  // Imagine this function is also long
  async function fetchData() {
    const result = await axios(getFetchUrl());
    setData(result.data);
  }

  useEffect(() => {
    fetchData();
  }, []);

  // ...
}

如果我們忘記更新調(diào)用這些函數(shù)的任何效果的deps(可能通過其他函數(shù)!)钠惩,我們的效果將無法同步來自props和state的更改柒凉。這聽起來不太好。

幸運(yùn)的是篓跛,這個(gè)問題有一個(gè)簡單的解決方案膝捞。如果你只在效果中使用某些函數(shù),請將它們直接移動(dòng)到該效果中:

function SearchResults() {
  // ...
  useEffect(() => {
    // 你的關(guān)注點(diǎn)在這里
    // We moved these functions inside!
    function getFetchUrl() {
      return 'https://hn.algolia.com/api/v1/search?query=react';
    }
    async function fetchData() {
      const result = await axios(getFetchUrl());
      setData(result.data);
    }

    fetchData();
  }, []); // ? Deps are OK
  // ...
}

(這里是一個(gè)示例)

那么有什么好處呢愧沟?我們不再需要考慮“傳遞依賴”蔬咬。我們的依賴數(shù)組不再欺騙你: 我們真的沒有在我們的效果中使用來自組件外部作用域的任何東西鲤遥。

如果我們稍后修改getFetchUrl以使用query狀態(tài),我們更有可能注意到我們在效果編輯它 - 因此林艘,我們需要向效果依賴項(xiàng)添加query

function SearchResults() {
  const [query, setQuery] = useState('react');

  useEffect(() => {
    function getFetchUrl() {
      return 'https://hn.algolia.com/api/v1/search?query=' + query;
    }

    async function fetchData() {
      const result = await axios(getFetchUrl());
      setData(result.data);
    }

    fetchData();
  }, [query]); // ? Deps are OK

  // ...
}

(這里是一個(gè)例子)

通過添加此依賴項(xiàng)盖奈,我們不僅僅是“安撫React”。當(dāng)query發(fā)生更改時(shí)北启,重新獲取數(shù)據(jù)是有意義的卜朗。useEffect的設(shè)計(jì)迫使你注意到我們的數(shù)據(jù)流中的變化,并選擇我們的效果應(yīng)該如何同步它——而不是忽略它咕村,直到我們的產(chǎn)品用戶遇到bug场钉。

感謝eslint-plugin-react-hooks插件中的exhaustive-deps lint規(guī)則,你可以在編輯器中輸入時(shí)分析效果懈涛,并獲得有關(guān)缺少哪些依賴項(xiàng)的建議逛万。換句話說,計(jì)算機(jī)可以告訴你組件未正確處理哪些數(shù)據(jù)流更改批钠。

image

很甜蜜宇植。

但我不能把這個(gè)函數(shù)放在一個(gè)效果中

有時(shí)你可能不想把函數(shù)移到效果。例如埋心,同一組件中的多個(gè)效果可能會(huì)調(diào)用相同的函數(shù)指郁,并且你不希望復(fù)制和粘貼其邏輯】酱簦或許這是一個(gè)prop闲坎。

你應(yīng)該在效果依賴中跳過這樣的函數(shù)嗎?我想不是茬斧。同樣腰懂,效果不應(yīng)該在依賴關(guān)系上撒謊。 通常有更好的解決方案项秉。一個(gè)常見的誤解是“一個(gè)函數(shù)永遠(yuǎn)不會(huì)改變”绣溜。但正如我們在整篇文章中所學(xué)到的,這不可能是事實(shí)娄蔼。實(shí)際上怖喻,組件內(nèi)定義的函數(shù)會(huì)在每個(gè)渲染中發(fā)生變化!

這本身就是一個(gè)問題岁诉。 假設(shè)兩個(gè)效果調(diào)用getFetchUrl

function SearchResults() {
  function getFetchUrl(query) {
    return 'https://hn.algolia.com/api/v1/search?query=' + query;
  }

  useEffect(() => {
    const url = getFetchUrl('react');
    // ... Fetch data and do something ...
  }, []); // ?? Missing dep: getFetchUrl

  useEffect(() => {
    const url = getFetchUrl('redux');
    // ... Fetch data and do something ...
  }, []); // ?? Missing dep: getFetchUrl

  // ...
}

在這種情況下罢防,你可能不希望在任何一個(gè)效果中移動(dòng)getFetchUrl,因?yàn)檫@樣你就無法共享邏輯唉侄。

另一方面咒吐,如果你對效果依賴性“誠實(shí)”,則可能會(huì)遇到問題。由于我們的效果都依賴于getFetchUrl每個(gè)渲染都不同 )恬叹,我們的依賴數(shù)組是無用的:

function SearchResults() {
  // ?? Re-triggers all effects on every render
  // 你的關(guān)注點(diǎn)在這里
  function getFetchUrl(query) {
    return 'https://hn.algolia.com/api/v1/search?query=' + query;
  }

  useEffect(() => {
    const url = getFetchUrl('react');
    // ... Fetch data and do something ...
  }, [getFetchUrl]); // ?? Deps are correct but they change too often

  useEffect(() => {
    const url = getFetchUrl('redux');
    // ... Fetch data and do something ...
  }, [getFetchUrl]); // ?? Deps are correct but they change too often

  // ...
}

一個(gè)誘人的解決方案是直接跳過deps列表中的getFetchUrl函數(shù)候生。但是,我不認(rèn)為這是一個(gè)很好的解決方案绽昼。這使我們很難注意到何時(shí)向數(shù)據(jù)流添加需要由效果處理的更改唯鸭。這會(huì)導(dǎo)致我們之前看到的“永不更新間隔(interval)”等錯(cuò)誤。

相反硅确,還有另外兩種更簡單的解決方案目溉。

首先,如果函數(shù)不使用組件范圍中的任何內(nèi)容菱农,你可以將其提升到組件外部缭付,然后在效果中自由使用它:

// ? Not affected by the data flow
function getFetchUrl(query) {
  return 'https://hn.algolia.com/api/v1/search?query=' + query;
}

function SearchResults() {
  useEffect(() => {
    const url = getFetchUrl('react');
    // ... Fetch data and do something ...
  }, []); // ? Deps are OK

  useEffect(() => {
    const url = getFetchUrl('redux');
    // ... Fetch data and do something ...
  }, []); // ? Deps are OK

  // ...
}

無需在deps中指定它,因?yàn)樗辉阡秩痉秶鷥?nèi)循未,并且不受數(shù)據(jù)流的影響陷猫。它不能偶然地依賴于props或狀態(tài)。

或者的妖,你可以將其包裝到useCallback Hook中:

function SearchResults() {
  // ? Preserves identity when its own deps are the same
  // 你的關(guān)注點(diǎn)在這里
  const getFetchUrl = useCallback((query) => {
    return 'https://hn.algolia.com/api/v1/search?query=' + query;
  }, []);  // ? Callback deps are OK

  useEffect(() => {
    const url = getFetchUrl('react');
    // ... Fetch data and do something ...
  }, [getFetchUrl]); // ? Effect deps are OK

  useEffect(() => {
    const url = getFetchUrl('redux');
    // ... Fetch data and do something ...
  }, [getFetchUrl]); // ? Effect deps are OK

  // ...
}

useCallback實(shí)際上就像添加另一層依賴性檢查绣檬。它正在另一端解決問題 — 我們只在必要時(shí)更改函數(shù)本身,而不是避免函數(shù)依賴嫂粟。

讓我們看看為什么這種方法很有用娇未。以前,我們的示例顯示了兩個(gè)搜索結(jié)果(針對'react''redux'搜索查詢)星虹。但是零抬,假設(shè)我們要添加一個(gè)輸入,以便可以搜索任意query搁凸。因此媚值,getFetchUrl現(xiàn)在不會(huì)將query作為參數(shù)狠毯,而是從本地狀態(tài)讀取它护糖。

我們會(huì)立即看到它缺少query依賴項(xiàng):

function SearchResults() {
  const [query, setQuery] = useState('react');
  const getFetchUrl = useCallback(() => { // No query argument
    return 'https://hn.algolia.com/api/v1/search?query=' + query;
  }, []); // ?? Missing dep: query
  // ...
}

如果我修復(fù)了useCallback deps以包含query,那么每當(dāng)query更改時(shí)嚼松,deps中的getFetchUrl的任何效果都會(huì)重新運(yùn)行:

function SearchResults() {
  const [query, setQuery] = useState('react');

  // ? Preserves identity until query changes
  const getFetchUrl = useCallback(() => {
    return 'https://hn.algolia.com/api/v1/search?query=' + query;
  }, [query]);  // ? Callback deps are OK

  useEffect(() => {
    const url = getFetchUrl();
    // ... Fetch data and do something ...
  }, [getFetchUrl]); // ? Effect deps are OK

  // ...
}

感謝useCallback嫡良,如果query相同,則getFetchUrl也保持不變献酗,并且我們的效果不會(huì)重新運(yùn)行寝受。但是如果query更改,getFetchUrl也會(huì)更改罕偎,我們將重新獲取數(shù)據(jù)很澄。這很像在Excel電子表格中更改某些單元格時(shí),使用它的其他單元格會(huì)自動(dòng)重新計(jì)算。

這只是擁抱數(shù)據(jù)流和同步思維的結(jié)果甩苛。相同的解決方案適用于從父級傳遞的函數(shù)props:

function Parent() {
  const [query, setQuery] = useState('react');

  // 你的關(guān)注點(diǎn)在這里
  // ? Preserves identity until query changes
  const fetchData = useCallback(() => {
    const url = 'https://hn.algolia.com/api/v1/search?query=' + query;
    // ... Fetch data and return it ...
  }, [query]);  // ? Callback deps are OK

  return <Child fetchData={fetchData} />
}

function Child({ fetchData }) {
  let [data, setData] = useState(null);

  useEffect(() => {
    fetchData().then(setData);
  }, [fetchData]); // ? Effect deps are OK

  // ...
}

由于fetchData只在query狀態(tài)發(fā)生變化時(shí)才會(huì)在Parent內(nèi)部發(fā)生變化蹂楣,所以我們的Child在應(yīng)用程序真正需要時(shí)才會(huì)重新獲取數(shù)據(jù)。

函數(shù)是數(shù)據(jù)流的一部分嗎讯蒲?

有趣的是痊土,用類打破了這種模式,真正顯示了效果和生命周期范例之間的差異墨林×拊停考慮一下這個(gè)轉(zhuǎn)換:

class Parent extends Component {
  state = {
    query: 'react'
  };
  // 你的關(guān)注點(diǎn)在這里(手動(dòng)高亮)
  fetchData = () => {
    const url = 'https://hn.algolia.com/api/v1/search?query=' + this.state.query;
    // ... Fetch data and do something ...
  };
  render() {
    return <Child fetchData={this.fetchData} />;
  }
}

class Child extends Component {
  state = {
    data: null
  };
  componentDidMount() {
    this.props.fetchData();
  }
  render() {
    // ...
  }
}

你可能會(huì)想:“得了吧,Dan旭等,我們都知道useEffect就像componentDidMountcomponentDidUpdate結(jié)合在一起酌呆,你無法繼續(xù)再搖旗吶喊了!” 然而辆雾,即使使用componentDidUpdate肪笋,這也不起作用:

class Child extends Component {
  state = {
    data: null
  };
  componentDidMount() {
    this.props.fetchData();
  }
  componentDidUpdate(prevProps) {
    // ?? This condition will never be true
    if (this.props.fetchData !== prevProps.fetchData) {
      this.props.fetchData();
    }
  }
  render() {
    // ...
  }
}

當(dāng)然,fetchData是一個(gè)類的方法度迂!(或者藤乙,更確切地說,一個(gè)類屬性——但這不會(huì)改變?nèi)魏螙|西惭墓。)由于狀態(tài)的變化坛梁,它不會(huì)有所不同。所以this.props.fetchData將保持等于prevProps.fetchData腊凶,我們將永遠(yuǎn)不會(huì)重新獲取划咐。讓我們刪除這個(gè)條件呢?

componentDidUpdate(prevProps) {
  this.props.fetchData();
}

哦等等钧萍,這會(huì)在每次重新渲染時(shí)獲取褐缠。(在上面的樹中添加一個(gè)動(dòng)畫是發(fā)現(xiàn)它的一種有趣的方式。) 也許讓我們將它綁定到特定的查詢风瘦?

render() {
  return <Child fetchData={this.fetchData.bind(this, this.state.query)} />;
}

但是队魏,即使query沒有改變,this.props.fetchData万搔!== prevProps.fetchData始終true胡桨!所以我們總是會(huì)重新獲取。

對于類的這個(gè)難題瞬雹,惟一真正的解決方案是咬緊牙關(guān)昧谊,將query本身傳遞給Child組件。Child實(shí)際上并沒有最終使用query酗捌,但它可以在更改時(shí)觸發(fā)重新獲饶匚堋:

class Parent extends Component {
  state = {
    query: 'react'
  };
  fetchData = () => {
    const url = 'https://hn.algolia.com/api/v1/search?query=' + this.state.query;
    // ... Fetch data and do something ...
  };
  render() {
    return <Child fetchData={this.fetchData} query={this.state.query} />;
  }
}

class Child extends Component {
  state = {
    data: null
  };
  componentDidMount() {
    this.props.fetchData();
  }
  componentDidUpdate(prevProps) {
    if (this.props.query !== prevProps.query) {
      this.props.fetchData();
    }
  }
  render() {
    // ...
  }
}

在多年使用React類的過程中涌哲,我已經(jīng)習(xí)慣了傳遞不必要的props并破壞父組件的封裝,直到一周前我才意識(shí)到為什么必須這樣做尚镰。

對于類膛虫,函數(shù)props本身并不是數(shù)據(jù)流的真正組成部分睁搭。 方法過于緊密于可變的this變量饶碘,這樣我們就不能依賴它們的標(biāo)識(shí)來表示任何內(nèi)容。 因此比伏,即使我們只想要一個(gè)函數(shù)敞曹,我們也必須傳遞一堆其他數(shù)據(jù)账月,以便能夠“區(qū)分”它。我們無法知道從父級傳遞的this.props.fetchData是否依賴于某種狀態(tài)澳迫,以及該狀態(tài)是否剛剛更改局齿。

使用useCallback,函數(shù)可以完全參與數(shù)據(jù)流橄登。 我們可以說抓歼,如果函數(shù)輸入發(fā)生了變化,函數(shù)本身就會(huì)發(fā)生變化拢锹,但如果沒有變化谣妻,它就會(huì)保持不變。由于useCallback提供的粒度卒稳,對props.fetchData等props的更改可以自動(dòng)向下傳播蹋半。

同樣,useMemo讓我們對復(fù)雜對象也這樣做:

function ColorPicker() {
  // Doesn't break Child's shallow equality prop check
  // unless the color actually changes.
  const [color, setColor] = useState('pink');
  const style = useMemo(() => ({ color }), [color]);
  return <Child style={style} />;
}

我想強(qiáng)調(diào)的是充坑,在任何地方使用useCallback都非常笨重减江。 當(dāng)一個(gè)函數(shù)傳遞下去并從一些子組件的效果內(nèi)部調(diào)用時(shí)它很有用∧硪或者辈灼,如果你試圖阻止破壞子組件的記憶。但Hooks更適合于避免回調(diào)完全傳遞下來也榄。

在上面的例子中巡莹,我更希望fetchData位于我的effect(它本身可以被提取到一個(gè)自定義的鉤子中)或頂層導(dǎo)入中。我希望保持effect簡單手蝎,并且其中的回調(diào)對此沒有幫助榕莺。(“如果在請求運(yùn)行期間某些props.onComplete回調(diào)發(fā)生了更改俐芯,該怎么辦棵介?”) 你可以模擬類行為,但這并不能解決競爭條件吧史。

說說競爭條件

使用類的經(jīng)典數(shù)據(jù)獲取示例可能如下所示:

class Article extends Component {
  state = {
    article: null
  };
  componentDidMount() {
    this.fetchData(this.props.id);
  }
  async fetchData(id) {
    const article = await API.fetchArticle(id);
    this.setState({ article });
  }
  // ...
}

你可能知道邮辽,這段代碼有些問題。它不處理更新。所以你可以在網(wǎng)上找到的第二個(gè)經(jīng)典例子是這樣的:

class Article extends Component {
  state = {
    article: null
  };
  componentDidMount() {
    this.fetchData(this.props.id);
  }
  componentDidUpdate(prevProps) {
    if (prevProps.id !== this.props.id) {
      this.fetchData(this.props.id);
    }
  }
  async fetchData(id) {
    const article = await API.fetchArticle(id);
    this.setState({ article });
  }
  // ...
}

這肯定更好吨述!但他仍然有些問題岩睁。它有問題的原因是請求順序可能出現(xiàn)問題。因此揣云,如果我正在獲取{id:10}的數(shù)據(jù)捕儒,切換到{id:20},但{id:20}請求首先出現(xiàn)邓夕,先前開始但稍后完成的請求將錯(cuò)誤地覆蓋我的狀態(tài)刘莹。

這被稱為競爭條件,它在代碼中是典型的焚刚,將async/await(假定有東西在等待結(jié)果)與自頂向下的數(shù)據(jù)流(當(dāng)我們處于異步函數(shù)的中間時(shí)点弯,屬性或狀態(tài)可能會(huì)改變)混合在一起。

效果并不能神奇地解決這個(gè)問題矿咕,但是如果你試圖將async函數(shù)直接傳遞給效果抢肛,它們會(huì)警告你。(我們需要改進(jìn)這個(gè)警告碳柱,以便更好地解釋你可能遇到的問題捡絮。)

如果你使用的異步方法支持取消,那就太棒了! 你可以在清理函數(shù)中取消異步請求莲镣。

或者锦援,最簡單的權(quán)宜之計(jì)方法是使用布爾值跟蹤它:

function Article({ id }) {
  const [article, setArticle] = useState(null);

  useEffect(() => {
+   let didCancel = false;
    async function fetchData() {
      const article = await API.fetchArticle(id);
+     if (!didCancel) {        
        setArticle(article);
      }
    }

    fetchData();

    // 這里也是你的關(guān)注點(diǎn)
    return () => {      
        didCancel = true;    
    };  
  }, [id]);

  // ...
}

這篇文章詳細(xì)介紹了如何處理錯(cuò)誤和加載狀態(tài),以及將該邏輯提取到自定義Hook中剥悟。如果你有興趣了解有關(guān)使用Hooks獲取數(shù)據(jù)的更多信息灵寺,我建議你查看一下。

提高標(biāo)準(zhǔn)

使用類生命周期思維模式区岗,副作用與渲染輸出的行為不同略板。渲染UI由props和state驅(qū)動(dòng),并保證與它們一致慈缔,但副作用不是叮称。這是bug的常見來源。

使用useEffect的思維模式藐鹤,默認(rèn)情況下會(huì)同步事物瓤檐。副作用成為React數(shù)據(jù)流的一部分。對于每個(gè)useEffect調(diào)用娱节,一旦你做對了挠蛉,你的組件就會(huì)更好地處理邊緣情況。

然而肄满,做好這件事的前期成本更高谴古。這可能很煩人质涛。編寫能夠很好地處理邊緣情況的同步代碼,本質(zhì)上比引發(fā)與渲染不一致的一次性副作用更困難掰担。

如果useEffect是你大多數(shù)時(shí)間使用的工具汇陆,這可能會(huì)令人擔(dān)憂。然而带饱,它是一個(gè)低層的構(gòu)建塊≌贝現(xiàn)在是使用鉤子的早期階段,所以每個(gè)人都一直在使用低級別的鉤子勺疼,尤其是在教程中月趟。但是在實(shí)踐中,隨著好的API獲得發(fā)展勢頭恢口,社區(qū)很可能會(huì)開始轉(zhuǎn)向更高級別的鉤子孝宗。

我看到不同的應(yīng)用程序創(chuàng)建了它們自己的鉤子,比如封裝了它們的一些應(yīng)用程序的auth邏輯的useFetch或使用主題上下文的useTheme耕肩。一旦你有了這些工具箱因妇,你就不會(huì)經(jīng)常使用useEffect。但它帶來的彈性使每個(gè)Hook都能在其上構(gòu)建猿诸。

到目前為止婚被,useEffect最常用于數(shù)據(jù)獲取。但是數(shù)據(jù)提取并不是一個(gè)同步問題梳虽。這一點(diǎn)尤其明顯址芯,因?yàn)槲覀兊膁eps經(jīng)常是[]。我們在同步什么?

從長遠(yuǎn)來看窜觉,數(shù)據(jù)獲取的suspense將允許第三方庫以一種一流的方式告訴React暫停渲染谷炸,直到異步(任何東西:代碼、數(shù)據(jù)禀挫、圖像)就緒旬陡。

隨著suspense逐漸覆蓋更多的數(shù)據(jù)獲取用例,我預(yù)計(jì)當(dāng)你真正想要同步props和狀態(tài)到某個(gè)副作用時(shí)语婴,useEffect將作為一個(gè)高級用戶工具淡出背景描孟。與數(shù)據(jù)獲取不同,它可以很自然地處理這種情況砰左,因?yàn)樗菫檫@種情況而設(shè)計(jì)的匿醒。但在此之前,這里所示的自定義鉤子是重用數(shù)據(jù)獲取邏輯的好方法缠导。

結(jié)束

現(xiàn)在你已經(jīng)了解了我使用效果的所有知識(shí)廉羔,請?jiān)陂_始時(shí)查看TLDR。有意義嗎?我錯(cuò)過什么了嗎?(我的紙還沒有用完呢!)

我很想在Twitter上收到你的來信酬核!謝謝閱讀蜜另。

筆者個(gè)人總結(jié)

其實(shí)這個(gè)就像前面所說的類和函數(shù)的區(qū)別了。因?yàn)樵赗eact中嫡意,props是不會(huì)改變的举瑰,而this(state)是可變的。所以造成了改變蔬螟。由于props是不可變的此迅,這樣針對每次的渲染,都會(huì)有一個(gè)特定的渲染(每次的渲染)旧巾。

對于effect內(nèi)使用的函數(shù)耸序,最好是放在effect中,這樣可以避免以后組件增加而未處理所有的情況鲁猩。如果有復(fù)用坎怪,可以把這些函數(shù)提到effect外面,使用useCallback包裹起來廓握。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末搅窿,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子隙券,更是在濱河造成了極大的恐慌男应,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件娱仔,死亡現(xiàn)場離奇詭異沐飘,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)牲迫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進(jìn)店門耐朴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人盹憎,你說我怎么就攤上這事隔箍。” “怎么了脚乡?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵蜒滩,是天一觀的道長。 經(jīng)常有香客問我奶稠,道長俯艰,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任锌订,我火速辦了婚禮竹握,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘辆飘。我一直安慰自己啦辐,他們只是感情好谓传,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著芹关,像睡著了一般续挟。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上侥衬,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天诗祸,我揣著相機(jī)與錄音,去河邊找鬼轴总。 笑死直颅,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的怀樟。 我是一名探鬼主播功偿,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼往堡!你這毒婦竟也來了脖含?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤投蝉,失蹤者是張志新(化名)和其女友劉穎养葵,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體瘩缆,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡关拒,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了庸娱。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片着绊。...
    茶點(diǎn)故事閱讀 39,690評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖熟尉,靈堂內(nèi)的尸體忽然破棺而出归露,到底是詐尸還是另有隱情,我是刑警寧澤斤儿,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布剧包,位于F島的核電站,受9級特大地震影響往果,放射性物質(zhì)發(fā)生泄漏疆液。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一陕贮、第九天 我趴在偏房一處隱蔽的房頂上張望堕油。 院中可真熱鬧,春花似錦、人聲如沸掉缺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽眶明。三九已至艰毒,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間赘来,已是汗流浹背现喳。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工凯傲, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留犬辰,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓冰单,卻偏偏與公主長得像幌缝,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子诫欠,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容