Мифы о useEffect

от автора

Доброго времени суток, друзья!

Представляю вашему вниманию перевод небольшой заметки Kent C. Dodds, в которой он делится своими соображениями относительно правильного использования хука «useEffect».

Я обучил React тысячи разработчиков. Как до, так и после релиза
хуков. Одной из вещей, которые я заметил, является не очень четкое понимание
назначения и механизма работы хука «useEffect». В этой статье я хочу немного об этом рассказать.

Никакого отношения к стадиям жизненного цикла — синхронизация дополнительных эффектов

Разработчики, которые имеют опыт работы с «классовыми» компонентами и стадиями
жизненного цикла, такими как «constructor», «componentDidMount»,
«componentDidUpdate» и «componentWillUnmount», иногда пытаются реализовать
аналогичное поведение в функциональных компонентах с помощью хуков. Это большая
ошибка. Позвольте мне это продемонстрировать. Вот пример забавного
пользовательского интерфейса:

Вот реализация компонента «DogInfo» с помощью классов:

  class DogInfo extends React.Component {     controller = null;     state = { dog: null };      fetchDog() {       this.controller?.abort();        this.controller = new AbortController();       getDog(this.props.dogId, { signal: this.controller.signal }).then(         (dog) => {           this.setState({ dog });         },         (error) => {           // обработка ошибок         }       );     }      componentDidMount() {       this.fetchDog();     }      componentDidUpdate(prevProps) {       // обработка изменения dogId       if (prevProps.dogId !== this.props.dogId) {         this.fetchDog();       }     }      componentWillUnmount() {       // отмена запроса       this.controller?.abort();     }      render() {       return <div>{/* рендеринг информации о собаке */}</div>;     }   } 

Это стандартный компонент для такого вида интеракции. В нем
используются стадии жизненного цикла «constructor», «componentDidMount»,
«componentDidUpdate» и «componentWillUnmount». Вот что получится, если мы
обернем эти стадии в хуки:

  function DogInfo({ dogId }) {     const controllerRef = React.useRef(null);     const [dog, setDog] = React.useState(null);      function fetchDog() {       controllerRef.current?.abort();        controllerRef.current = new AbortController();        getDog(dogId, { signal: controllerRef.current.signal }).then(         (d) => setDog(d),         (er) => {           // обработка ошибок         }       );     }      // didMount     React.useEffect(() => {       fetchDog();       // eslint-disable-next-line react-hooks/exhaustive-deps     }, []);      // didUpdate     const prevDogId = usePrevious(dogId);     useUpdate(() => {       if (prevDogId !== dogId) {         fetchDog();       }     });      // willUnmount     React.useEffect(() => {       return () => {         controllerRef.current?.abort();       };     }, []);      return <div>{/* рендеринг информации о собаке */}</div>;   }    function usePrevious(value) {     const ref = useRef();     useEffect(() => {       ref.current = value;     }, [value]);     return ref.current;   } 

Здесь имеется некоторая несогласованность между хуками. Если бы они
предназначались для использования таким образом, я тоже был бы их противником.
Но правда в том, что useEffect — это не стадия жизненного цикла. Это механизм
синхронизации дополнительных эффектов с состоянием приложения. В
приведенном примере наша задача состоит в том, чтобы запрашивать информацию о
собаке при изменении dogId. Учитывая это, useEffect становится намного проще:

  function DogInfo({ dogId }) {     const [dog, setDog] = useState(null);      useEffect(() => {       const controller = new AbortController();       getDog(dogId, { signal: controller.signal }).then(         (d) => setDog(d),         (er) => {           // обработка ошибок         }       );       return () => controller.abort();     }, [dogId]);      return <div>{/* рендеринг информации о собаке */}</div>;   } 

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

Запомните высказывание Ryan Florence:
«Вопрос не в том, когда запускать эффект, вопрос в том, с каким состоянием
он должен синхронизироваться»

useEffect(fn) // все состояния
useEffect(fn, []) // отсутствие состояний
useEffect(fn, [these, states]) // указанные состояния

Могу я игнорировать eslint-plugin-react-hooks/exhaustive-deps?

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

Один большой useEffect

Честно говоря, я давно такого не видел. Но такое порой все-таки случается. То,
что мне действительно нравится в useEffect, так это возможность разделения задачи на любое количество подзадач. Вот простой пример:

Вот некоторый всевдокод для этого демо:

  class ChatFeed extends React.Component {     componentDidMount() {       this.subscribeToFeed();       this.setDocumentTitle();       this.subscribeToOnlineStatus();       this.subscribeToGeoLocation();     }     componentWillUnmount() {       this.unsubscribeFromFeed();       this.restoreDocumentTitle();       this.unsubscribeFromOnlineStatus();       this.unsubscribeFromGeoLocation();     }     componentDidUpdate(prevProps, prevState) {       // ... сранение пропсов, повторной подписки и т.д.     }     render() {       return <div>{/* интерфейс чата */}</div>;     }   } 

Видите эти 4 задачи? Они смешаны. Что, если вы захотите поделиться частью функционала? Я имею ввиду, что пропсы рендеринга — отличная вещь, но хуки
все же лучше. Я видел, как некоторые люди создают огромный useEffect, отвечающий
за все:

  function ChatFeed() {     React.useEffect(() => {       // подписка на ленту       // установка заголовка документа       // перевод статуса в онлайн       // определения местоположения       return () => {         // отписка от ленты         // восстановление заголовка         // перевод статуса в офлайн         // отключение определения местоположения       };     });     return <div>{/* интерфейс чата */}</div>;   } 

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

  function ChatFeed() {     React.useEffect(() => {       // подписка на ленту       return () => {         // отписка от ленты       };     });     React.useEffect(() => {       // установка заголовка документа       return () => {         // восстановление заголовка       };     });     React.useEffect(() => {       // перевод статуса в онлайн       return () => {         // перевод статуса в офлайн       };     });     React.useEffect(() => {       // определения местоположения       return () => {         // отключение определения местоположения       };     });     return <div>{/* интерфейс чата */}</div>;   } 

Самодостаточность хуков дает массу преимуществ.

Внешние функции

Я видел такое несколько раз. Позвольте мне просто привести код до и после:

  // до. Не делайте так   function DogInfo({ dogId }) {     const [dog, setDog] = React.useState(null);     const controllerRef = React.useRef(null);     const fetchDog = React.useCallback((dogId) => {       controllerRef.current?.abort();       controllerRef.current = new AbortController();       return getDog(dogId, { signal: controller.signal }).then(         (d) => setDog(d),         (error) => {           // обработка ошибок         }       );     }, []);     React.useEffect(() => {       fetchDog(dogId);       return () => controller.current?.abort();     }, [dogId, fetchDog]);     return <div>{/* рендеринг информации о собаках */}</div>;   } 

Мы уже видели, как можно упростить этот код, но давайте взглянем на него еще раз:

  function DogInfo({ dogId }) {     const [dog, setDog] = React.useState(null);     React.useEffect(() => {       const controller = new AbortController();       getDog(dogId, { signal: controller.signal }).then(         (d) => setDog(d),         (error) => {           // обработка ошибок         }       );       return () => controller.abort();     }, [dogId]);     return <div>{/* рендеринг информации о собаках */}</div>;   } 

Я пытаюсь сказать, что определение функции за пределами колбэка useEffect — это плохая идея. Поскольку такая функция является внешней по отношению к useEffect, нам приходится добавлять ее в список зависимостей. Также нам приходится кэшировать ее во избежание бесконечного цикла. Кроме того, мы вынуждены создавать ref для контроллера.

Запомните: при необходимости определения функции, вызываемой в эффекте, определяйте ее внутри колбэка данного эффекта, а не за его пределами.

Заключение

Когда Dan Abramov представил хуки, такие как «useEffect», он сравнил компоненты с атомами, а хуки с электронами. Хуки представляют собой низкоуровневые примитивы, и именно это делает их такими мощными. Красота этих примитивов заключается в абстрагировании стадий жизненного цикла, с которыми мы имели дело раньше. С момента релиза хуков мы наблюдаем взрыв инноваций и появление хороших идей и библиотек на основе этих примитивов, что помогает нам, как разработчикам, создавать более качественные приложения.

ссылка на оригинал статьи https://habr.com/ru/post/522268/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *