Как использовать все преимущества useEffect в React

Как использовать все преимущества useEffect в React

12 декабря 2022 г.

useEffect — один из наиболее широко используемых хуков в React. Оптимизация useEffect в React значительно повышает производительность и иногда избавляет ваш код от неприятных ошибок. Я сталкивался со многими ситуациями, когда возникали проблемы с производительностью или некоторые трудно обнаруживаемые ошибки из-за неправильного использования useEffect. Здесь я расскажу о нескольких шаблонах оптимизации, которых очень легко придерживаться, но они очень эффективны.

1. Использовать примитивы в массиве зависимостей

Мы часто передаем объекты JavaScript в массиве зависимостей useEffect. Хотя в этом нет ничего плохого, иногда имеет смысл деструктурировать объект JavaScript и передать только конкретное значение в массив зависимостей useEffect. Давайте посмотрим на пример. Предположим, у нас есть компонент UserInfo, который принимает объект user в качестве реквизита и извлекает данные транзакции пользователя всякий раз, когда пользователь изменяется. Нам нужно только получать данные о транзакциях пользователя при изменении пользователя. Наш пользовательский объект выглядит так:

{
  "id": 123,
  "name": "John Doe",
  "imageURL": "...",
  "isActive": false
}

Типичная реализация будет выглядеть так:

function UserInfo({ user }) {

  useEffect(() => {
    getUserData(user.id);
  }, [user]);

  return (
    ...
  );
}

Это будет получать пользовательские данные с сервера всякий раз, когда изменяется пользовательский объект. Но глядя на пользовательский объект, имеет смысл получать новые данные только при изменении user.id. Могут быть случаи, когда данные пользователя изменяются, а id нет, что не принесет нам пользы при вызове API и повторном получении данных с сервера. Допустим, пользовательский isActive изначально был false, а позже изменился на true. В этом случае нам не нужно снова получать данные о транзакциях пользователя, потому что пользователь все тот же. Мы можем избежать этого избыточного вызова API, изменив наши зависимости useEffect. Вот рефакторинг компонента с использованием примитивных типов данных:

function UserInfo({ user }) {
  const { id } = user;

  useEffect(() => {
    getUserData(id);
  }, [id]);

  return (
    ...
  );
}

С приведенной выше оптимизацией useEffect мы будем получать данные о транзакциях пользователя только при изменении id пользователя. Помните, что в зависимостях useEffect непримитивы (например, объекты и массивы) проверяются на равенство только по ссылке, а не по значению. Таким образом, следующий код всегда будет возвращать false:

{ name: "Huzaima" } === { name: "Huzaima" }

2. Избегайте ненужных зависимостей

Иногда мы используем useEffect для получения нового состояния из нашего текущего состояния. Предположим, у нас есть счетчик, который суммирует длину строки каждый раз, когда обновляется входное значение. Мы можем реализовать это следующим образом:

function CharacterCounter() {
  const [inputValue, setInputValue] = useState("");
  const [compoundStringLength, setCompoundStringLength] = useState(0);

  useEffect(() => {
    if (inputValue) {
      setCompoundStringLength(compoundStringLength + inputValue.length);
    }
  }, [inputValue, compoundStringLength]);

  return (
    ...
  );
}

Приведенный выше код приводит к бесконечному выполнению useEffect. Почему? Из-за неправильной зависимости. Мы обновляем compoundStringLength внутри useEffect и используем его в качестве зависимости. Как решить эту проблему? Нам нужно использовать compoundStringLength внутри useEffect для вычислений, поэтому мы не можем от него избавиться. Но что мы можем сделать, так это удалить его из массива зависимостей. Вы можете подумать, что это приведет к неправильному значению compoundStringLength из-за того, что useEffect является замыканием. Вы правы... и ошибаетесь. Это правильно, поскольку useEffect является замыканием, поэтому мы не получим правильное значение compoundStringLength, если не используем его в массиве зависимостей. Тем не менее, есть способ получить правильное значение compoundStringLength, не указывая его в массиве зависимостей. Функция установки состояния (setCompoundStringLength) принимает не только значение, но и функцию. Сигнатура этой функции выглядит так:

setCompoundStringLength: (currentStateValue) => newStateValue;

Мы можем использовать приведенную выше сигнатуру функции в useEffect, чтобы получить правильное значение compoundStringLength для наших вычислений без указания его в качестве зависимости. Рефакторинг оптимизации useEffect выглядит следующим образом:

function CharacterCounter() {
  const [inputValue, setInputValue] = useState("");
  const [compoundStringLength, setCompoundStringLength] = useState(0);

  useEffect(() => {
    if (inputValue) {
      setCompoundStringLength(
        (compoundStringLength) => compoundStringLength + inputValue.length
      );
    }
  }, [inputValue]);

  return (
    ...
  );
}

3. Функции подъема

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

function EmailValidate() {
  const [email, setEmail] = useState("");

  const isEmailValid = (input) => {
    const regex =
      /^(([^<>()[].,;:s@"]+(.[^<>()[].,;:s@"]+)*)|(".+"))@(([[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}])|(([a-zA-Z-0-9]+.)+[a-zA-Z]{2,}))$/;

    if (regex.test(input)) return "Email is valid";
    return "Email is invalid";
  };

  return (
    <>
      <input value={email} onChange={(event) => setEmail(event.target.value)} />
      <p>{isEmailValid(email)}</p>
    </>
  );
}

Здесь вы можете видеть, что функция isEmailValid является чистой функцией и не зависит от свойств и/или состояния компонента. Мы можем легко оптимизировать это, извлекая функцию за пределы компонента. Переписывание вышеуказанного компонента будет выглядеть так:

