если ты играешь часамиReact Hooks, вы можете столкнуться с неприятной проблемой: используяsetInterval
всегдаотклонятьсяэффект, который вы хотите.
Это Райан ФлоренсТочные слова:
Я встречал много людей, упоминающих, что хуки с setInterval часто бьют React по лицу, но это первый раз, когда я вижу проблемы с устаревшим состоянием. Если эта задача чрезвычайно сложна в хуках, то мы имеем другой уровень сложности по сравнению с классовыми компонентами.
Честно говоря, я думаю, что эти люди имеют набор или, по крайней мере, путаются в этом.
Однако я обнаружил, что это не проблема с хуками, аМодель программирования Reactа такжеsetInterval
вызвано несоответствием. Хуки ближе к модели программирования React, чем классы, что делает это несоответствие более очевидным.
В этой статье мы увидим, как интервалы и хуки работают вместе, почему это решение имеет смысл и какие новые функции оно может предоставить.
Отказ от ответственности: в центре внимания этого постаПример вопроса. Несмотря на то, что API могут упростить сотни ситуаций, обсуждение всегда указывает на более сложные проблемы..
Если вы новичок в Hooks и не знаете, что здесь происходит, сначала проверьте это.Это введениеа такжеДокументация. В этой статье предполагается, что вы используете хуки более часа.
покажи мне код напрямую
Без лишних слов, вот счетчик, который увеличивается каждую секунду:
import React, { useState, useEffect, useRef } from 'react';
function Counter() {
let [count, setCount] = useState(0);
useInterval(() => {
// 你自己的代码
setCount(count + 1);
}, 1000);
return <h1>{count}</h1>;
}
(ЭтоCodeSandbox demo).
в демоuseInterval
Не встроенный React Hook, а тот, который я написалcustom Hook.
import React, { useState, useEffect, useRef } from 'react';
function useInterval(callback, delay) {
const savedCallback = useRef();
// 保存新回调
useEffect(() => {
savedCallback.current = callback;
});
// 建立 interval
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
(Это то, что вы могли пропустить в предыдущей демонстрацииCodeSandbox demo. )
мойuseInterval
Хук имеет встроенный интервал и очищается при размонтировании, это функция в жизненном цикле компонентаsetInterval
а такжеclearInterval
Комбинация.
Не стесняйтесь копировать и вставлять его в свой проект или импортировать с помощью npm.
Если вам все равно, как это реализовано, можете не читать! Следующая часть предназначена для тех, кто хочет глубже изучить React Hooks..
Чего вы ждете?! 🤔
Я знаю, о чем ты думаешь:
Дэн, этот код вообще не интересен, что может быть "просто JavaScript"? Признайтесь, React поймал акулу с помощью Hooks!
Я сначала тоже так думал, но потом передумал, и хочу изменить и ваше. Прежде чем объяснять, почему этот код имеет смысл, я хочу показать, на что он способен.
ЗачемuseInterval()
лучший API
напомнить тебе, мойuseInterval
Хук принимает функцию и параметр задержки:
useInterval(() => {
// ...
}, 1000);
это очень похожеsetInterval
:
setInterval(() => {
// ...
}, 1000);
Так почему бы просто не использоватьsetInterval
Шерстяная ткань?
Сначала это может быть неочевидно, но ты нашел мойuseInterval
а такжеsetInterval
После разницы междуЕго параметры "динамически".
Я проиллюстрирую это конкретным примером.
Предположим, мы хотим, чтобы задержку можно было регулировать:
Хотя вам не обязательно использовать задержки управления вводом, динамические настройки могут быть полезны — например, для уменьшения интервала обновления опроса AJAX, когда пользователь переключается на другие вкладки.
Итак, в классе, как вы используетеsetInterval
сделать это? я бы сделал так:
class Counter extends React.Component {
state = {
count: 0,
delay: 1000,
};
componentDidMount() {
this.interval = setInterval(this.tick, this.state.delay);
}
componentDidUpdate(prevProps, prevState) {
if (prevState.delay !== this.state.delay) {
clearInterval(this.interval);
this.interval = setInterval(this.tick, this.state.delay);
}
}
componentWillUnmount() {
clearInterval(this.interval);
}
tick = () => {
this.setState({
count: this.state.count + 1
});
}
handleDelayChange = (e) => {
this.setState({ delay: Number(e.target.value) });
}
render() {
return (
<>
<h1>{this.state.count}</h1>
<input value={this.state.delay} onChange={this.handleDelayChange} />
</>
);
}
}
(ЭтоCodeSandbox demo. )
Это тоже не плохо!
Как выглядит версия Крюка?
🥁🥁🥁
function Counter() {
let [count, setCount] = useState(0);
let [delay, setDelay] = useState(1000);
useInterval(() => {
// Your custom logic here
setCount(count + 1);
}, delay);
function handleDelayChange(e) {
setDelay(Number(e.target.value));
}
return (
<>
<h1>{count}</h1>
<input value={delay} onChange={handleDelayChange} />
</>
);
}
(ЭтоCodeSandbox demo. )
Да,Это все.
В отличие от версии класса,useInterval
В примере с хуком «обновить» для динамической настройки задержки очень просто:
// 固定 delay
useInterval(() => {
setCount(count + 1);
}, 1000);
// 可调整 delay
useInterval(() => {
setCount(count + 1);
}, delay);
когдаuseInterval
Хук получает другую задержку, он сбрасывает интервал.
Объявите интервал с динамически регулируемой задержкой вместо записиДобавить ка такжеЧистоКод для интервала -useInterval
Крюк сделал это за нас.
если я хочу временноПаузаЧто должен делать интервал? Я могу сделать это с состоянием:
const [delay, setDelay] = useState(1000);
const [isRunning, setIsRunning] = useState(true);
useInterval(() => {
setCount(count + 1);
}, isRunning ? delay : null);
(Этоdemo!)
Это снова вдохновило меня на React и Hooks. Мы можем обернуть существующие императивные API и создать декларативные API, которые более точно выражают наши намерения. В случае рендеринга мы можем одновременно и точно описать процесс в каждый момент времени без необходимости тщательно манипулировать им с помощью инструкций.
Я надеюсь, здесь вы начнете чувствоватьuseInterval()
Хуки — лучший API, по крайней мере, по сравнению с компонентами.
Но зачем использовать в хукахsetInterval()
а такжеclearInterval()
Это раздражает? Вернемся к примеру со счетчиком и попробуем реализовать его вручную.
первая попытка
Я бы начал с простого примера, который просто отображает начальное состояние:
function Counter() {
const [count, setCount] = useState(0);
return <h1>{count}</h1>;
}
Теперь мне нужен интервал, который увеличивается каждую секунду, этоНужно убрать побочные эффекты, поэтому я буду использоватьuseEffect()
и вернуть функцию очистки:
function Counter() {
let [count, setCount] = useState(0);
useEffect(() => {
let id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
});
return <h1>{count}</h1>;
}
(ПроверитьCodeSandbox demo.)
Такая работа кажется легкой, не так ли?
Однако этот код имеет странное поведение.
По умолчанию React будет повторно выполнять эффекты после каждого рендеринга, что является целесообразным и помогает избежать использования компонентов класса React.какие-то баги.
Обычно это хорошо, так как требуется много API-интерфейсов подписки, чтобы легко удалять старые слушатели и добавлять новые в любое время. но,setInterval
не такие, как они. когда мы выполняемclearInterval
а такжеsetInterval
, они попадут в очередь времени, и если мы будем часто повторно рендерить и повторно выполнять эффекты, интервал может не иметь шанса быть выполненным!
Мы можем сделать это с помощьюкорочеПовторно визуализируйте наш компонент через определенные промежутки времени, чтобы отловить ошибку:
setInterval(() => {
// 重渲染和重执行 Counter 的 effects
// 这里会发生 clearInterval()
// 在 interval 被执行前 setInterval()
ReactDOM.render(<Counter />, rootElement);
}, 100);
(см. эту ошибкуdemo)
вторая попытка
ты наверное знаешьuseEffect()
позволит намвыборочноЧтобы повторно выполнить эффекты, вы можете указать массив зависимостей в качестве второго параметра, и React будет перезапускаться только в том случае, если что-то в массиве изменится:
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]);
когда мыТолькоЧтобы очистить его, когда эффект выполняется при монтировании и при размонтировании, мы можем передать пусто[]
массив зависимостей.
Однако, если вы не знакомы с замыканиями в JavaScript, вы столкнетесь с распространенной ошибкой. Давайте сделаем эту ошибку сейчас! (Мы также установили ранний отзыв об этой ошибкеправила ворса, но еще не готов. )
В нашей первой попытке наша проблема заключалась в том, что повторный запуск эффектов приводил к преждевременному сбросу таймеров, мы можем попытаться исправить их без повторного запуска:
function Counter() {
let [count, setCount] = useState(0);
useEffect(() => {
let id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
Однако теперь, когда наш таймер обновлен до 1, он не движется. (Просмотр реальных ошибок. )
что случилось? !
Проблема в,useEffect
Получить значение 0 при первом рендерингеcount
, мы больше не выполняем эффект повторно, поэтомуsetInterval
Всегда обращайтесь к замыканию при первом рендереcount
, так чтоcount + 1
всегда1
. Ой!
Я слышу, как ты скрежещешь зубами, крючки раздражают, не так ли??
почини этопрочьиспользуется какsetCount(c => c + 1)
Такая замена "апдейтера"setCount(count + 1)
, чтобы можно было прочитать новую переменную состояния. Но это не поможет вам получить новый реквизит.
другой методиспользуетсяuseReducer()
. Этот подход обеспечивает большую гибкость для вас. В редюсере вы можете получить доступ к текущему состоянию и новым свойствам.dispatch
Сам метод никогда не меняется, поэтому в него можно поместить данные из любого замыкания.useReducer()
Одним из ограничений является то, что вы не можете использовать его для выполнения побочных эффектов. (Однако вы можете вернуться в новое состояние — активировать некоторые эффекты.)
Но зачем так усложнять?
Несоответствие импеданса
Иногда упоминается термин,Phil HaackОбъяснение следующее:
Некоторые говорят, что базы данных с Марса, а объекты с Венеры, базы данных не отображаются естественным образом в объектной модели. Это очень похоже на попытку столкнуть полюса магнита вместе.
Наше «соответствие импеданса» не между базами данных и объектами, а между моделью программирования React и императивом.setInterval
между API.
Компонент React может проходить через множество различных состояний перед монтированием, но результат его рендеринга будетвсе вместеОпишите это.
// 描述每次渲染
return <h1>{count}</h1>
Хуки позволяют нам применять тот же декларативный подход к эффектам:
// 描述每个间隔状态
useInterval(() => {
setCount(count + 1);
}, isRunning ? delay : null);
мы ненастраиватьинтервал, но указать егобудь тоУстановите задержку или сколько задерживать, наши хуки делают, описывают непрерывный процесс в дискретных терминах
Напротив,setInterval
Процесс не описан своевременно - после того, как интервал установлен, вы не можете внести в него какие-либо изменения, кроме как очистить его..
Это модель React иsetInterval
Несоответствие между API.
Реквизиты и состояние в компонентах React можно изменить, React перерендерит их и «отбросит» любые результаты предыдущего рендеринга, они больше не актуальны.
useEffect()
Хук также «отбрасывает» последний результат рендеринга, он очистит предыдущий эффект и создаст следующий эффект, следующий эффект блокирует новые реквизиты и состояние, что также является нашимпервая попыткаПричина, по которой простой пример работает правильно.
ноsetInterval
Не «выброшен».Он будет продолжать ссылаться на старые свойства и состояние до тех пор, пока вы его не замените — вы не можете сделать это без сброса времени.
Или подождите, вы можете это сделать?
Рефы могут!
Проблема сводится к следующему:
- Выполняем бэнд на первом рендере
callback1
изsetInterval(callback1, delay)
. - Мы получаем новые реквизиты и состояние на следующем рендере.
callbaxk2
. - Мы не можем заменить существующий интервал без сброса времени.
то если мы вообще не будем заменять интервал, а введем указатель нановыйИзменяемый для интервальных обратных вызововsavedCallback
что случится?
Теперь давайте посмотрим на эту схему:
- мы называем
setInterval(fn, delay)
,вfn
передачаsavedCallback
. - После первого рендера
savedCallback
установить какcallback1
. - После следующего рендера будет
savedCallback
установить какcallback2
. - ???
- Заканчивать
эта переменнаяsavedCallback
Должен «сохраняться» при повторном рендеринге, поэтому не может быть обычной переменной, нам нужно поле, подобное экземпляру.
Как мы узнали из FAQ по хукам,useRef()
дает желаемый результат:
const savedCallback = useRef();
// { current: null }
(Возможно, вы знакомы с ReactDOM refs). Хуки используют ту же концепцию для хранения произвольных изменяемых значений. ref похож на "коробку", куда можно положить что угодно
useRef()
возвращаетcurrent
Обычные объекты с изменяемыми свойствами распределяются между рендерами, мы можем сохранитьновыйИнтервал возвращается к нему:
function callback() {
// 可以读到新 props,state等。
setCount(count + 1);
}
// 每次渲染后,保存新的回调到我们的 ref 里。
useEffect(() => {
savedCallback.current = callback;
});
Затем мы можем прочитать и вызвать его из нашего интервала:
useEffect(() => {
function tick() {
savedCallback.current();
}
let id = setInterval(tick, 1000);
return () => clearInterval(id);
}, []);
благодарный[]
, интервал не будет сброшен без повторного выполнения нашего эффекта. Кроме того, спасибоsavedCallback
ref, поэтому мы всегда можем прочитать обратный вызов после нового рендеринга и вызвать его в тик интервала.
Вот полное решение:
function Counter() {
const [count, setCount] = useState(0);
const savedCallback = useRef();
function callback() {
setCount(count + 1);
}
useEffect(() => {
savedCallback.current = callback;
});
useEffect(() => {
function tick() {
savedCallback.current();
}
let id = setInterval(tick, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
(СмотретьCodeSandbox demo. )
Извлеките крючок
По общему признанию, приведенный выше код сбивает с толку, смешение противоположных парадигм вызывает недоумение и, возможно, путает изменяемые ссылки.
Я чувствую, что хуки предоставляют примитивы более низкого уровня, чем классы, но их прелесть в том, что они позволяют нам составлять и создавать лучшие декларативные абстракции..
В идеале я просто хочу написать:
function Counter() {
const [count, setCount] = useState(0);
useInterval(() => {
setCount(count + 1);
}, 1000);
return <h1>{count}</h1>;
}
Я скопировал код для своего механизма ссылок в пользовательский хук:
function useInterval(callback) {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
});
useEffect(() => {
function tick() {
savedCallback.current();
}
let id = setInterval(tick, 1000);
return () => clearInterval(id);
}, []);
}
ток,1000
задержка жестко запрограммирована, я хочу сделать ее параметром:
function useInterval(callback, delay) {
Я бы использовал его после создания interval :
let id = setInterval(tick, delay);
Сейчасdelay
Может меняться между рендерами, мне нужно объявить это в разделе зависимостей эффекта интервала:
useEffect(() => {
function tick() {
savedCallback.current();
}
let id = setInterval(tick, delay);
return () => clearInterval(id);
}, [delay]);
Подождите, мы не пытаемся избежать сброса эффекта интервала, а специально пропускаем[]
избежать этого? Не совсем так, мы просто хотимПерезвонитеНе сбрасывайте его при изменении, но когдаdelay
когда мы изменимсяхочуПерезапустите таймер!
Проверим, работает ли наш код:
function Counter() {
const [count, setCount] = useState(0);
useInterval(() => {
setCount(count + 1);
}, 1000);
return <h1>{count}</h1>;
}
function useInterval(callback, delay) {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
});
useEffect(() => {
function tick() {
savedCallback.current();
}
let id = setInterval(tick, delay);
return () => clearInterval(id);
}, [delay]);
}
(попытайсяCodeSandbox. )
эффективный! Нам не нужно слишком много думать сейчасuseInterval()
процесс реализации, используйте его в любом компоненте.
Преимущество: интервал паузы
Предположим, мы хотим иметь возможность пройтиnull
так какdelay
Чтобы приостановить наш интервал:
const [delay, setDelay] = useState(1000);
const [isRunning, setIsRunning] = useState(true);
useInterval(() => {
setCount(count + 1);
}, isRunning ? delay : null);
Как этого добиться? При ответе: интервал не создается.
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
(СмотретьCodeSandbox demo. )
Вот и все. Этот код обрабатывает все возможные изменения: изменение задержки, приостановку или возобновление интервала.useEffect()
API требует, чтобы мы проводили больше предварительной работы, описывая сборку и очистку, но добавлять новые случаи несложно.
Бонус: интересная демонстрация
useInterval()
Хуки — это очень весело, а когда побочные эффекты декларативны, гораздо проще организовать сложное поведение вместе.
Например: в нашем интервалеdelay
Может управляться другим:
function Counter() {
const [delay, setDelay] = useState(1000);
const [count, setCount] = useState(0);
// 增加计数器
useInterval(() => {
setCount(count + 1);
}, delay);
// 每秒加速
useInterval(() => {
if (delay > 10) {
setDelay(delay / 2);
}
}, 1000);
function handleReset() {
setDelay(1000);
}
return (
<>
<h1>Counter: {count}</h1>
<h4>Delay: {delay}</h4>
<button onClick={handleReset}>
Reset delay
</button>
</>
);
}
(СмотретьCodeSandbox demo! )
Эпилог
Крючки требуют времени, чтобы привыкнуть к ним.оченьнаходится в коде, который охватывает императивный и декларативный. вы можете создать что-то вродеReact SpringТе же абстракции, но иногда они вызывают у вас беспокойство.
Крючки все еще находятся на ранних стадиях, и нет сомнений, что эта модель все еще нуждается в практике и сравнении. Если вы привыкли следовать известным «лучшим практикам», не спешите внедрять хуки, это требует много экспериментов и исследований.
Я надеюсь, что эта статья поможет вам понятьsetInterval()
Часто задаваемые вопросы об хуках для других API, шаблонах, которые могут помочь вам преодолеть их, и сладких плодах более выразительных декларативных API, построенных на их основе.
перевести оригиналMaking setInterval Declarative with React Hooks(2019-02-04)