Недавно некоторые студенты в команде вызывали некоторые баги из-за написания хуков реакции, и даже один случай былонлайн вопросы. Из-за этого внутри команды возникли споры: хотите ли вы писать хуки? Вы хотите добавить линт? Хотите добавить автозапуск? Итог дискуссии таков:
- писать или писать;
- прежде чем писать хукидолженСначала выучить крючки;
- Команда выдает еще один обязательный к прочтению документ, и каждого учащегося нужно попросить сначала прочитать, а затем написать.
Отсюда и эта статья.
В этой статье основное внимание уделяется двум моментам:
- Жесткие требования перед написанием хуков;
- Есть несколько общих моментов, которые следует учитывать при написании хуков.
жесткие требования
1. Вы должны прочитать официальную документацию React Hooks один раз
Английский документ:реагировать JS.org/docs/hooks-…
Китайский документ:this-function.react JS.org/docs/hooks-…
Среди них ключевые обязательные к просмотру хуки: useState, useReducer, useEffect, useCallback, useMemo.
Также рекомендуется к прочтению:
- ДэнаПолное руководство по использованию эффектов
- Яньлян "Полное руководство для начинающих по React Hooks》
2. В проекте необходимо внедрить плагин lint и включить соответствующие правила
плагин линта:Ууууу, эта лошадь плюс .com/package/evil force…
Обязательные правила:
{
"plugins": ["react-hooks"],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}
в,react-hooks/exhaustive-deps
хотя бы предупредить, также может быть ошибкой. Рекомендуется непосредственно назначать «ошибку» к новым проектам и «предупреждать» в исторические проекты.
Помните, что этот раздел является тяжелым состоянием.
Если в вашем проекте в настоящее время не включено правило lint для хуков, пожалуйста, не пишите код хуков. Если вы обнаружите интерфейсный проект другой стороны, когда вы кодируете CR, соответствующие правила не были открыты, а код хуков был отправлен, пожалуйста, не объединяйтесь. Это требование относится к любому интерфейсному проекту React.
Эти два правила не дадут нам наступить на яму. Хотя для новичков в хуках этот процесс может оказаться «болезненным». но,Если вы чувствуете, что эти два правила вызывают у вас затруднения при написании кода, значит, вы не полностью усвоили зацепки.
Если вам действительно не нужны «исчерпывающие отсылки» для некоторых сценариев, вы можете добавить:// eslint-disable-next-line react-hooks/exhaustive-deps
Помните, что здесь можно забанить только код, нельзя лениться и забанить весь файл.
3. Если есть предупреждение, вызванное линтом, связанным с хуками, не выполнять глобальное автоматическое исправление.
За исключением хуков, обычный lint принципиально не меняет логику кода, а лишь корректирует спецификацию написания. Но правила ворса хуков разные,exhaustive-deps
Изменение кода приведет к изменению логики кода, что, скорее всего, вызовет онлайн-проблемы, поэтому в связи с исчезновением хуков не выполняйте глобальную операцию автоисправления. Если не гарантируется, что каждая логика была полностью возвращена.
Кроме того, барышня в компании добавила: eslint-plugin-react-hooks Начиная с версии 2.4.0 отменен автофикс исчерпывающих депов. так,Пожалуйста, попробуйте обновить плагин lint проекта до последней версии, чтобы снизить риск ошибок..
Затем рекомендуется включить «автоисправление при сохранении» vscode. Независимо от того, в чем проблема в будущем, ошибки и предупреждения могут быть локализованы в максимально возможной степени на начальном этапе разработки, чтобы обеспечить самотестирование и тестирование кода, соответствующего правилам.
Общие примечания
проблема зависимости
Проблема зависимостей и замыканий должна быть включенаexhaustive-deps
основная причина. Наиболее распространенные ошибки:Событие связывается во время монтирования, и при последующих обновлениях состояния возникает ошибка.
Пример кода ошибки: (здесь для привязки по клику используется addEventListener, просто для удобства иллюстрации ситуации)
function ErrorDemo() {
const [count, setCount] = useState(0);
const dom = useRef(null);
useEffect(() => {
dom.current.addEventListener('click', () => setCount(count + 1));
}, []);
return <div ref={dom}>{count}</div>;
}
Первоначальная идея этого кода такова: каждый раз, когда пользователь нажимает на дом, счетчик увеличивается на 1. Идеальный эффект состоит в том, чтобы держать точку и продолжать добавлять. Но фактический эффект заключается в том, что его нельзя добавить после того, как {count} достигнет "1".
Давайте разберемся,useEffect(fn, [])
Представляет, что он будет срабатывать только при монтировании. То есть, когда выполняется первый рендер, fn выполняется один раз, событие клика привязывается, и клик срабатывает.setCount(count + 1)
. На первый взгляд, счет все тот же, и он обязательно будет все время добавляться.Конечно, реальность бьет по лицу.
В чем суть изменения состояния, запускающего рендеринг страницы? Суть в томui = fn(props, state, context)
. Изменения реквизитов, внутреннего состояния и контекста приведут к повторному выполнению функции рендеринга (здесь ErrorDemo), а затем возврату к новому представлению.
Теперь вот проблема,ErrorDemo
Эта функция выполняется несколько раз, первый раз внутри функцииcount
следовал несколько разcount
Будет ли это иметь значение? Думая об этом таким образом, это не должно иметь значения. Тогда почему вы знаете, что количество во второй раз равно 1, а не 0? в первый разsetCount
Это та же функция, что и следующая? Это включает в себя некоторые основные принципы ловушек, а также то, почему объявление ловушек должно быть объявлено в верхней части функции и не может быть объявлено в условном операторе. Здесь нечего сказать.
Вывод: каждый разcount
являются повторно объявленными переменными, указывающими на совершенно новые данные; каждый разsetCount
Несмотря на повторное объявление, он указывает на ту же ссылку.
Возвращаясь к теме, мы знаем, что каждый раз, когда мы визуализируем, внутренний счетчик на самом деле является совершенно новой переменной. Затем мы привязываем метод события клика, то есть:setCount(count + 1)
, счетчик здесь на самом деле относится к счетчику при первом рендеринге, поэтому он всегда равен 0, поэтому setCount всегда устанавливает счетчик равным 1.
Как решить эту проблему?
Прежде всего, необходимо выполнить предыдущие жесткие требования, добавить правила lint и включить автоисправление при сохранении. Тогда вы обнаружите, что на самом деле этоeffect
зависитcount
из. autofix поможет вам автоматически заполнить зависимости, код становится таким:
useEffect(() => {
dom.current.addEventListener('click', () => setCount(count + 1));
}, [count]);
Тогда это точно неправильно, это равносильно перепривязке события каждый раз, когда меняется счетчик. Итак, для привязки событий или подобных сценариев есть несколько идей, и я расставляю приоритеты в соответствии с моей обычной обработкой:
Идея 1: устранить зависимости
В этом сценарии это очень просто, мы в основном используемsetCount
Другое использованиеfunctional updates. Просто напишите это так:() => setCount(prevCount => ++prevCount)
, не беспокойтесь о том, что нового, что старого, какие закрытия, избавьте от беспокойства и неприятностей.
Идея 2: Перепривязать события
Так что, если наше событие должно потреблять это количество? Например:
dom.current.addEventListener('click', () => {
console.log(count);
setCount(prevCount => ++prevCount);
});
Нам не нужно зацикливаться на том, чтобы сделать это только один раз во время монтирования. Вы также можете каждый раз удалять событие перед повторным рендерингом и привязывать событие после рендеринга. Здесь используйте useEffectхарактеристика, можете посмотреть документацию сами:
useEffect(() => {
const $dom = dom.current;
const event = () => {
console.log(count);
setCount(prev => ++prev);
};
$dom.addEventListener('click', event);
return () => $dom.removeEventListener('click', event);
}, [count]);
**Идея 3: Если вы считаете, что это слишком дорого или сложно писать, вы также можете использоватьuseRef
**
это практичноuseRef
Это тоже очень хлопотно. Лично мне эта операция не нравится, но тоже может решить проблему. Код такой:
const [count, setCount] = useState(0);
const countRef = useRef(count);
useEffect(() => {
dom.current.addEventListener('click', () => {
console.log(countRef.current);
setCount(prevCount => {
const newCount = ++prevCount;
countRef.current = newCount;
return newCount;
});
});
}, []);
useCallback и useMemo
Эти два API-интерфейса на самом деле хорошо понятны концептуально, один из них — «кэш-функция», а другой — кеш «возвращаемого значения функции». Но мы часто слишком ленивы, чтобы использовать его, а иногда даже используем неправильно.
Из приведенной выше проблемы с зависимостями мы можем узнать, что хуки на самом деле очень чувствительны к точке «есть ли какие-либо изменения». Если данные или метод используются внутри эффекта. Если наши зависимости не добавляют его, легко сделать данные или метод не идеальными из-за проблемы замыкания. Если мы его добавим, то очень вероятно, что эффект будет дико выполняться из-за их изменения. В реальной разработке каждый должен часто сталкиваться с такого рода проблемами.
Поэтому здесь предлагается:
- Внутри компонента этиСтанет методом других зависимостей useEffect, рекомендуется использовать
useCallback
пакетили непосредственно в useEffect, который на него ссылается. - Делай другим, не навязывай другим,Если ваша функция будет передана дочерним компонентам в качестве реквизита, обязательно используйте
useCallback
пакет, для дочерних компонентов, если каждый рендер будет вызывать изменение функции, которую вы передаете, это может вызвать много проблем. В то же время это не способствует оптимизации рендеринга.
Однако есть еще один сценарий, который легко проигнорировать, и легко спутать useCallback с useMemo Типичный сценарий: дросселирование и защита от встряхивания.
Например:
function BadDemo() {
const [count, setCount] = useState(1);
const handleClick = debounce(() => {
setCount(c => ++c);
}, 1000);
return <div onClick={handleClick}>{count}</div>;
}
Мы хотим, чтобы пользователи не запускали несколько изменений, постоянно нажимая, добавляя защиту от сотрясения и запуская через 1 секунду после остановки клика.count + 1
Этот компонент в порядке под идеальной логикой. Но реальность худая, у нас много компонентов страницы, этоBadDemo
Он может быть перерендерен из-за операции родителя. Теперь, если наша страница перерисовывается каждые 500 миллисекунд, она выглядит так:
function BadDemo() {
const [count, setCount] = useState(1);
const [, setRerender] = useState(false);
const handleClick = debounce(() => {
setCount(c => ++c);
}, 1000);
useEffect(() => {
// 每500ms,组件重新render
window.setInterval(() => {
setRerender(r => !r);
}, 500);
}, []);
return <div onClick={handleClick}>{count}</div>;
}
Каждый раз, когда рендеринг заставляет handleClick быть другой функцией, этот анти-шейк, естественно, не работает. В такой ситуации существует больший онлайн-риск для некоторых сценариев с особенно высокими требованиями к защите ключей.
тогда что нам делать? Естественно хочу добавитьuseCallback
:
const handleClick = useCallback(debounce(() => {
setCount(c => ++c);
}, 1000), []);
Теперь мы обнаруживаем, что эффект соответствует нашим ожиданиям, но за ним все еще стоит огромная яма.
Предположим, у этой функции защиты от сотрясений есть какие-то зависимости? НапримерsetCount(c => ++c);
сталsetCount(count + 1)
. Тогда эта функция зависит отcount
. Код становится таким:
const handleClick = useCallback(
debounce(() => {
setCount(count + 1);
}, 1000),
[]
);
Вы обнаружите, что ваши правила lint не требуют использования count в качестве зависимости для заполнения массива deps. Это, в свою очередь, привело к исходной проблеме, когда учитывался только первый клик++. Почему это?
Потому что в useCallback передается оператор выполнения, а не объявление функции. Просто сказав, что он выполняет новую функцию, которая возвращается позже, мы передаем ее какuseCallback
Входные параметры функции и что это за новая функция, на самом деле, я не знаю правил lint.
Более разумной позицией должно быть использованиеuseMemo
:
const handleClick = useMemo(
() => debounce(() => {
setCount(count + 1);
}, 1000),
[count]
);
Это гарантирует, что всякий раз, когдаcount
Когда он изменится, он вернет новую функцию с добавленной функцией защиты от сотрясений.
Тем не менее, это все еще имеет проблемы.
Вопрос 1: useMemo "будущее" не является "стабильным"
реагироватьофициальная документацияупоминается в:
ты можешь поставить
useMemo
как средство оптимизации производительности, но не как семантическая гарантия**. **В будущем React может решить «забыть» некоторые предыдущие запомненные значения и пересчитать их при следующем рендеринге, например, высвобождая память для компонентов за кадром. Напишите сначала безuseMemo
код, который также может быть выполнен без — добавьте позже в свой кодuseMemo
, чтобы достичь цели оптимизации производительности.
То есть в будущем в особом случае эта функция защиты от сотрясений все равно не сработает. Конечно, эта ситуация возникает в «будущем» и является относительно экстремальной, а вероятность ее возникновения мала, даже если она и произойдет, то не будет происходить «постоянно в течение короткого промежутка времени». Следовательно, риск относительно невелик для сценариев, которые не предполагают, что «передняя часть не может быть стабилизирована без сотрясения».
вопрос 2: useMemo не решает все сценарии функций высшего порядка раз и навсегда
В примере сценария логика защиты от сотрясений выглядит следующим образом: «После 1 секунды непрерывных щелчков логика фактически выполняется, а повторные щелчки во время этого процесса недействительны». А если изменить бизнес-логику на «состояние меняется сразу после клика, а повторный клик в течение 1 секунды недействителен», то наш код может стать.
const handleClick = useMemo(
() => throttle(() => {
setCount(count + 1);
}, 1000),
[count]
);
А потом узнал, что опять не работает. Причина в том, что после клика счетчик сразу меняется, а потом handleClick повторно генерирует новую функцию, и это дросселирование становится недействительным.
Таким образом, в этом сценарии идея возвращается к вышеупомянутому «устранению зависимостей» или «использованию ссылок».. Конечно, вы также можете вручную реализовать его самостоятельно.debounce
илиthrottle
. яРекомендуется напрямую использовать библиотеки сообщества, такие какreact-use, либо обратитесь к их реализации и сами напишите две реализации.
Другие моменты внимания, я продолжу добавлять их позже или добро пожаловать в ответ~