天天看點

使用 React Hooks 時要避免的6個錯誤!

今天來看看在使用React hooks時的一些坑,以及如何正确的使用避免這些坑。

​問題概覽:​

  1. 不要改變 hooks 的調用順序;
  2. 不要使用舊的狀态;
  3. 不要建立舊的閉包;
  4. 不要忘記清理副作用;
  5. 不要在不需要重新渲染時使用useState;
  6. 不要缺少useEffect依賴。

1. 不要改變 hooks 的調用順序

下面先來看一個例子:

const FetchGame = ({ id }) => {
  if (!id) {
    return '請選擇一個遊戲';
  }

  const [game, setGame] = useState({ 
    name: '',
    description: '' 
  });

  useEffect(() => {
    const fetchGame = async () => {
      const response = await fetch(`/api/game/${id}`);
      const fetchedGame = await response.json();
      setGame(fetchedGame);
    };
    fetchGame();
  }, [id]);

  return (
<div>
<div>Name: {game.name}</div>
<div>Description: {game.description}</div>
</div>
  );
}      

這個元件接收一個參數id,在useEffect中會使用這個id作為參數去請求遊戲的資訊。并将擷取的資料儲存在狀态變量game中。

當元件執行時,會擷取到資料并更新狀态。但是這個元件有一個警告:

使用 React Hooks 時要避免的6個錯誤!

這裡是告訴我們,鈎子的執行是不正确的。因為當id為空時,元件會提示,并直接退出。如果id存在,就會調用useState和useEffect這兩個hook。這樣有條件的執行鈎子時就可能會導緻意外并且難以調試的錯誤。實際上,React hooks内部的工作方式要求元件在渲染時,總是以相同的順序來調用hook。

這也就是React官方文檔中所說的:​不要在循環,條件或嵌套函數中調用 Hook, 確定總是在你的 React 函數的最頂層以及任何 return 之前調用他們。​​

解決這個問題最直接的辦法就是按照官方文檔所說的,​確定總是在你的 React 函數的最頂層以及任何 return 之前調用他們:​

const FetchGame = ({ id }) => {
  const [game, setGame] = useState({ 
    name: '',
    description: '' 
  });

  useEffect(() => {
    const fetchGame = async () => {
      const response = await fetch(`/api/game/${id}`);
      const fetchedGame = await response.json();
      setGame(fetchedGame);
    };
    id && fetchGame();
  }, [id]);

  if (!id) {
    return '請選擇一個遊戲';
  }

  return (
<div>
<div>Name: {game.name}</div>
<div>Description: {game.description}</div>
</div>
  );
}      

這樣,無論傳入的id是否為空,useState和useEffect總會以相同的順序來調用,這樣就不會出錯啦~

React官方文檔中的Hook規則:《Hook 規則》,可以使用插件eslint-plugin-react-hooks來幫助我們檢查這些規則。

2. 不要使用舊的狀态

先來看一個計數器的例子:

const Increaser = () => {
  const [count, setCount] = useState(0);

  const increase = useCallback(() => {
    setCount(count + 1);
  }, [count]);

  const handleClick = () => {
    increase();
    increase();
    increase();
  };

  return (
<>
<button onClick={handleClick}>+</button>
<div>Counter: {count}</div>
</>
  );
}      

這裡的handleClick方法會在點選按鈕後執行三次增加狀态變量count的操作。那麼點選一次是否會增加3呢?事實并非如此。點選按鈕之後,count隻會增加1。問題就在于,當我們點選按鈕時,相當于下面的操作:

const handleClick = () => {
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);
};      

當第一次調用setCount(count + 1)時是沒有問題的,它會将count更新為1。接下來第2、3次調用setCount時,count還是使用了舊的狀态(count為0),是以也會計算出count為1。發生這種情況的原因就是狀态變量會在下一次渲染才更新。

解決這個問題的辦法就是,​使用函數的方式來更新狀态:​

const Increaser = () => {
  const [count, setCount] = useState(0);

  const increase = useCallback(() => {
    setCount(count => count + 1);
  }, [count]);

  const handleClick = () => {
    increase();
    increase();
    increase();
  };

  return (
<>
<button onClick={handleClick}>+</button>
<div>Counter: {count}</div>
</>
  );
}      

這樣改完之後,React就能拿到最新的值,當點選按鈕時,就會每次增加3。是以需要記住:​如果要使用目前狀态來計算下一個狀态,就要使用函數的式方式來更新狀态:​

setValue(prevValue => prevValue + someResult)      

3. 不要建立舊的閉包

衆所周知,React Hooks是依賴閉包實作的。當使用接收一個回調作為參數的鈎子時,比如:

useEffect(callback, deps)
useCallback(callback, deps)      

此時,我們就可能會建立一個舊的閉包,該閉包會捕獲過時的狀态或者prop變量。這麼說可能有些抽象,下面來看一個例子,這個例子中,useEffect每2秒會列印一次count的值:

const WatchCount = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setInterval(function log() {
      console.log(`Count: ${count}`);
    }, 2000);
  }, []);

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

  return (
<>
<button onClick={handleClick}>+</button>
<div>Count: {count}</div>
</>
  );
}      

最終的輸出的結果如下:

使用 React Hooks 時要避免的6個錯誤!

可以看到,每次列印的count值都是0,和實際的count值并不一樣。為什麼會這樣呢?

在第一次渲染時應該沒啥問題,閉包log會将count列印出0。從第二次開始,每次當點選按鈕時,count會增加1,但是setInterval仍然調用的是從初次渲染中捕獲的count為0的舊的log閉包。log方法就是一個舊的閉包,因為它捕獲的是一個過時的狀态變量count。

這裡的解決方案就是,當count發生變化時,就重置定時器:

const WatchCount = () => {
  const [count, setCount] = useState(0);

  useEffect(function() {
    const id = setInterval(function log() {
      console.log(`Count: ${count}`);
    }, 2000);
    return () => clearInterval(id);
  }, [count]);

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

  return (
<>
<button onClick={handleClick}>+</button>
<div>Count: {count}</div>
</>
  );
}      

這樣,當狀态變量count發生變化時,就會更新閉包。為了防止閉包捕獲到舊值,就要確定在提供給hook的回調中使用的prop或者state都被指定為依賴性。

4. 不要忘記清理副作用

有很多副作用,比如fetch請求、setTimeout等都是異步的,如果不需要這些副作用或者元件在解除安裝時,不要忘記清理這些副作用。下面來看一個計數器的例子:

const DelayedIncreaser = () => {
  const [count, setCount] = useState(0);
  const [increase, setShouldIncrease] = useState(false);

  useEffect(() => {
    if (increase) {
      setInterval(() => {
        setCount(count => count + 1)
      }, 1000);
    }
  }, [increase]);

  return (
<>
<button onClick={() => setShouldIncrease(true)}>
        +
</button>
<div>Count: {count}</div>
</>
  );
}

const MyApp = () => {
  const [show, setShow] = useState(true);

  return (
<>
      {show ? <DelayedIncreaser /> : null}
<button onClick={() => setShow(false)}>解除安裝</button>
</>
  );
}      

這個元件很簡單,就是在點選按鈕時,狀态變量count每秒會增加1。當我們點選+按鈕時,它會和我們預期的一樣。但是當我們點選“解除安裝”按鈕時,控制台就會出現警告:

使用 React Hooks 時要避免的6個錯誤!

修複這個問題隻需要使用useEffect來清理定時器即可:

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

當我們編寫一些副作用時,我們需要知道這個副作用是否需要清除。

5. 不要在不需要重新渲染時使用useState

在React hooks 中,我們可以使用useState hook來進行狀态的管理。雖然使用起來比較簡單,但是如果使用不恰當,就可能會出現意想不到的問題。來看下面的例子:

const Counter = () => {
  const [counter, setCounter] = useState(0);

  const onClickCounter = () => {
    setCounter(counter => counter + 1);
  };

  const onClickCounterRequest = () => {
    apiCall(counter);
  };

  return (
<div>
<button onClick={onClickCounter}>Counter</button>
<button onClick={onClickCounterRequest}>Counter Request</button>
</div>
  );
}      

在上面的元件中,有兩個按鈕,第一個按鈕會觸發計數器加一,第二個按鈕會根據目前的計數器狀态發送一個請求。可以看到,狀态變量counter并沒有在渲染階段使用。是以,每次點選第一個按鈕時,都會有不需要的重新渲染。

是以,當遇到這種需要在元件中使用一個變量在渲染中保持其狀态,并且不會觸發重新渲染時,那麼useRef會是一個更好的選擇,下面來對上面的例子使用useRef進行改編:

const Counter = () => {
  const counter = useRef(0);

  const onClickCounter = () => {
    counter.current++;
  };

  const onClickCounterRequest = () => {
    apiCall(counter.current);
  };

  return (
<div>
<button onClick={onClickCounter}>Counter</button>
<button onClick={onClickCounterRequest}>Counter Request</button>
</div>
  );
}      

6. 不要缺少useEffect依賴

useEffect是React Hooks中最常用的Hook之一。預設情況下,它總是在每次重新渲染時運作。但這樣就可能會導緻不必要的渲染。我們可以通過給useEffect設定依賴數組來避免這些不必要的渲染。

來看下面的例子:

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

  const showCount = (count) => {
    console.log("Count", count);
  };

  useEffect(() => {
    showCount(count);
  }, []);

  return (
<div>Counter: {count}</div>
  );
}      

這個元件可能沒有什麼實際的意義,隻是列印了count的值。這時就會有一個警告:

使用 React Hooks 時要避免的6個錯誤!

這裡是說,useEffect缺少一個count依賴,這樣是不安全的。我們需要包含一個依賴項或者移除依賴數組。否則useEffect中的代碼可能會使用舊的值。

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

  const showCount = (count) => {
    console.log("Count", count);
  };

  useEffect(() => {
    showCount(count);
  }, [count]);

  return (
<div>Counter: {count}</div>
  );
}      

如果useEffect中沒有用到狀态變量count,那麼依賴項為空也會是安全的:

useEffect(() => {
  showCount(996);
}, []);      

今天的分享就到這裡,如果覺得有用就來個三連吧~