React Hook填坑

若是已經使用過 Hook,相信你必定回不去了,這種用函數的方式去編寫有狀態組件簡直太爽啦。javascript

若是還沒使用過 Hook,那你要趕忙升級你的 React(v16.8+),投入 Hook 的懷抱吧。html

至於 Hook 的好處這裏就很少說了,上一篇已經講過了——React Hook上車java

Hook 雖好,操做不當但是容易翻車的哦。react

下面,咱們就來聊聊在使用過程當中可能遇到的坑吧......git

useState

useState 只在組件首次渲染的時候執行

坑:useState的初始值,只在第一次有效github

證據:npm

當點擊按鈕修改name的值的時候,我發如今Child組件,是收到了,可是並無經過useState賦值給name!segmentfault

const Child = ({data}) =>{
    console.log('child render...', data) // 每次更新都會執行
    const [name, setName] = useState(data) // 只會在首次渲染組件時執行
    return (
        <div>
            <div>child</div>
            <div>{name} --- {data}</div>
        </div>
    );
}

const Hook =()=>{
    console.log('Hook render...')
    const [name, setName] = useState('rose')
    return(
        <div>
            <div>
                {count}
            </div>
            <button onClick={()=>setName('jack')}>update name </button>
            <Child data={name}/>
        </div>
    )
}

想在第一次 render 前執行的代碼放 useState() 裏面

上面咱們已經知道了useState()只會在第一次渲染的時候才執行,那麼這有什麼實用價值嗎?答案:能夠把第一次 render 前執行的代碼放入其中。數組

例如:緩存

const instance = useRef(null);
useState(() => {
  instance.current = 'initial value';
});

相似 class component 裏的constructorcomponentWillMount

useState 裏數據必須爲 immutable

啥?你還不知道 immutable 是個啥?甩手就是兩個連接:Immutable.js 瞭解一下Immutable 詳解及在 React 實踐

什麼是 Immutable Data?
首先,你要知道 JavaScript 中的對象通常是可變的(Mutable Data),由於使用了引用賦值。
Immutable Data 就是一旦建立,就不能再被更改的數據。對 Immutable 對象的任何修改或添加刪除操做都會返回一個新的 Immutable 對象。

雖然 class component 的 state 也提倡使用 immutable data,但不是強制的,由於只要調用了setState就會觸發更新。

可是使用useState時,若是在更新函數裏傳入同一個對象將沒法觸發更新

證據:

const [list, setList] = useState([2,32,1,534,44]);
return (
  <>
    <ol>
      {list.map(v => <li key={v}>{v}</li>)}
    </ol>
    <button
      onClick={() => {
        // bad:這樣沒法觸發更新!
        setList(list.sort((a, b) => a - b));
        // good:必須傳入一個新的對象!
        setList(list.slice().sort((a, b) => a - b));
      }}
    >sort</button>
  </>
)

useState 過期的閉包

以前就說過,Hook 產生問題時,90%都是閉包引發的。下面就來看一下這個詭異的bug:

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

  function handleClickAsync() {
    setTimeout(function delay() {
      setCount(count + 1); // 問題所在:此時的 count 爲1s前的count!!!
    }, 1000);
  }

  function handleClickSync() {
    setCount(count + 1);
  }

  return (
    <div>
      {count}
      <button onClick={handleClickAsync}>異步加1</button>
      <button onClick={handleClickSync}>同步加1</button>
    </div>
  );
}

點擊「異步加1」按鍵,而後當即點擊「同步加1」按鈕。你會驚奇的發現,count 只更新到 1。

這是由於 delay() 是一個過期的閉包。

來看看這個過程發生了什麼:

  1. 初始渲染:count 值爲 0。
  2. 點擊「異步加1」按鈕,delay() 閉包捕獲 count 的值 0,setTimeout() 1 秒後調用 delay()。
  3. 點擊「同步加1」按鈕,handleClickSync() 調用 setCount(0 + 1) 將 count 的值設置爲 1,組件從新渲染。
  4. 1 秒以後,setTimeout() 執行 delay() 。可是 delay() 中閉包保存 count 的值是初始渲染的值 0,因此調用 setState(0 + 1),結果count保持爲 1。

delay() 是一個過期的閉包,它使用在初始渲染期間捕獲的過期的 count 變量。

爲了解決這個問題,可使用函數方法來更新 count 狀態:

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

  function handleClickAsync() {
    setTimeout(function delay() {
      setCount(count => count + 1); // 重點:setCount傳入的回調函數用的是最新的 state!!!
    }, 1000);
  }

  function handleClickSync() {
    setCount(count + 1);
  }

  return (
    <div>
      {count}
      <button onClick={handleClickAsync}>異步加1</button>
      <button onClick={handleClickSync}>同步加1</button>
    </div>
  );
}

useEffect

如何在 useEffect 中使用 async

上一篇文章中咱們提到過:useEffect的 callback 函數要麼返回一個能清除反作用的函數,要麼就不返回任何內容。

而 async 函數返回的是 Promise 對象,那咱們要怎麼在 useEffect 的callback 中使用 async 呢?

最簡單的方法是IIFE(自執行函數):

useEffect(() => {
  (async () => {
    await fetchSomething();
  })();
}, []);

useEffect 死循環

  1. useEffect 在傳入第二個參數時必定注意:第二個參數不能爲引用類型,會形成死循環。
    好比:[]===[] 爲false,因此會形成 useEffect 會一直不停的渲染。
  2. useEffect 的 callback 函數中改變的 state 必定不能在該 useEffect 的依賴數組中。好比:useEffect(()=>{ setCount(count); }, [count]);依賴 count,callback 中又 setCount(count)。

推薦啓用 eslint-plugin-react-hooks 中的 exhaustive-deps 規則。此規則會在添加錯誤依賴時發出警告並給出修復建議。

函數做爲依賴的時候死循環

有時候,咱們須要將函數做爲依賴項傳入依賴數組中,例如:

// 子組件
let Child = React.memo((props) => {
  useEffect(() => {
    props.onChange(props.id)
  }, [props.onChange, props.id]);
  
  return (
    <div>{props.id}</div>
  );
});

// 父組件
let Parent = () => {
  let [id, setId] = useState(0);
  let [count, setCount] = useState(0);
  const onChange = (id) => {
    // coding
    setCount(id);
  }
  
  return (
    <div>
    	{count}
      <Child onChange={onChange} id={id} />  // 重點:這裏有性能問題!!!
    </div>
  );
};

代碼中重點位置,每次父組件render,onChange引用值確定會變。所以,子組件Child一定會render,子組件觸發useEffect,從而再次觸發父組件render....循環往復,這就會形成死循環。下面咱們來優化一下:

// 子組件
let Child = React.memo((props) => {
  useEffect(() => {
    props.onChange(props.id)
  }, [props.onChange, props.id]);
  return (
    <div>{props.id}</div>
  );
});

// 父組件
let Parent = () => {
  let [id, setId] = useState(0);
  let [count, setCount] = useState(0);
  const onChange = useCallback(() => { // 重點:經過useCallback包裹一層便可達到緩存函數的目的
    // coding
  }, [id]); // id 爲依賴值
  return (
    <div>
    	{count}
      <Child onChange={onChange} id={id} />  // 重點:這個onChange在每次父組件render都會改變!
    </div>
  );
};

useCallback將返回該回調函數的 memoized 版本,該回調函數僅在某個依賴項改變時纔會更新。當你把回調函數傳遞給通過優化的並使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子組件時,它將很是有用。

useCallback(fn, deps) 至關於 useMemo(() => fn, deps)

useEffect 裏面拿不到最新的props和state

useEffect裏面使用到的 state 的值, 固定在了useEffect內部,不會被改變,除非useEffect刷新,從新固定 state 的值。

useRef保存任何可變化的值,.current屬性老是取最新的值。

function Example() {
  const [count, setCount] = useState(0);
  const latestCount = useRef(count);

  useEffect(() => {
    // Set the mutable latest value
    latestCount.current = count;
    
    setTimeout(() => {
      // Read the mutable latest value
      console.log(`You clicked ${latestCount.current} times`);
    }, 3000);
  });

總結

以上只是收集了一部分工做中可能會遇到的坑,大體分爲2種:

  1. 閉包引發的 state 值過時
  2. 依賴值監聽問題致使死循環

之後遇到其餘的問題會繼續補充...

參考:
react hooks踩坑記錄

使用 JS 及 React Hook 時須要注意過期閉包的坑(文中有解決方法)

終於搞懂react hooks了!!!!

相關文章
相關標籤/搜索