React Hooks — это новая функция React 16.8, которая может использовать такие функции, как состояние, без написания класса, таким образом делая функциональные компоненты излица без гражданстваИзменятьс сохранением состояния. Пакет типов React @types/react также изменил React.SFC (функциональный компонент без сохранения состояния) на React.FC (функциональный компонент).
Благодаря этому обновлению компоненты, написанные в исходном классе, могут быть полностью заменены функциональными компонентами. Хотя вопрос о том, менять ли все компоненты класса в старом проекте на функциональные компоненты, зависит от человека к человеку, новые написанные компоненты стоит попробовать, потому что объем кода действительно значительно сокращается, особенно повторяющийся код (например, componentDidMount + componentDidUpdate). + componentWillUnmount=useEffect).
С выхода 16.8 прошло уже больше полугода (в феврале этого года), но мой уровень ограничен, особенно при использовании useEffect в сочетании с асинхронными задачами, я часто наступаю на какие-то ямы. Эта статья написана специально, и ее следует записать для ознакомления коллегам, столкнувшимся с такой же проблемой. Я коснусь трех очень распространенных проблем с проектами:
- Как инициировать асинхронную задачу при загрузке компонента
- Как инициировать асинхронные задачи при взаимодействии компонентов
- другие ловушки
TL;DR
- использовать
useEffect
Инициировать асинхронную задачу, второй параметр использует пустой массив для выполнения тела метода при загрузке компонента, а функция возвращаемого значения выполняется один раз при выгрузке компонента для очистки некоторых вещей, таких как таймер. - Используйте AbortController или собственный семафор какой-либо библиотеки (
axios.CancelToken
), чтобы управлять запросом на прерывание и выходить более изящно. - Когда таймер необходимо установить в другом месте (например, в обработчике кликов), в
useEffect
При очистке возвращаемого значения используйте локальную переменную илиuseRef
записать этоtimer
.не хочуиспользоватьuseState
. - появляется в компоненте
setTimeout
В ожидании замыкания старайтесь обращаться к ref вместо state внутри замыкания, иначе легко прочитать старое значение. -
useState
Возвращаемый метод состояния обновления является асинхронным, и новое значение нельзя получить до следующей перерисовки. Не пытайтесь получить состояние сразу после его изменения.
Как инициировать асинхронную задачу при загрузке компонента
Такого рода требования очень распространены.Типичным примером является отправка запроса на серверную часть при загрузке компонента списка, а затем отображение списка после его получения.
Отправка запросов также является одним из определенных побочных эффектов React, поэтому его следует использоватьuseEffect
написать. Я не буду слишком подробно объяснять основной синтаксис, код выглядит следующим образом:
import React, { useState, useEffect } from 'react';
const SOME_API = '/api/get/value';
export const MyComponent: React.FC<{}> = () => {
const [loading, setLoading] = useState(true);
const [value, setValue] = useState(0);
useEffect(() => {
(async () => {
const res = await fetch(SOME_API);
const data = await res.json();
setValue(data.value);
setLoading(false);
})();
}, []);
return (
<>
{loading ? (
<h2>Loading...</h2>
) : (
<h2>value is {value}</h2>
)}
</>
);
}
Выше приведен базовый компонент с функцией загрузки, который отправляет асинхронный запрос на серверную часть, чтобы получить значение и отобразить его на странице. Если стандарта примера достаточно, но чтобы применить его к проекту, необходимо рассмотреть несколько вопросов.
Что, если компонент будет уничтожен до того, как вернется ответ?
React сообщит о предупреждении
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.in Notification
К тому, что он не должен изменять его состояние после удаления компонента. Хотя не влияет на операцию, но от имени перфекционных программистов групп не может терпеть это происходит, то как ее решить?
Суть проблемы в том, что он все еще вызывается после удаления компонентаsetValue(data.value)
а такжеsetLoading(false)
изменить статус. Таким образом, простой способ - отметить, был ли компонент удален, вы можете использоватьuseEffect
Возвращаемое значение.
// 省略组件其他内容,只列出 diff
useEffect(() => {
let isUnmounted = false;
(async () => {
const res = await fetch(SOME_API);
const data = await res.json();
if (!isUnmounted) {
setValue(data.value);
setLoading(false);
}
})();
return () => {
isUnmounted = true;
}
}, []);
Таким образом, этого предупреждения можно успешно избежать.
Есть ли более элегантное решение?
Вышеупомянутый метод заключается в том, чтобы судить, когда ответ получен, то есть он слегка пассивен, что необходимо дождаться завершения ответа в любом случае. Более проактивный способ — напрямую прерывать запрос при обнаружении выгрузки, и естественно нет необходимости ждать ответа. Такой проактивный подход требует использованияAbortController.
AbortController — это экспериментальный интерфейс для браузеров, который может возвращать сигнал для отмены отправленного запроса. Этот интерфейс совместим со всеми, кроме IE (такими как Chrome, Edge, FF и большинством мобильных браузеров, включая Safari).
useEffect(() => {
let isUnmounted = false;
const abortController = new AbortController(); // 创建
(async () => {
const res = await fetch(SOME_API, {
singal: abortController.singal, // 当做信号量传入
});
const data = await res.json();
if (!isUnmounted) {
setValue(data.value);
setLoading(false);
}
})();
return () => {
isUnmounted = true;
abortController.abort(); // 在组件卸载时中断
}
}, []);
Реализация singal зависит от метода, используемого для фактической отправки запроса, как в приведенном выше примере.fetch
метод принимаетsingal
Атрибуты. Если вы используете axios, он уже содержитaxios.CancelToken
, можно использовать напрямую, пример находится вздесь.
Как инициировать асинхронные задачи при взаимодействии компонентов
Другим распространенным требованием является отправка запроса или запуск таймера при взаимодействии компонента (например, нажатие кнопки), а затем изменение данных для воздействия на страницу после получения ответа. Самая большая разница между этим и предыдущим разделом (когда компонент загружен) заключается в том, что React Hooks можно писать только на уровне компонента, а не на уровне метода (dealClick
) или логика управления (if
, for
и т. д.) написан внутри, поэтому его нельзя вызвать снова в функции ответа на кликuseEffect
. Но мы все еще должны использоватьuseEffect
Функция возврата для очистки.
Возьмем в качестве примера таймер. Предположим, мы хотим создать компонент, который запускает таймер (5 с) после нажатия кнопки и изменяет состояние по истечении таймера. Но если компонент уничтожается до истечения времени таймера, мы хотим остановить таймер, чтобы избежать утечек памяти. Если вы реализуете это в коде, вы обнаружите, что таймер запуска и таймер очистки будут находиться в разных местах, поэтому этот таймер необходимо записать. См. следующий пример:
import React, { useState, useEffect } from 'react';
export const MyComponent: React.FC<{}> = () => {
const [value, setValue] = useState(0);
let timer: number;
useEffect(() => {
// timer 需要在点击时建立,因此这里只做清理使用
return () => {
console.log('in useEffect return', timer); // <- 正确的值
window.clearTimeout(timer);
}
}, []);
function dealClick() {
timer = window.setTimeout(() => {
setValue(100);
}, 5000);
}
return (
<>
<span>Value is {value}</span>
<button onClick={dealClick}>Click Me!</button>
</>
);
}
Поскольку таймер должен быть записан, естественно использовать внутреннюю переменную для его хранения (на данный момент не считается, что непрерывное нажатие кнопки приводит к появлению нескольких таймеров, предполагая, что она нажата только один раз. Потому что на самом деле нажатие на кнопку вызовет другие изменения состояния, а затем изменится интерфейс. , он не будет нажиматься).
Здесь следует отметить, что еслиtimer
Обновите до состояния, вместо этого кодпроблема появляется. Рассмотрим следующий код:
import React, { useState, useEffect } from 'react';
export const MyComponent: React.FC<{}> = () => {
const [value, setValue] = useState(0);
const [timer, setTimer] = useState(0); // 把 timer 升级为状态
useEffect(() => {
// timer 需要在点击时建立,因此这里只做清理使用
return () => {
console.log('in useEffect return', timer); // <- 0
window.clearTimeout(timer);
}
}, []);
function dealClick() {
let tmp = window.setTimeout(() => {
setValue(100);
}, 5000);
setTimer(tmp);
}
return (
<>
<span>Value is {value}</span>
<button onClick={dealClick}>Click Me!</button>
</>
);
}
семантическиtimer
Рассматривается ли это как состояние компонента или нет, давайте пока отложим это в сторону и посмотрим только на уровень кода. использоватьuseState
помнитьtimer
состояние, использованиеsetTimer
Чтобы изменить состояние, это кажется разумным. Но в реальной эксплуатации вuseEffect
В возвращаемой функции очистки полученныйtimer
это начальное значение, т.0
.
Почему два написания различны?
Суть в том, являются ли записанная переменная и прочитанная переменная одной и той же переменной.
Первый способ написать код — поместитьtimer
Используется как локальная переменная внутри компонента. При первом рендеринге компонентаuseEffect
Возвращаемая функция закрытия указывает на эту локальную переменнуюtimer
. существуетdealClick
Возвращаемое значение по-прежнему записывается в эту локальную переменную, когда таймер установлен вновыйлокальная переменнаяtimer
, но это не влияет на внутреннюю часть крышкиСтарый timer
, так что результат правильный.
Второй способ письма,timer
ЯвляетсяuseState
Возвращаемое значение не является простой переменной. из реагирующих хуковисходный код, он возвращается[hook.memorizedState, dispatch]
, соответствующий выбранному нами значению и методу изменения. при звонкеsetTimer
а такжеsetValue
, вызвать две перерисовки соответственно, так чтоhook.memorizedState
указал наnewState
(Примечание: не модификация, а переуказание). ноuseEffect
вернуть закрытиеtimer
По-прежнему указывает на старое состояние, поэтому новое значение получить нельзя. (то есть читается старое значение, но пишется новое значение, не то же самое)
Если вам трудно читать исходный код хуков, вы можете понять его с другой точки зрения: хотя React запустил хуки в 16.8, на самом деле он только усиливает метод написания функциональных компонентов, чтобы они имели состояние и использовались в качестве альтернативы классу. компоненты. , но внутренности состояния React не изменились. в реакцииsetState
Внутри новое состояние и старое состояние объединяются посредством операции слияния, а затем возвращается новое состояние.новыйгосударственный объект. Независимо от того, как написаны хуки, этот принцип остается неизменным. Теперь замыкание указывает на старый объект состояния, иsetTimer
а такжеsetValue
Повторная генерация и указание на новый объект состояния не влияет на замыкание, в результате чего замыкание не может прочитать новое состояние.
Мы заметили, что React также предоставляет намuseRef
, который определяется как
useRef возвращает изменяемый объект ref, чей
current
Свойства инициализируются переданным параметром (initialValue). возвращаемый объект ссылкиОн остается неизменным на протяжении всего срока службы компонента.
Объект ref может гарантировать, что значение не изменится на протяжении всего жизненного цикла и обновится синхронно, потому что возвращаемое значение ref всегда имеет только один экземпляр, а все операции чтения и записи указывают на него самого. Таким образом, это также может быть использовано для решения проблемы здесь.
import React, { useState, useEffect, useRef } from 'react';
export const MyComponent: React.FC<{}> = () => {
const [value, setValue] = useState(0);
const timer = useRef(0);
useEffect(() => {
// timer 需要在点击时建立,因此这里只做清理使用
return () => {
window.clearTimeout(timer.current);
}
}, []);
function dealClick() {
timer.current = window.setTimeout(() => {
setValue(100);
}, 5000);
}
return (
<>
<span>Value is {value}</span>
<button onClick={dealClick}>Click Me!</button>
</>
);
}
На самом деле, как мы увидим позже,useRef
Более безопасно и стабильно работать с асинхронными задачами.
другие ловушки
Изменение состояния является асинхронным
Это на самом деле довольно просто.
import React, { useState } from 'react';
export const MyComponent: React.FC<{}> = () => {
const [value, setValue] = useState(0);
function dealClick() {
setValue(100);
console.log(value); // <- 0
}
return (
<span>Value is {value}, AnotherValue is {anotherValue}</span>
);
}
useState
Возвращаемая функция модификации является асинхронной и не вступит в силу сразу после вызова, поэтому прочитайте ее немедленно.value
Получено старое значение (0
).
Дизайн React предназначен для соображений производительности, и легко понять, что легко понять, что проблема обновления может быть решена путем перерисовки только один раз после всех изменений состояния, а не изменения и перерисовки один раз.
Невозможно прочитать новые значения для других состояний по тайм-ауту
import React, { useState, useEffect } from 'react';
export const MyComponent: React.FC<{}> = () => {
const [value, setValue] = useState(0);
const [anotherValue, setAnotherValue] = useState(0);
useEffect(() => {
window.setTimeout(() => {
console.log('setAnotherValue', value) // <- 0
setAnotherValue(value);
}, 1000);
setValue(100);
}, []);
return (
<span>Value is {value}, AnotherValue is {anotherValue}</span>
);
}
Этот вопрос и приведенное выше использованиеuseState
записатьtimer
Точно так же при создании закрытия тайм-аута значение value равно 0. Хотя после прохожденияsetValue
Состояние изменено, но React уже внутренне указал на новую переменную, а замыкание по-прежнему ссылается на старую переменную, поэтому замыкание по-прежнему получает старое начальное значение, равное 0.
Чтобы исправить эту проблему, это все еще полезноuseRef
,следующим образом:
import React, { useState, useEffect, useRef } from 'react';
export const MyComponent: React.FC<{}> = () => {
const [value, setValue] = useState(0);
const [anotherValue, setAnotherValue] = useState(0);
const valueRef = useRef(value);
valueRef.current = value;
useEffect(() => {
window.setTimeout(() => {
console.log('setAnotherValue', valueRef.current) // <- 100
setAnotherValue(valueRef.current);
}, 1000);
setValue(100);
}, []);
return (
<span>Value is {value}, AnotherValue is {anotherValue}</span>
);
}
или проблема с тайм-аутом
Предположим, мы хотим реализовать кнопку, которая по умолчанию отображает false. Изменяется на true при нажатии, но возвращается на false через две секунды (true и false взаимозаменяемы). Рассмотрим следующий код:
import React, { useState } from 'react';
export const MyComponent: React.FC<{}> = () => {
const [flag, setFlag] = useState(false);
function dealClick() {
setFlag(!flag);
setTimeout(() => {
setFlag(!flag);
}, 2000);
}
return (
<button onClick={dealClick}>{flag ? "true" : "false"}</button>
);
}
Мы обнаружим, что он может нормально переключаться при нажатии, но не изменится обратно через две секунды. Причина все же в томuseState
Обновление должно переназначить новое значение, но закрытие тайм-аута по-прежнему указывает на старое значение. Итак, в примереflag
всегдаfalse
, хотя в последующемsetFlag(!flag)
, но все равно не влияет на таймаут внутриflag
.
Есть два решения.
Первый заключается в использованииuseRef
import React, { useState, useRef } from 'react';
export const MyComponent: React.FC<{}> = () => {
const [flag, setFlag] = useState(false);
const flagRef = useRef(flag);
flagRef.current = flag;
function dealClick() {
setFlag(!flagRef.current);
setTimeout(() => {
setFlag(!flagRef.current);
}, 2000);
}
return (
<button onClick={dealClick}>{flag ? "true" : "false"}</button>
);
}
Второй заключается в использованииsetFlag
Может получать функции в качестве параметров и использовать замыкания и параметры для реализации
import React, { useState } from 'react';
export const MyComponent: React.FC<{}> = () => {
const [flag, setFlag] = useState(false);
function dealClick() {
setFlag(!flag);
setTimeout(() => {
setFlag(flag => !flag);
}, 2000);
}
return (
<button onClick={dealClick}>{flag ? "true" : "false"}</button>
);
}
когдаsetFlag
Когда параметр является типом функции, смысл функции состоит в том, чтобы сообщить React, какТекущее состояниепроизводитьновое состояние(Похоже на редуктор редукса, но дочерний редуктор только для одного состояния). Поскольку это текущее состояние, эффект может быть достигнут инвертированием возвращаемого значения.
Суммировать
Особенно таймаута, мы должны уделять особое внимание асинхронные задачи, появляются в крючке.useState
Гарантируется только состояние между несколькими перерисовкамистоимостьодинаковы, но не гарантируется, что они будут одним и тем же объектом, поэтому, когда есть ссылка на замыкание, попробуйте использоватьuseRef
Вместо того, чтобы напрямую использовать само состояние, легко наступить на яму. С другой стороны, если вы столкнулись с ситуацией, когда устанавливается новое значение, но считывается старое значение, вы также можете подумать в этом направлении, что может быть причиной.