Источник: WebForMyself.com
Обычно в React есть несколько способов написать код. И хотя можно создать одно и то же по-разному, могут быть один или два подхода, которые технически работают «лучше», чем другие. Я на самом деле сталкиваюсь со множеством примеров, когда код, используемый для создания компонента React, технически «правильный», но вызывает проблемы, которых можно было бы избежать.
Итак, давайте рассмотрим некоторые из этих примеров. Я собираюсь предоставить три экземпляра «ошибочного» кода React, который технически выполняет свою работу в конкретной ситуации, и способы его улучшения, чтобы сделать его более удобным в обслуживании, отказоустойчивым и, в конечном итоге, функциональным.
Код ошибки # 1: Мутация состояния и свойств
Это большой анти-шаблон для изменения состояния или свойств в React. Не делайте этого!
Это не революционный совет — обычно это одна из первых вещей, которую вы усваиваете, когда начинаете работать с React. Но вы можете подумать, что можете справится с этим.
Я собираюсь показать вам, как ошибки могут закрасться в ваш код, если вы изменяете свойства. Иногда вам понадобится компонент, который будет показывать преобразованную версию некоторых данных. Давайте создадим родительский компонент, который содержит счетчик и кнопку, которая будет увеличивать его. Мы также создадим дочерний компонент, который получает счетчик через свойство и показывает, значение счетчика с добавленным к нему 5.
Вот пример, демонстрирующий наивный подход. Этот пример работает. Он делает то, что мы хотим: мы нажимаем кнопку увеличения, и он добавляет единицу к счетчику. Затем дочерний компонент повторно визуализируется, чтобы показать значение счетчика с добавленным к нему 5. Мы поменяли свойство у потомка, и он отлично работает! Почему все говорят нам, что мутировать реквизит — это так плохо?
А что, если позже мы проведем рефакторинг кода и нам потребуется сохранить счетчик в объекте? Это может произойти, если нам нужно хранить больше свойств в одном и том же хуке useState по мере роста нашей кодовой базы.
Вместо увеличения числа, хранящегося в состоянии, мы увеличиваем свойство count. В нашем дочернем компоненте мы получаем объект через свойство и добавляем к count, чтобы показать, как будет выглядеть счетчик, если мы добавим 5.
Посмотрим, как это пойдет. Попробуйте увеличить состояние несколько раз.
О, нет! Теперь, когда мы увеличиваем счетчик, кажется, что на каждый клик добавляется 6! Почему это происходит? Единственное, что изменилось между этими двумя примерами, — это то, что мы использовали объект вместо числа!
Более опытные программисты на JavaScript знают, что большая разница здесь в том, что примитивные типы, такие как числа, логические значения и строки, неизменяемы и передаются по значению, тогда как объекты передаются по ссылке. Это значит, что:
- Если вы поместите число в переменную, назначите ему другую переменную, а затем измените вторую переменную, первая переменная не изменится.
- Если вы поместите объект в переменную, назначите ему другую переменную, а затем измените вторую переменную, первая переменная будет изменена.
Когда дочерний компонент изменяет свойство объекта, он добавляет 5 к тому же объекту, который React использует при обновлении состояния. Это означает, что когда наша функция увеличения срабатывает после клика, React использует тот же объект после того, как он был обработан нашим дочерним компонентом, что отображается как добавление 6 при каждом клике.
Решение
Есть несколько способов избежать этих проблем. В такой простой ситуации вы можете избежать любых мутаций и выразить изменение в функции рендеринга:
Однако в более сложном случае вам может потребоваться многократное использование state.count + 5 или передача преобразованных данных нескольким дочерним элементам.
Один из способов сделать это — создать копию свойства в дочернем элементе, а затем преобразовать свойства клонированных данных. Есть несколько разных способов клонирования объектов в JavaScript с различными компромиссами. Вы можете использовать литерал объекта:
Но если есть вложенные объекты, они все равно будут ссылаться на старую версию. Вместо этого вы можете преобразовать объект в JSON, а затем сразу же его распарсить:
Это будет работать для большинства простых типов объектов. Но если в ваших данных используются более сложные типы, вы можете использовать библиотеки. Популярным методом было бы использование deepClone lodash. Вот пример, который показывает версию с использованием литерала объекта и spread синтаксиса.
Еще один вариант — использовать такую библиотеку, как Immutable.js. Если у вас есть ограничение использовать только неизменяемые структуры данных, вы можете быть уверены, что ваши данные не будут неожиданно изменены. Вот еще один пример использования неизменяемого класса Map для представления состояния приложения счетчика.
Код ошибки # 2: производное состояние
Допустим, у нас есть родительский и дочерний компоненты. У них обоих есть хуки useState со счетчиком. И предположим, что родительский элемент передает свое состояние в качестве свойства дочернему элементу, который тот использует для инициализации своего счетчика.
Что происходит с дочерним состоянием, когда состояние родительского элемента изменяется, и дочерний элемент повторно отображается с другими свойствами? Будет ли дочернее состояние оставаться прежним или изменится, чтобы отразить новый счетчик, который ему был передан?
Мы имеем дело с функцией, поэтому дочернее состояние должно быть удалено и заменено, верно? Неправильно! Состояние наследника проритетнее новое свойство от родителя. После того, как состояние дочернего компонента инициализировано при первом рендеринге, оно полностью не зависит от каких-либо свойств, которые он получает. Пример по ссылке.
React сохраняет состояние компонента для каждого компонента в дереве, и состояние удаляется только при удалении компонента.
Использование свойств для инициализации состояния называется «производным состоянием» и является своего рода анти-шаблоном. Это устраняет преимущество компонента, имеющего единственный источник достоверных данных.
Использование свойств key
Но что, если у нас есть коллекция элементов, которые мы хотим редактировать, используя тот же тип дочернего компонента, и мы хотим, чтобы дочерний элемент содержал черновик элемента, который мы редактируем? Нам нужно будет сбрасывать состояние дочернего компонента каждый раз, когда мы перебираем элементы из коллекции.
Вот пример: давайте напишем приложение, в котором мы можем ежедневно составлять список из пяти вещей, за которые мы благодарны каждый день. Мы будем использовать родительский элемент с состоянием, инициализированным как пустой массив, который мы собираемся заполнить пятью строковыми операторами. Затем у нас будет дочерний компонент с текстовым вводом для ввода нашего оператора.
Мы собираемся использовать криминальный уровень чрезмерной инженерии в нашем крошечном приложении, но это чтобы проиллюстрировать шаблон, который может вам понадобиться в более сложном проекте: мы собираемся сохранить черновое состояние ввода текста в дочернем компоненте.
Понижение состояния дочернего компонента может быть оптимизацией производительности, чтобы предотвратить повторный рендеринг родительского компонента при изменении состояния ввода. В противном случае родительский компонент будет повторно отображаться каждый раз при изменении ввода текста.
Мы также передадим пример состояния в качестве значения по умолчанию для каждой из пяти заметок, которые мы напишем. Вот ошибочный способ сделать это:
Тут есть проблема: каждый раз, когда мы отправляем состояние, input ошибочно содержит отправленную запись в текстовом поле. Мы хотим заменить его примером из нашего списка.
Несмотря на то, что мы каждый раз передаем другую строку примера, наследник запоминает старое состояние, а наше новое свойство игнорируется. Вы потенциально можете проверить, изменились ли свойства при каждом рендеринге в useEffect, а затем сбросить состояние, если они изменились. Но это может вызвать ошибки, когда разные части ваших данных используют одни и те же значения, и вы хотите принудительно сбросить дочернее состояние, даже если свойство остается прежним.
Решение
Если вам нужен компонент наследника, где родительский компонент должен иметь возможность по требованию сделать сброс состояния наследника, то есть способ сделать это: это, изменяя свойство key наследника.
Возможно, вы видели особое свойство key, когда рендерили элементы на основе массива, и React выдает предупреждение с просьбой предоставить ключ для каждого элемента. Изменение ключа дочернего элемента гарантирует, что React создаст совершенно новую версию элемента. Это способ сообщить React, что вы визуализируете концептуально другой элемент, используя один и тот же компонент.
Давайте добавим ключевое свойство нашему дочернему компоненту. Значение — это индекс, который мы собираемся заполнить нашим состоянием:
Вот как это выглядит в нашем приложении со списком.
Обратите внимание, что единственное, что здесь изменилось, это то, что у дочернего компонента теперь есть свойство key, основанное на индексе массива, который мы собираемся заполнить. Тем не менее, поведение компонента полностью изменилось.
Теперь каждый раз, когда мы отправляем и завершаем запись состояния, старое состояние в дочернем компоненте отбрасывается и заменяется состоянием из примера.
Код ошибки # 3: устаревшие ошибки замыкания
Это обычная проблема с хуками React. Давайте рассмотрим несколько ситуаций, в которых вы можете столкнуться с проблемами. Первый всплывает при использовании useEffect. Если мы делаем что-то внутри useEffect асинхронно, у нас могут возникнуть проблемы с использованием старого состояния или свойств.
Вот пример. Нам нужно увеличивать счет каждую секунду. Мы устанавливаем его при первом рендеринге с помощью useEffect, обеспечивая закрытие, которое увеличивает счетчик в качестве первого аргумента и пустой массив в качестве второго аргумента. Мы дадим ему пустой массив, так как мы не хотим, чтобы React перезапускал интервал при каждом рендеринге.
О, нет! Счетчик увеличивается до 1, но после этого больше не изменяется! Почему это происходит? Это связано с двумя вещами:
- поведение замыканий в JavaScript
- второй аргумент useEffect
Взглянув на документацию MDN, мы можем видеть:
Замыкание — это комбинация функции и лексического окружения, в котором эта функция была объявлена. Это окружене состоит из любых локальных переменных, которые были в области видимости во время создания замыкания.
«Лексическая среда», в которой объявляется наше замыкание useEffect, находится внутри компонента Counter. Интересующая нас локальная переменная count равна нулю на момент объявления (первого рендеринга).
Проблема в том, что это замыкание больше никогда не объявляется. Если при объявлении времени счетчик равен нулю, он всегда будет равен нулю. Каждый раз, когда срабатывает интервал, он запускает функцию, которая начинается с нуля и увеличивает его до 1.
Итак, как мы можем снова объявить функцию? Здесь появляется второй аргумент вызова useEffect. Мы думали, что были очень умны, начав интервал только один раз, используя пустой массив, но при этом мы выстрелили себе в ногу. Если бы мы не использовали этот аргумент, замыкание внутри useEffect объявлялось бы снова с новым счетчиком каждый раз.
Массив зависимостей useEffect выполняет две функции:
- Он запустит функцию useEffect при изменении зависимости.
- Он также повторно объявит замыкание с обновленной зависимостью, сохраняя замыкание безопасным от устаревшего состояния или свойств.
Фактически, существует даже правило lint, чтобы защитить ваши экземпляры useEffect от устаревшего состояния и свойств, убедившись, что вы добавляете правильные зависимости ко второму аргументу.
Но на самом деле мы не хотим сбрасывать интервал каждый раз при рендеринге компонента. Как же тогда решить эту проблему?
Решение
Опять же, здесь есть несколько решений нашей проблемы. Начнем с самого простого: вообще не использовать состояние count, а вместо этого передать функцию в наш вызов setState:
Это было просто. Другой вариант — использовать такой хук useRef, чтобы сохранить изменяемую ссылку счетчика:
Другие устаревшие ошибки замыкания
Но устаревшие замыкания на просто появятся в useEffect. Они также могут появляться в обработчиках событий и других замыканиях внутри ваших компонентов React. Давайте посмотрим на компонент React с устаревшим обработчиком событий; мы создадим полосу прокрутки, которая выполняет следующие действия:
- увеличивает ширину экрана по мере того, как пользователь ее прокручивает
- начинается с прозрачного и становится все более и более непрозрачной по мере того, как пользователь прокручивает
- предоставляет пользователю кнопку, которая рандомизирует цвет полосы прокрутки
Мы собираемся оставить индикатор выполнения за пределами дерева React и обновить его в обработчике событий. Вот наша реализация с ошибками:
Наша полоса становится шире и непрозрачнее по мере прокрутки страницы. Но если вы нажмете кнопку изменения цвета, наши рандомизированные цвета не повлияют на индикатор выполнения. Мы получаем эту ошибку, потому что на замыкание влияет состояние компонента, и это замыкание никогда не объявляется повторно, поэтому мы получаем только исходное значение состояния и никаких обновлений.
Вы можете увидеть, как настройка замыканий, которые вызывают внешние API-интерфейсы с использованием состояния React или свойства компонентов могут доставить вам неприятности, если вы не будете осторожны.
Решение
Опять же, есть несколько способов решить эту проблему. Мы могли бы сохранить состояние цвета в изменяемой ссылке, которую позже могли бы использовать в нашем обработчике событий:
Это работает достаточно хорошо, но не кажется идеальным. Вам может потребоваться написать подобный код, если вы имеете дело со сторонними библиотеками и не можете найти способ перенести их API в свое дерево React. Но, убирая один из наших элементов из дерева React и обновляя его внутри нашего обработчика событий, мы плывем против течения.
Это простое исправление, поскольку мы имеем дело только с DOM API. Простой способ рефакторинга — включить индикатор выполнения в наше дерево React и отобразить его в JSX, чтобы он мог ссылаться на состояние компонента. Теперь мы можем использовать функцию обработки событий исключительно для обновления состояния.
Так лучше. Мы не только устранили возможность устаревания нашего обработчика событий, но и превратили индикатор выполнения в автономный компонент, который использует декларативную природу React.
Подведение итогов
Мы рассмотрели три различных способа создания ошибок в приложениях React и некоторые способы их исправления. Можно легко взглянуть на примеры счетчиков, которые идут по правильному пути и не показывают тонкостей API, которые могут вызвать проблемы.
Источник: css-tricks.com