6 ошибок, которых следует избегать при использовании React Hooks

внешний интерфейс JavaScript React.js
6 ошибок, которых следует избегать при использовании React Hooks

Это 4-й день моего участия в ноябрьском испытании обновлений, подробности о мероприятии:Вызов последнего обновления 2021 г.


Сегодня давайте рассмотрим некоторые подводные камни при использовании хуков React и как их правильно использовать, чтобы их избежать.

Обзор проблемы:

  1. Не меняйте порядок вызова хуков;
  2. не используйте старое состояние;
  3. не создавайте старые замыкания;
  4. Не забудьте убрать побочные эффекты;
  5. Не используйте useState, если повторный рендеринг не требуется;
  6. Не пропустите зависимости useEffect.

1. Не меняйте порядок вызова хуков

Сначала рассмотрим пример:

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

Этот компонент получает идентификатор параметра, который используется в качестве параметра в useEffect для запроса информации об игре. И сохраните полученные данные в переменной состояния игры. ​

Когда компонент выполняется, он извлекает производные данные и обновляет состояние. Но у этого компонента есть предупреждение:image.pngЭто говорит нам о том, что выполнение хука неверно. Потому что, когда идентификатор пуст, компонент запросит и выйдет напрямую. Если идентификатор существует, вызываются два хука useState и useEffect. Такое условное выполнение хуков может привести к неожиданным и трудно отлаживаемым ошибкам. На самом деле внутренняя работа хуков React требует, чтобы при рендеринге компонента хуки всегда вызывались в одном и том же порядке. ​

Вот что говорится в официальной документации React:Не вызывайте хуки в циклах, условных выражениях или вложенных функциях, всегда вызывайте их в самом верху вашей функции React и перед возвратом.

Самый прямой способ решить эту проблему — следовать официальной документации.Убедитесь, что вы всегда вызываете их в самом верху ваших функций React и перед любым возвратом:

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

Таким образом, независимо от того, является ли входящий идентификатор пустым, useState и useEffect всегда будут использоваться в одном и том же порядке, так что ошибки не будет~ ​

Правила хуков в официальной документации React:«Правила крючка», вы можете использовать плагин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 здесь выполнит операцию увеличения счетчика переменной состояния три раза после нажатия кнопки. Так один клик увеличивается на 3? Но на самом деле это не так. После нажатия кнопки счетчик увеличится только на 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)

2. Не создавайте старые замыкания

Как мы все знаем, React Hooks реализуются с помощью замыканий. При использовании хука, который принимает обратный вызов в качестве параметра, например:

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

На этом этапе мы можем создать старое замыкание, которое фиксирует устаревшее состояние или переменные свойства. Это может быть немного абстрактно. Давайте рассмотрим пример. В этом примере useEffect печатает значение count каждые 2 секунды:

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

Окончательный вывод выглядит следующим образом:

image.png

Как вы можете видеть, значение счетчика, печатаемое каждый раз, равно 0, что не совпадает с фактическим значением счетчика. Почему это так?

При первом рендеринге все должно быть в порядке, журнал закрытия будет печатать счет как 0. Начиная со второго раза, count увеличивается на 1 каждый раз, когда нажимается кнопка, но setInterval по-прежнему вызывает старое закрытие журнала со значением count 0, полученным из первого рендеринга. Метод журнала является старым замыканием, поскольку он фиксирует устаревшую переменную состояния 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>
    </>
  );
}

Таким образом, когда изменяется количество переменных состояния, замыкание обновляется. Чтобы предотвратить захват старых значений замыканием, убедитесь, что любое свойство или состояние, используемое в обратном вызове, предоставленном хуку, указано как зависимость.

4. Не забудьте убрать побочные эффекты

Есть много побочных эффектов, таких как запросы на выборку, 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>
    </>
  );
}

Этот компонент очень прост, то есть при нажатии кнопки счетчик переменной состояния будет увеличиваться на 1 каждую секунду. Когда мы нажимаем кнопку +, она работает так, как мы ожидали. Но когда мы нажимаем кнопку «Удалить», в консоли появляется предупреждение:

image.png

Чтобы исправить это, просто используйте useEffect для очистки таймера:

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

Когда мы пишем какие-то побочные эффекты, нам нужно знать, нужно ли убрать этот побочный эффект.

5. Не используйте useState, если вам не нужен повторный рендеринг

В хуках React мы можем использовать хук useState для управления состоянием. Хотя он относительно прост в использовании, при неправильном использовании могут возникнуть неожиданные проблемы. Рассмотрим следующий пример:

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

В приведенном выше компоненте есть две кнопки, первая кнопка запускает увеличение счетчика на единицу, а вторая кнопка отправляет запрос на основе текущего состояния счетчика. Как видите, счетчик переменной состояния не используется на этапе рендеринга. Таким образом, каждый раз, когда нажимается первая кнопка, будет происходить нежелательный повторный рендеринг. ​

Поэтому, когда возникает необходимость использовать переменную в компоненте для сохранения его состояния во время рендеринга, а не запускать повторный рендеринг, лучше использовать 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. По умолчанию он всегда запускается при каждом повторном рендеринге. Но это может привести к ненужному рендерингу. Мы можем избежать этих ненужных визуализаций, установив для массива зависимостей значение useEffect. ​

Рассмотрим следующий пример:

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

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

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

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

Этот компонент может не иметь реального значения, просто выведите значение count. Затем будет предупреждение:

image.png

Это означает, что в useEffect отсутствует зависимость от счетчика, что небезопасно. Нам нужно включить зависимость или удалить массив зависимостей. В противном случае код в useEffect может использовать старое значение.

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

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

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

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

Если переменная состояния count не используется в useEffect, также безопасно оставить зависимость пустой:

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

На сегодня это все. Если вы считаете, что это полезно, приходите в серию из трех ссылок~