const isEmailValid = (input) => {
  const regex =
    /^(([^<>()[].,;:s@"]+(.[^<>()[].,;:s@"]+)*)|(".+"))@(([[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}])|(([a-zA-Z-0-9]+.)+[a-zA-Z]{2,}))$/;

  if (regex.test(input)) return "Email is valid";
  return "Email is invalid";
};

function EmailValidate() {
  const [email, setEmail] = useState("");

  return (
    <>
      <input value={email} onChange={(event) => setEmail(event.target.value)} />
      <p>{isEmailValid(email)}</p>
    </>
  );
}

4. Очистить

Основной целью useEffect является обработка побочных эффектов. Иногда вам нужно убрать эти побочные эффекты, такие как подписки на обработчики событий или наблюдаемые объекты. Вы можете вернуть функцию с логикой очистки из функции внутри useEffect. Давайте рассмотрим пример поля ввода, которое должно очищаться, когда пользователь нажимает клавишу Escape. Мы можем сделать это следующим образом:

function Input() {
  const [input, setInput] = useState("");

  const escFunction = useCallback((event) => {
    if (event.key === "Escape") {
      setInput("");
    }
  }, []);

  useEffect(() => {
    document.addEventListener("keydown", escFunction, false);

    return () => {
      document.removeEventListener("keydown", escFunction, false);
    };
  }, [escFunction]);

  return (
    <>
      <input value={input} onChange={(event) => setInput(event.target.value)} />
    </>
  );
}

В приведенном выше коде мы возвращаем функцию из useEffect. Эта функция отменяет подписку на прослушиватель событий keydown, избавляя нас от неприятных проблем с утечкой памяти.

5. Используйте несколько эффектов для разделения задач

Вы когда-нибудь слышали о "разделяй и властвуй"? Ага! Это именно то, что нам нужно сделать здесь. Также известный как разделение ответственности, широко практикуемый принцип в программная инженерия. Команда React выступает за разделение задач в useEffect, то есть использование разных useEffect для каждого варианта использования. Предположим, у нас есть компонент, который получает userId и organizationId в свойствах. Мы делаем вызов API, чтобы получить данные о пользователе и организации на основе реквизита. Мы можем написать useEffect следующим образом:

useEffect(() => {
  getUser(userId);
  getOrganization(organizationId);
}, [userId, organizationId]);

Приведенный выше useEffect будет работать нормально. Но есть одна принципиальная проблема. Если userId изменяется, а organizationId нет, getOrganization все равно будет вызываться. Почему? Потому что мы добавили две отдельные задачи в один useEffect. Нам нужно разделить их на два разных useEffect. Мы можем переписать это так:

useEffect(() => {
  getUser(userId);
}, [userId]);

useEffect(() => {
  getOrganization(organizationId);
}, [organizationId]);

6. Пользовательские крючки

Пользовательские хуки — отличный способ извлечения побочных эффектов, которые можно повторно использовать во многих других компонентах. Это также элегантный способ организации кода. Предположим, у нас есть специальный хук для возврата сведений о входной строке. Мы можем написать этот пользовательский хук следующим образом:

function useStringDescription(inputValue) {
  if (!inputValue) {
    return defaultValue;
  } else {
    const wordCount = inputValue.trim().split(/s+/).length;
    const noOfVowels = inputValue.match(/[aeiou]/gi)?.length || 0;
    return {
      wordCount,
      noOfVowels,
    };
  }
}

Хук useStringDescription возвращает количество гласных и количество слов в строке, переданной в аргументе. Мы можем использовать этот хук следующим образом:

function App() {
  const [inputValue, setInputValue] = useState("");
  const stringDescription = useStringDescription(inputValue);
  const { wordCount, noOfVowels } = stringDescription;

  useEffect(() => {
    console.log("stringDescription changed", stringDescription);
  }, [stringDescription]);

  return (
    ...
  );
}

Давайте попробуем использовать приведенную выше настройку и посмотрим, сколько раз выполняется useEffect.

Вы можете видеть, что useEffect вызывается, даже если значение объекта stringDescription не меняется. Мы можем деструктурировать объект stringDescription, как определено в 1-м правиле этой статьи, или мы можем оптимизировать наш пользовательский хук следующим образом:

function useStringDescription(inputValue) {
  const [stringDescription, setStringDescription] = useState(defaultValue);

  useEffect(() => {
    if (!inputValue) {
      setStringDescription(defaultValue);
    } else {
      const wordCount = inputValue.trim().split(/s+/).length;
      const noOfVowels = inputValue.match(/[aeiou]/gi)?.length || 0;

      if (
        wordCount !== stringDescription.wordCount ||
        noOfVowels !== stringDescription.noOfVowels
      ) {
        setStringDescription({
          wordCount,
          noOfVowels,
        });
      }
    }
  }, [inputValue, stringDescription.noOfVowels, stringDescription.wordCount]);

  return stringDescription;
}

Теперь мы обновляем состояние при изменении любого значения. Посмотрим на результаты:

Видеть? useEffect не вызывался напрасно, потому что выполняется ссылочное равенство.

Это был набор оптимизаций useEffect, которые мы можем использовать, чтобы сделать использование хуков более эффективным и избавиться от неприятных и общеизвестно трудно обнаруживаемых ошибок, вызванных неправильным использованием хуков. Оптимизация useEffect в React также повышает производительность. Каковы ваши секретные рецепты useEffect? Дайте мне знать в Twitter. И не забудьте прочитать мой предыдущий блог о ускорении обещаний.


Также опубликовано здесь


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE