React Hooks
, в отличие от Class Components
, предоставляют низкоуровневые строительные блоки для оптимизации и составления приложений с минимальным набором шаблонов.
Без глубоких знаний могут возникнуть проблемы с производительностью, а сложность кода может увеличиться из-за мелких ошибок и дырявых абстракций.
Я создал тематическое исследование из 12 частей, чтобы продемонстрировать распространенные проблемы и способы их решения. Я также собрал React Hooks Radar
и React Hooks Checklist
для небольших рекомендаций и быстрой справки.
Пример использования: внедрение интервала
Цель состоит в том, чтобы реализовать счетчик, который начинается с 0
и увеличивается каждые 500ms
. Должны быть предусмотрены три кнопки управления: start
, stop
и clear
.
Уровень 0: Hello World
export default function Level00() { console.log('renderLevel00'); const [count, setCount] = useState(0); return ( <div> count => {count} <button onClick={() => setCount(count + 1)}>+</button> <button onClick={() => setCount(count - 1)}>-</button> </div> ); }
Это простой, правильно реализованный счетчик, который увеличивается или уменьшается при нажатии пользователем.
Уровень 1: setInterval
export default function Level01() { console.log('renderLevel01'); const [count, setCount] = useState(0); setInterval(() => { setCount(count + 1); }, 500); return <div>count => {count}</div>; }
Этот код предназначен для увеличения счетчика каждые 500ms
. Этот код имеет огромную утечку ресурсов и реализован неправильно. Это легко приведет к сбою вкладки браузера. Поскольку функция Level01
вызывается каждый раз, когда происходит рендеринг, этот компонент создает новый интервал при каждом запуске рендеринга.
Мутации, подписки, таймеры, ведение журнала и другие побочные эффекты не допускаются внутри основного тела функционального компонента (называемого фазой рендеринга React). Это приведет к сбивающим с толку ошибкам и несоответствиям в пользовательском интерфейсе.
🔗 Справочник по API хуков: useEffect
Уровень 2: useEffect
export default function Level02() { console.log('renderLevel02'); const [count, setCount] = useState(0); useEffect(() => { setInterval(() => { setCount(count + 1); }, 500); }); return <div>Level 2: count => {count}</div>; }
Большинство побочных эффектов происходит внутри useEffect
. Этот код также имеет огромную утечку ресурсов и реализован неправильно. По умолчанию useEffect
запускается после каждого рендеринга, поэтому новый интервал будет создаваться при каждом изменении счетчика.
🔗 Справочник по API хуков: useEffect, Сроки эффектов.
Уровень 3: беги только один раз
export default function Level03() { console.log('renderLevel03'); const [count, setCount] = useState(0); useEffect(() => { setInterval(() => { setCount(count + 1); }, 300); }, []); return <div>count => {count}</div>; }
Указание []
в качестве второго аргумента для useEffect
вызовет функцию один раз после mount
. Несмотря на то, что setInterval
вызывается только один раз, этот код реализован неправильно.
count
увеличится с 0
до 1
и останется прежним . Стрелочная функция будет создана один раз, и когда это произойдет, count
будет 0
.
В этом коде есть незначительная утечка ресурсов. Даже после размонтирования компонента setCount
все равно будет вызываться.
🔗 Справочник по API хуков: useEffect, Условное срабатывание эффекта.
Уровень 4: очистка
useEffect(() => { const interval = setInterval(() => { setCount(count + 1); }, 300); return () => clearInterval(interval); }, []);
Чтобы предотвратить утечку ресурсов, все должно быть удалено по окончании жизненного цикла обработчика. В этом случае возвращаемая функция будет вызвана после размонтирования компонента.
Этот код не имеет утечек ресурсов, но, как и предыдущий, реализован неправильно.
🔗 Справочник по API хуков: Очистка эффекта.
Уровень 5: используйте count
как зависимость
useEffect(() => { const interval = setInterval(() => { setCount(count + 1); }, 500); return () => clearInterval(interval); }, [count]);
Если присвоить массиву dependencies
useEffect
его жизненный цикл изменится. В этом примере useEffect
будет вызываться один раз после mount
и каждый раз при изменении count
. Функция очистки будет вызываться каждый раз, когда count
изменяется для удаления предыдущего ресурса.
Этот код работает правильно, без ошибок, но слегка вводит в заблуждение. setInterval
создается и удаляется каждые 500ms
. Каждый setInterval
всегда вызывается один раз.
🔗 Справочник по API хуков: useEffect, Условное срабатывание эффекта.
Уровень 6: setTimeout
useEffect(() => { const timeout = setTimeout(() => { setCount(count + 1); }, 500); return () => clearTimeout(timeout); }, [count]);
Этот код и приведенный выше код работают правильно. Поскольку useEffect
вызывается каждый раз при изменении count
, использование setTimeout
имеет тот же эффект, что и вызов setInterval
.
Этот пример неэффективен, новый setTimeout
создается каждый раз, когда происходит рендеринг. React
предлагает лучший способ решения проблемы.
Уровень 7: функциональные обновления для useState
useEffect(() => { const interval = setInterval(() => { setCount(c => c + 1); }, 500); return () => clearInterval(interval); }, []);
В предыдущем примере мы запускали useEffect
при каждом count
изменении. Это было необходимо, потому что нам нужно было всегда иметь актуальное текущее значение.
useState
предоставляет API для обновления предыдущего состояния без сохранения текущего значения. Для этого все, что нам нужно сделать, это указать лямбду для setState
.
Этот код работает правильно и более эффективно. Мы используем один setInterval
в течение жизненного цикла компонента. clearInterval
будет вызываться только один раз после размонтирования компонента.
🔗 Справочник по API хуков: useState, Функциональные обновления.
Уровень 8: локальная переменная
export default function Level08() { console.log('renderLevel08'); const [count, setCount] = useState(0); let interval = null; const start = () => { interval = setInterval(() => { setCount(c => c + 1); }, 500); }; const stop = () => { clearInterval(interval); }; return ( <div> count => {count} <button onClick={start}>start</button> <button onClick={stop}>stop</button> </div> ); }
Мы добавили кнопки start
и stop
. Этот код реализован неправильно, кнопка stop
не работает. Новая ссылка создается во время каждого render
, поэтому stop
будет иметь ссылку на null
.
🔗 Справочник по API хуков: Есть ли что-нибудь вроде переменных экземпляра?
Уровень 9: useRef
export default function Level09() { console.log('renderLevel09'); const [count, setCount] = useState(0); const intervalRef = useRef(null); const start = () => { intervalRef.current = setInterval(() => { setCount(c => c + 1); }, 500); }; const stop = () => { clearInterval(intervalRef.current); }; return ( <div> count => {count} <button onClick={start}>start</button> <button onClick={stop}>stop</button> </div> ); }
useRef
- лучший вариант, если требуется изменяемая переменная. В отличие от локальных переменных, React
гарантирует, что во время каждого рендеринга будет возвращаться одна и та же ссылка.
Этот код кажется правильным, но содержит небольшую ошибку. Если start
вызывается несколько раз, setInterval
будет вызываться несколько раз, вызывая утечку ресурсов.
🔗 Справочник по API хуков: useRef
Уровень 10: useCallback
export default function Level10() { console.log('renderLevel10'); const [count, setCount] = useState(0); const intervalRef = useRef(null); const start = () => { if (intervalRef.current !== null) { return; } intervalRef.current = setInterval(() => { setCount(c => c + 1); }, 500); }; const stop = () => { if (intervalRef.current === null) { return; } clearInterval(intervalRef.current); intervalRef.current = null; }; return ( <div> count => {count} <button onClick={start}>start</button> <button onClick={stop}>stop</button> </div> ); }
Чтобы избежать утечки ресурсов, мы просто игнорируем вызовы, если interval
уже запущен. Хотя вызов clearInterval(null)
не вызывает никаких ошибок, рекомендуется удалять ресурс только один раз.
В этом коде отсутствуют утечки ресурсов, он реализован правильно, но может иметь проблемы с производительностью.
memoization
- основной инструмент оптимизации производительности в React
. React.memo
выполняет поверхностное сравнение, и если ссылки совпадают, рендеринг пропускается.
Если start
и stop
были переданы компоненту memoized
, вся оптимизация не удалась бы, потому что новая ссылка возвращается после каждого рендеринга.
Уровень 11: useCallback
export default function Level11() { console.log('renderLevel11'); const [count, setCount] = useState(0); const intervalRef = useRef(null); const start = useCallback(() => { if (intervalRef.current !== null) { return; } intervalRef.current = setInterval(() => { setCount(c => c + 1); }, 500); }, []); const stop = useCallback(() => { if (intervalRef.current === null) { return; } clearInterval(intervalRef.current); intervalRef.current = null; }, []); return ( <div> count => {count} <button onClick={start}>start</button> <button onClick={stop}>stop</button> </div> ); }
Чтобы React.memo
выполняла свою работу должным образом, все, что нам нужно сделать, это сделать memoize
функции, используя useCallback
ловушку. Таким образом, одна и та же ссылка будет предоставляться после каждого рендеринга.
В этом коде нет утечек ресурсов, он реализован правильно, не имеет проблем с производительностью, но код довольно сложный, даже для простого счетчика.
🔗 Справочник по API хуков: useCallback
Уровень 12: кастомный крючок
function useCounter(initialValue, ms) { const [count, setCount] = useState(initialValue); const intervalRef = useRef(null); const start = useCallback(() => { if (intervalRef.current !== null) { return; } intervalRef.current = setInterval(() => { setCount(c => c + 1); }, ms); }, []); const stop = useCallback(() => { if (intervalRef.current === null) { return; } clearInterval(intervalRef.current); intervalRef.current = null; }, []); const reset = useCallback(() => { setCount(0); }, []); return { count, start, stop, reset }; }
Чтобы упростить код, нам нужно инкапсулировать всю сложность внутри useCounter
настраиваемого хука и предоставить чистый api: { count, start, stop, reset }
.
export default function Level12() { console.log('renderLevel12'); const { count, start, stop, reset } = useCounter(0, 500); return ( <div> count => {count} <button onClick={start}>start</button> <button onClick={stop}>stop</button> <button onClick={reset}>reset</button> </div> ); }
🔗 Справочник по API хуков: Использование пользовательского хука
React Hooks Radar
Все React Hooks
равны, но некоторые крючки более равны, чем другие.
✅ зеленый
Зеленые ловушки - это основные строительные блоки современных React
приложений. Их можно безопасно использовать практически везде, не задумываясь.
🌕 желтый
Желтые хуки обеспечивают полезную оптимизацию производительности с помощью мемоизации. К управлению жизненным циклом и входными данными следует подходить с осторожностью.
🔴 красный
Красные крючки взаимодействуют с изменчивым миром, используя побочные эффекты. Они являются наиболее мощными и должны использоваться с особой осторожностью. Таможенные крючки рекомендуются для всех нетривиальных случаев использования.
Контрольный список React Hooks
- Соблюдайте Правила крючков.
- Не выполняйте никаких побочных эффектов в основной функции рендеринга.
- Отписаться / утилизировать / уничтожить все используемые ресурсы.
- Предпочитайте
useReducer
или функциональные обновления дляuseState
, чтобы предотвратить чтение и запись одного и того же значения в ловушку. - Не используйте изменяемые переменные внутри функции рендеринга, вместо этого используйте
useRef
. - Если то, что вы сохраняете в
useRef
, имеет меньший жизненный цикл, чем сам компонент, не забудьте сбросить значение при утилизации ресурса. - Будьте осторожны с бесконечной рекурсией и нехваткой ресурсов.
- Запомните функции и объекты, когда это необходимо для повышения производительности.
- Правильно фиксируйте входные зависимости (
undefined
= ›каждый рендер,[a, b]
=› при измененииa
илиb
,[]
= ›только один раз). - Используйте таможенные хуки для нетривиальных случаев использования.