<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          React Hook 避坑指南(useState & useEffect)

          共 15028字,需瀏覽 31分鐘

           ·

          2023-10-31 07:49

          useState


          const [state, setState] = useState(initialState);
          • 返回一個 state,以及更新 state 的函數。

          • 在初始渲染期間,返回的狀態(tài) (state) 與傳入的第一個參數 (initialState) 值相同。

          • setState 函數用于更新 state。它接收一個新的 state 值并將組件的一次重新渲染加入隊列。

          state的更新

          通過 setState 方法可以更新state。例如:查看在線示例

          const [count, setCount] = useState(0);
          function handleOnClick() { setCount(count + 1); setCount(count + 1); setCount(count + 1);}
          return ( <div> <div> count: {count} </div> <button onClick={handleOnClick}> +1 </button> </div>);

          如果點擊按鈕后連續(xù)調用3次 setCount(count + 1),你會發(fā)現界面上count的值并沒有 +3,仍然是 + 1。

          函數式更新

          如果新的 state 需要通過使用先前的 state 計算得出,那么可以將函數傳遞給 setState。該函數將接收先前的 state,并返回一個更新后的值。

          setCount(count => count + 1);setCount(count => count + 1);setCount(count => count + 1);

          更新對象

          當useState的值為對象時,可能會存在視圖不更新的情況,例如:查看在線示例

          const [list, setList] = useState([0, 1, 2]);const [useInfo, setUserInfo] = useState({    name: "張三",    age: 18});
          function handleOnClick() { list.push(4); list.push(4); setList(list);
          useInfo.name = "李四"; useInfo.age = 20; setUserInfo(useInfo);}
          return ( <div> <p>姓名:{useInfo.name}</p> <p>年齡:{useInfo.age}</p> <p>ist.length: {list.length}</p> <button onClick={handleOnClick}> 修改 </button> </div>);

          問題原因:React 中默認是淺監(jiān)聽,當state的值為對象時,棧中存的是對象的引用(地址),setState改變的是堆中的數據,棧中的地址還是原地址,React淺監(jiān)聽到地址沒變,故會認為State并未改變,所以沒有重渲染頁面。

          解決方案:只要改變了原對象的地址即可,可通過以下幾種方式實現

          • 將原對象進行克隆

          • 使用ES6的拓展運算符

          對于數組我們可以使用一些數組自身的方法來進行深拷貝:

          // 使用Array.sliceconst nextList = list.slice(0);nextList.push("slice");setList(nextList);
          // 使用Array.concatconst nextList = list.concat();nextList.push("concat");setList(nextList);

          總結:無論是在 useState 中,還是傳入函數中的參數,都不要直接去操作對象本身,先克隆出一份來再操作,避免引起一些意想不到的問題。


          無法在setSate后拿到最新的值

          由于setSate后并不會立即更新,React會在某個時候將多個 setSate進行合并后再更新。因此無法在 setState后拿到最新的值。一般有以下幾種方式可以拿到最新值:

          • 使用 useRef ,但是數據的更新不會引起視圖的更新

          • 使用 useEffect ,這種方式在很多場景下也不適用,每次更新都會執(zhí)行 useEffect 中的內容,往往我們在需求并不是如此

          • 使用函數式更新

          • 使用 ahooks 的 useGetState    【原理:使用useRef將useState的值存起來】

          查看在線示例

          const [count, setCount] = useState(0);const countRef = useRef(0);
          useEffect(() => { console.log("useEffect", count);}, [count]);
          function handleOnClick() { countRef.current += 1; setCount(count + 1); console.log("正常打印", count); console.log("countRef", countRef.current); setCount(count => { console.log("函數式更新獲取最新值", count); return count; });}
          return ( <div> <div> count: {count} </div> <button onClick={handleOnClick}> +1 </button> </div>);

          查看在線示例

          const useGetState = (initiateState) => {      const [state, setState] = useState(initiateState);      const stateRef = useRef(state);      stateRef.current = state;          const getState = useCallback(() => stateRef.current, []);          return [state, setState, getState];};

          定時器中獲取最新值

          在下面的例子中,無論是視圖還是打印,count 的值永遠都是0。查看在線示例

          const [count, setCount] = useState(0);useEffect(() => {    const interval = setInterval(() => {        console.log(count);        setCount(count + 1);    }, 1000);    return () => {        clearInterval(interval);    }}, []);

          問題原因:定時器在創(chuàng)建后一直都沒有被清除,因此內部獲取的狀態(tài)始終都是創(chuàng)建時state的狀態(tài)

          解決方案

             (1)定時器內部更新state使用函數式更新,函數式更新可以獲取到state的最新狀態(tài)。此方法可以解決視圖更新問題,但是在定時器中的打印仍然是0。

          (2)將state作為 useEffect 的依賴,state發(fā)生變化后會重新創(chuàng)建定時器


          useEffect


          如果你熟悉 React class 的生命周期函數,你可以把 useEffect Hook 看做 componentDidMountcomponentDidUpdatecomponentWillUnmount 這三個函數的組合。

          componentDidMountcomponentDidUpdate 不同的是,傳給 useEffect 的函數會在瀏覽器完成布局與繪制之后,在一個延遲事件中被調用。這使得它適用于許多常見的副作用場景,比如設置訂閱和事件處理等情況,因為絕大多數操作不應阻塞瀏覽器對屏幕的更新。查看官方文檔

          import React, { useState, useEffect } from 'react';
          function Example() { const [count, setCount] = useState(0);
          // Similar to componentDidMount and componentDidUpdate: useEffect(() => { // Update the document title using the browser API document.title = `You clicked ${count} times`; });
          return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> );}

          useEffect 在每次渲染后都會執(zhí)行,包括第一次渲染后和每次更新React 保證了每次運行 effect 的同時,DOM 都已經更新完畢。

          可以通過第二個參數來控制 useEffect 在什么情況下才執(zhí)行:查看在線示例

          import { useState, useEffect } from "react";
          export default () => { const [count, setCount] = useState(0); const [number, setNumber] = useState(0);
          // 沒有任何依賴,每次重新渲染都要執(zhí)行 useEffect(() => { console.log("null", count); });
          // 依賴值為空,只在第一次渲染后執(zhí)行一次 useEffect(() => { console.log("[]", count); }, []);
          // 只有依賴值發(fā)生變化后,才會執(zhí)行;第一次渲染也會執(zhí)行 useEffect(() => { console.log("count", count); }, [count]);
          function addCount() { setCount(count + 1); }
          function addNumber() { setNumber(number + 1); }
          return ( <div> <div>count: {count}</div> <div>number: {number}</div> <button onClick={addCount}>count+1</button> <button onClick={addNumber}>number+1</button> </div> );};

          依賴值為對象的時

          我們經常會將一個對象作為依賴,一般我們都是希望對象的內容發(fā)生變化時,去執(zhí)行某些操作。在實際的業(yè)務開發(fā)中,我們會遇到一些莫名其妙的坑,列舉幾個常見的現象:

          • 明明對象的內容已經發(fā)生了變化,但是為什么沒有觸發(fā)useEffect

          • 明明對象的內容沒有發(fā)生變化,但是為什么一直觸發(fā)useEffect

          這看起來有點像在說繞口令,出現問題的本質就是因為對象是引用類型,通過下面幾個例子可以更加深入的理解

          案例1:改變對象中的屬性值,未觸發(fā)useEffect

          const [info, setInfo] = useState({    name: "張三",    age: 18});
          useEffect(() => {console.log("info", info);}, [info]);
          function handleChangeName(e) { const value = e.target.value; setInfo((info) => { info.name = value; return info; });}
          return <input onChange={handleChangeName} />;

          問題原因:調用 setInfo 時,是直接改變的入參,此時返回改變后的信息其引用是沒有發(fā)生變化的。

          注意點:在任何情況下,都不能直接去改變入參,或者是直接改變state值本身。

          // 錯誤寫法info.name = value;setInfo(info);
          // 錯誤寫法setInfo((info) => { info.name = value; return info;});
          // 正確寫法setInfo({ ...info, name: value});
          // 正確寫法setInfo((info) => { return { ...info, name: value };});

          案例2:接受父組件的對象屬性作為依賴,useEffect頻繁觸發(fā)

          開發(fā)組件時,對某些屬性需要設置默認值,一般的寫法就是解構props時同時賦予默認值

          const {    count = 0,    list = []} = props;

          如果父組件沒有傳遞list屬性,每當父組件重新渲染時,子組件會跟隨重新渲染,每次渲染都會觸發(fā)useEffect。在線查看示例

          import { useState, useEffect } from "react";
          const Com = () => { const [count, setCount] = useState(0);
          function hanleOnClick() { setCount((count) => count + 1); }
          return ( <div> <button onClick={hanleOnClick}>add</button> <SubCom count={count} /> </div> );};
          const SubCom = (props) => { const { list = [], count } = props;
          useEffect(() => { console.log(list); }, [list]);
          return <div>子組件{count}</div>;};
          export default Com;

          問題原因:當父組件更新時,會重新渲染子組件,每次渲染,props.list 都被賦予了新的引用, 雖然看起來都是空數組,但是useEffect 是判斷l(xiāng)ist的引用發(fā)生了變化,所以就會執(zhí)行。一旦該組件用于復雜場景,導致更新頻繁就會出現白屏現象。

          正確寫法:在用到的地方去做兼容處理,而不是直接賦予默認值。


          案例3:對象內容未變化時,我們不希望觸發(fā)useEffect

          將對象作為依賴時,往往都是希望其內容發(fā)生變化時,才觸發(fā)相應的執(zhí)行。但是 useEffect 的本質是監(jiān)聽引用的變化,很多情況下這與我們實際的業(yè)務開發(fā)有點不相符。

          • 業(yè)務層經常會對一些狀態(tài)進行重置,setState([]) 或者 setState({}) 。有可能本身state的值就是 [] 或者 {} ,重置后,內容未發(fā)生變化,但是引用已經改變,從而導致觸發(fā) useEffect 。查看在線示例

          import { useState, useEffect } from "react";
          const Com = () => { const [list, setList] = useState([]);
          function reset() { setList([]); }
          return ( <div> <p>{list.join(",")}</p> <button onClick={reset}>reset</button> <SubCom list={list} /> </div> );};
          const SubCom = (props) => { const { list } = props;
          useEffect(() => { console.log(list); }, [list]);
          return <div>子組件</div>;};
          export default Com;

          解決方案

          • 將對象轉為字符串后再作為useEffect的依賴。

          useEffect(() => {console.log(list);}, [JSON.stringify(list)]);
          • 使用 ahooks 的 useDeepCompareEffect 來解決。用法與 useEffect 一致,但 deps 通過 lodash isEqual 進行深比較。

          import { useRef } from 'react';import type { DependencyList, useEffect, useLayoutEffect } from 'react';import isEqual from 'lodash/isEqual';
          type EffectHookType = typeof useEffect | typeof useLayoutEffect;type CreateUpdateEffect = (hook: EffectHookType) => EffectHookType;
          const depsEqual = (aDeps: DependencyList = [], bDeps: DependencyList = []) => { return isEqual(aDeps, bDeps);};
          export const createDeepCompareEffect: CreateUpdateEffect = (hook) => (effect, deps) => { const ref = useRef<DependencyList>(); const signalRef = useRef<number>(0);
          // 本地更新的依賴值與緩存的依賴深比較 if (deps === undefined || !depsEqual(deps, ref.current)) { // 將依賴保存一份 ref.current = deps; // 如果發(fā)現變更,則改變signalRef的值,是為了觸發(fā)真正的useEffect signalRef.current += 1; }
          hook(effect, [signalRef.current]);};

          案例4:第一次渲染時,不希望觸發(fā)useEffect

          useEffect第一次渲染后和每次更新 都會執(zhí)行。

          有的業(yè)務場景并不希望在第一次加載的時候觸發(fā),此場景可通過創(chuàng)建一個標志位來解決。當然可以直接使用 ahooks 中的 useUpdateEffect 這個hook,其原理也是使用標志位來實現的。查看在線示例

          import { useState, useEffect, useRef } from "react";
          export default () => { const [count, setCount] = useState(0); const isMounted = useRef(false);
          // 第一次渲染置為false useEffect(() => { isMounted.current = false; }, []);
          useEffect(() => { console.log("第一渲染時會執(zhí)行"); }, [count]);
          // 第一次渲染將標志位置為true useEffect(() => { if (!isMounted.current) { isMounted.current = true; } else { console.log("第一渲染時不會執(zhí)行,后續(xù)更新才會執(zhí)行"); } }, [count]);
          return ( <div> <button onClick={() => setCount((c) => c + 1)}>+1</button> </div> );};

          案例5:兩個useEffect更新相互依賴,無限更新導致白屏

          const {    value,    defaultValue = 0.5,    onChange} = props;
          const [innerValue, setInnerValue] = useState<number>(defaultValue);
          // 取名為useEffect1useEffect(() => { if (value !== undefined) { setInnerValue(value); }}, [value]);
          // 取名為useEffect2useEffect(() => { onChange?.(innerValue);}, [innerValue]);

          組件功能:這里是一個自定義的表單組件,其中 value 是受控屬性,當改變表單值時,通過 onChange 通知上層,上層改變 value 值。

          如果業(yè)務層在初始化時,對value 賦予的初始值不是undefined 并且不等于 defaultValue 的值,則會導致白屏現象,下面來分析一下整個過程:

          • 假設業(yè)務層對 value 賦予了一個初始值0.6。在第一次加載時,useEffect1 和 useEffect2 都會執(zhí)行一遍。

          • useEffect1 執(zhí)行時,會將 innerValue 的值設置為 0.6

          • useEffect2 執(zhí)行時,會將 innerValue 的值通過onChange 方法通知到業(yè)務層,這里要注意,此時的 innerValue 值為 defaultValue 的值,是0.5 。并不是 useEffect1 中改變后的 0.6;

          • 當業(yè)務層監(jiān)聽到調用了 onChange 時,會將 onChange 傳過來的值也就是0.5更新到 value上。

          • 當進入第二次更新時,useEffect1 監(jiān)聽到 value 的值從0.6變?yōu)榱?.5,因此會執(zhí)行useEffect1 。useEffect2 監(jiān)聽到 innerValue 的值從0.5 變?yōu)榱?.6,因此也會執(zhí)行useEffect2,從而又觸發(fā)了onChange

          • 由于 value 與 innerValue 的值永遠都在同一次更新中,更新為了不同的值,會導致這個更新會無限的循環(huán)執(zhí)行下去,從而導致白屏。


          問題點:

          • 在第一次加載時,就會觸發(fā)useEffect2導致調用onChange方法。

          • 如果是業(yè)務層手動變更了value值,也會觸發(fā)onChange

          正確寫法:

          • 在真正手動改變表單值的時候,去調用 onChange,而不是直接去使用useEffect監(jiān)聽innerValue的變化


          案例6:不要將普通變量作為依賴

          查看在線示例

          import { useState, useEffect } from "react";
          export default () => { const [count, setCount] = useState(0);
          const list = [];
          useEffect(() => { console.log("觸發(fā)useEffect", count); }, [list]);
          return ( <div> <p>{count}</p> <button onClick={() => setCount((c) => c + 1)}>+1</button> </div> );};

          問題原因: 組件在每次更新時,會對list賦予新的值,與 案例2 原理相同。


          案例7:依賴監(jiān)聽useRef的值,有時可以觸發(fā)更新,有時無法觸發(fā)更新

          查看在線示例

          import { useState, useEffect, useRef } from "react";
          export default () => { const [count, setCount] = useState(0); const countRef = useRef(0); // 取名為useEffect1 useEffect(() => { console.log("count", count); }, [count]);
          // 取名為useEffect2 useEffect(() => { console.log("countRef", countRef); }, [countRef.current]);

          return ( <div> <p>{count}</p> <button onClick={() => setCount((c) => c + 1)}>button1</button> <button onClick={() => (countRef.current += 1)}>button2</button> </div> );};

          現象

          • 點擊 button1 時,會觸發(fā) useEffect1

          • 點擊 button2 時,不會觸發(fā) useEffect2

          • 再次點擊 button1 時,會觸發(fā) useEffect1useEffect2

          問題原因:只有狀態(tài)變更的時候,才會觸發(fā)更新,而狀態(tài)變更,只有 useStateuseReducer 可以觸發(fā)更新。

          使用指南:建議不要使用 useRef 的值作為依賴,除非你十分確定當 useRef 的值改變時,有state發(fā)生了改變。



          瀏覽 915
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  豆花av电影在线 豆花视频精品一区 | 国产黄片免费视频 | 99久久99九九九99九他书对 | 亚洲黄色毛片 | 中文字幕日本无码 |