Вопросы и ответы по хукам
Хуки — это новое дополнение в React 16.8. Они позволяют использовать состояние и другие возможности React без написания класса.
Эта страница отвечает на некоторые часто задаваемые вопросы о хуках.
-
- Какие версии React включают хуки?
- Нужно ли переписывать все мои компоненты класса?
- Что можно сделать с хуками, чего нельзя было сделать с классами?
- Сколько моих знаний о React остаётся актуальным?
- Использовать хуки, классы или их смесь?
- Хуки покрывают все варианты использования классов?
- Хуки заменяют render props и higher-order компоненты?
- Что означают хуки для популярных API, таких как Redux connect() и React Router?
- Хуки работают со статической типизацией?
- Как тестировать компоненты, использующие хуки?
- Что именно проверяют правила линтера?
-
- Как методы жизненного цикла соответствуют хукам?
- Как можно делать загрузку данных с помощью хуков?
- Есть ли что-то подобное переменным экземпляра?
- Использовать одну или несколько переменных состояния?
- Можно ли запустить эффект только при обновлениях?
- Как получить предыдущие значения props или состояния?
- Почему я вижу устаревшие props или состояние внутри функции?
- Как реализовать getDerivedStateFromProps?
- Есть ли что-то подобное forceUpdate?
- Можно ли создать ссылку на функциональный компонент?
- Как измерить узел DOM?
- Что означает const [thing, setThing] = useState()?
-
Оптимизация производительности
- Можно ли пропустить эффект при обновлениях?
- Можно ли безопасно опустить функции из списка зависимостей?
- Что делать, если зависимости эффекта слишком часто изменяются?
- Как реализовать shouldComponentUpdate?
- Как кешировать вычисления?
- Как создавать дорогостоящие объекты лениво?
- Хуки медленные из-за создания функций в render?
- Как избежать передачи колбэков вниз по иерархии?
- Как читать часто меняющееся значение из useCallback?
Стратегия внедрения
Какие версии React включают хуки?
Начиная с версии 16.8.0, React включает стабильную реализацию React хуков для:
- React DOM
- React Native
- React DOM Server
- React Test Renderer
- React Shallow Renderer
Обратите внимание, что для включения хуков все пакеты React должны быть версии 16.8.0 или выше. Хуки не будут работать, если вы забудете обновить, например, React DOM.
React Native 0.59 и выше поддерживают хуки.
Нужно ли переписывать все мои компоненты класса?
Нет. Нет планов удалять классы из React — мы все должны продолжать выпускать продукты и не можем позволить себе переписывание. Мы рекомендуем попробовать хуки в новом коде.
Что можно сделать с хуками, чего нельзя было сделать с классами?
Хуки предлагают мощный и выразительный новый способ повторного использования функциональности между компонентами. “Создание собственных хуков” демонстрирует возможности. Эта статья от члена команды разработчиков React более подробно рассматривает новые возможности, открытые хуками.
Сколько моих знаний о React остаётся актуальным?
Хуки — более прямой способ использования уже известных вам возможностей React, таких как состояние, жизненный цикл, контекст и ссылки. Они не меняют принципы работы React, и ваши знания о компонентах, props и передаче данных сверху вниз остаются актуальными.
Хуки имеют свой собственный кривой обучения. Если в этой документации чего-то не хватает, создайте вопрос, и мы постараемся помочь.
Использовать хуки, классы или их смесь?
Когда вы будете готовы, мы рекомендуем начать использовать хуки в новых компонентах, которые вы создаёте. Убедитесь, что все члены вашей команды согласны использовать их и знакомы с этой документацией. Не рекомендуем переписывать ваши существующие классы в хуки, если вы не планировали их переписывание (например, для исправления ошибок).
Вы не можете использовать хуки *внутри* компонента класса, но вы определённо можете смешивать классы и функциональные компоненты с хуками в одном дереве. Будет ли компонент классом или функцией, использующей хуки, — это деталь реализации этого компонента. В долгосрочной перспективе ожидается, что хуки будут основным способом написания компонентов React.
Хуки покрывают все варианты использования классов?
Наша цель — как можно скорее покрыть все варианты использования классов с помощью хуков. Пока нет эквивалентов хуков для редких getSnapshotBeforeUpdate, getDerivedStateFromError и componentDidCatch методов жизненного цикла, но мы планируем добавить их вскоре.
Хуки заменяют render props и higher-order компоненты?
Зачастую render props и higher-order компоненты отображают только один дочерний элемент. Мы считаем, что хуки — более простой способ решения этой задачи. Обе модели по-прежнему имеют место (например, компонент виртуальной прокрутки может иметь renderItem свойство, или компонент визуального контейнера может иметь свою собственную структуру DOM). Но в большинстве случаев хуки будут достаточными и могут помочь уменьшить вложенность в вашем дереве.
Что означают хуки для популярных API, таких как Redux connect() и React Router?
Вы можете продолжить использование тех же API, что и всегда; они по-прежнему будут работать.
React Redux с версии 7.1.0 поддерживает API хуков и предоставляет хуки, такие как useDispatch или useSelector.
React Router поддерживает хуки с версии 5.1.
Другие библиотеки также могут в будущем поддерживать хуки.
Хуки работают со статической типизацией?
Хуки разрабатывались с учётом статической типизации. Поскольку они являются функциями, их легче правильно типизировать, чем такие модели, как higher-order компоненты. Последние определения Flow и TypeScript для React включают поддержку React хуков.
Важно, что пользовательские хуки предоставляют возможность ограничить API React, если вы хотите типизировать их более строго каким-либо способом. React предоставляет примитивы, но вы можете комбинировать их различными способами, отличными от того, что мы предоставляем из коробки.
Как тестировать компоненты, использующие хуки?
С точки зрения React, компонент, использующий хуки, — это просто обычный компонент. Если ваша система тестирования не полагается на внутренние механизмы React, тестирование компонентов с хуками не должно отличаться от того, как вы обычно тестируете компоненты.
Примечание
Примеры тестов содержат множество примеров, которые можно копировать и вставлять.
Например, предположим, что у нас есть этот компонент счётчика:
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
} Мы протестируем его с помощью React DOM. Чтобы убедиться, что поведение соответствует тому, что происходит в браузере, мы обернём код рендеринга и его обновления в вызовы ReactTestUtils.act():
import React from 'react';
import ReactDOM from 'react-dom';
import { act } from 'react-dom/test-utils';
import Counter from './Counter';
let container;
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
document.body.removeChild(container);
container = null;
});
it('can render and update a counter', () => {
// Test first render and effect
act(() => {
ReactDOM.render(<Counter />, container);
});
const button = container.querySelector('button');
const label = container.querySelector('p');
expect(label.textContent).toBe('You clicked 0 times');
expect(document.title).toBe('You clicked 0 times');
// Test second render and effect
act(() => {
button.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(label.textContent).toBe('You clicked 1 times');
expect(document.title).toBe('You clicked 1 times');
}); Вызовы act() также очистят эффекты внутри них.
Если вам нужно протестировать пользовательский хук, вы можете сделать это, создав компонент в тесте и использовав ваш хук в нём. Затем вы можете протестировать написанный вами компонент.
Для уменьшения объёма кода мы рекомендуем использовать React Testing Library, который разработан для написания тестов, использующих ваши компоненты так же, как это делают конечные пользователи.
Для получения более подробной информации см. Примеры тестов.
Что именно проверяют правила линтера?
Мы предоставляем плагин ESLint, который проверяет правила хуков, чтобы избежать ошибок. Он предполагает, что любая функция, начинающаяся с «use» и за которой следует заглавная буква, является хуком. Мы понимаем, что этот эвристический подход не идеален и может давать ложные срабатывания, но без общесистемной соглашения нет способа заставить хуки работать правильно — и более длинные имена отпугнут людей от того, чтобы либо принять хуки, либо следовать соглашению.
В частности, правило проверяет, что:
- Вызовы хуков находятся внутри функциональной функции (предполагается, что это компонент) или другой функциональной функции (предполагается, что это пользовательский хук).
- Хуки вызываются в одном и том же порядке на каждом рендеринге.
Есть ещё несколько эвристических правил, и они могут со временем меняться по мере доработки правил для балансировки поиска ошибок и избежания ложных срабатываний.
От классов к хукам
Как методы жизненного цикла соответствуют хукам?
-
constructor: Компоненты-функции не нуждаются в конструкторе. Вы можете инициализировать состояние в вызовеuseState. Если вычисление начального состояния затратно, вы можете передать функцию вuseState. -
getDerivedStateFromProps: Распланируйте обновление во время отрисовки вместо этого. -
shouldComponentUpdate: См.React.memoниже. -
render: Это и есть тело компонента-функции. -
componentDidMount,componentDidUpdate,componentWillUnmount: ХукuseEffectможет выразить все комбинации этих случаев (включая менее распространённые случаи). -
getSnapshotBeforeUpdate,componentDidCatchиgetDerivedStateFromError: Пока нет эквивалентов хуков для этих методов, но они скоро появятся.
Как выполнить загрузку данных с помощью хуков?
Вот небольшая демонстрация, чтобы вы начали. Чтобы узнать больше, ознакомьтесь со статьёй о загрузке данных с помощью хуков.
Есть ли что-то подобное переменным экземпляра?
Да! Хук useRef() не только для ссылок на DOM. Объект «ref» — это универсальный контейнер, чьё свойство current изменяемо и может содержать любое значение, аналогично свойству экземпляра класса.
Вы можете записывать в него изнутри useEffect:
function Timer() {
const intervalRef = useRef();
useEffect(() => {
const id = setInterval(() => {
// ...
});
intervalRef.current = id;
return () => {
clearInterval(intervalRef.current);
};
});
// ...
} Если бы мы просто хотели установить интервал, нам бы не понадобился ref (id мог бы быть локальным для эффекта), но он полезен, если мы хотим очистить интервал из обработчика событий:
// ...
function handleCancelClick() {
clearInterval(intervalRef.current);
}
// ... По концепции, refs подобны переменным экземпляра в классе. По возможности избегайте установки refs во время отрисовки, — это может привести к неожиданному поведению. Вместо этого, обычно вы хотите изменять refs в обработчиках событий и эффектах.
Стоит ли использовать одну или несколько переменных состояния?
Если вы пришли из классов, вам, возможно, захочется всегда вызывать useState() один раз и поместить всё состояние в один объект. Вы можете это сделать, если хотите. Вот пример компонента, который отслеживает перемещение мыши. Его позиция и размер хранятся в локальном состоянии:
function Box() {
const [state, setState] = useState({ left: 0, top: 0, width: 100, height: 100 });
// ...
} Теперь предположим, что мы хотим написать логику, которая изменяет left и top при перемещении пользователем мыши. Обратите внимание, как нам нужно вручную объединять эти поля в предыдущий объект состояния:
// ...
useEffect(() => {
function handleWindowMouseMove(e) {
// Spreading "...state" ensures we don't "lose" width and height
setState(state => ({ ...state, left: e.pageX, top: e.pageY }));
}
// Note: this implementation is a bit simplified
window.addEventListener('mousemove', handleWindowMouseMove);
return () => window.removeEventListener('mousemove', handleWindowMouseMove);
}, []);
// ... Это происходит потому, что при обновлении переменной состояния мы заменяем её значение. Это отличается от this.setState в классе, который объединяет обновлённые поля в объект.
Если вы пропустите автоматическое объединение, вы можете написать собственный хук useLegacyState, который объединяет обновления состояния объекта. Однако, мы рекомендуем разделять состояние на несколько переменных состояния на основе того, какие значения, как правило, изменяются вместе.
Например, мы могли бы разделить состояние нашего компонента на объекты position и size и всегда заменять position без необходимости объединения:
function Box() {
const [position, setPosition] = useState({ left: 0, top: 0 });
const [size, setSize] = useState({ width: 100, height: 100 });
useEffect(() => {
function handleWindowMouseMove(e) {
setPosition({ left: e.pageX, top: e.pageY });
}
// ... Разделение независимых переменных состояния также даёт другое преимущество. Это упрощает последующую выделение некоторой связанной логики в пользовательский хук, например:
function Box() {
const position = useWindowPosition();
const [size, setSize] = useState({ width: 100, height: 100 });
// ...
}
function useWindowPosition() {
const [position, setPosition] = useState({ left: 0, top: 0 });
useEffect(() => {
// ...
}, []);
return position;
} Обратите внимание, как мы смогли переместить вызов useState для переменной состояния position и связанный эффект в пользовательский хук без изменения их кода. Если всё состояние находится в одном объекте, его выделение будет сложнее.
И размещение всего состояния в одном вызове useState и наличие вызова useState для каждого поля могут работать. Компоненты, как правило, наиболее удобочитаемы, когда вы находите баланс между этими двумя крайностями и группируете связанное состояние в несколько независимых переменных состояния. Если логика состояния становится сложной, мы рекомендуем управлять ею с помощью редьюсера или пользовательского хука.
Можно ли запустить эффект только при обновлениях?
Это редкий случай. Если вам это нужно, вы можете использовать изменяемый ref, чтобы вручную сохранить булево значение, соответствующее тому, находитесь ли вы на первом или последующем рендере, а затем проверить этот флаг в своём эффекте. (Если вы часто сталкиваетесь с такой задачей, вы можете создать для неё пользовательский хук.)
Как получить предыдущие значения props или состояния?
В настоящее время вы можете сделать это вручную с помощью ref:
function Counter() {
const [count, setCount] = useState(0);
const prevCountRef = useRef();
useEffect(() => {
prevCountRef.current = count;
});
const prevCount = prevCountRef.current;
return <h1>Now: {count}, before: {prevCount}</h1>;
} Это может быть немного запутанным, но вы можете вынести его в пользовательский хук:
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return <h1>Now: {count}, before: {prevCount}</h1>;
}
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
} Обратите внимание, как это будет работать для props, состояния или любого другого вычисленного значения.
function Counter() {
const [count, setCount] = useState(0);
const calculation = count + 100;
const prevCalculation = usePrevious(calculation);
// ... Возможно, в будущем React предоставит usePrevious хук по умолчанию, так как это относительно распространённый случай.
См. также рекомендованный шаблон для выведенного состояния.
Почему я вижу устаревшие props или состояние внутри моей функции?
Любая функция внутри компонента, включая обработчики событий и эффекты, «видит» props и состояние из рендера, в котором она была создана. Например, рассмотрите такой код:
function Example() {
const [count, setCount] = useState(0);
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count);
}, 3000);
}
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
<button onClick={handleAlertClick}>
Show alert
</button>
</div>
);
} Если вы сначала нажмёте «Показать алерт», а затем увеличите счётчик, алерт отобразит переменную count в момент нажатия кнопки «Показать алерт». Это предотвращает ошибки, вызванные предположением кода о том, что props и состояние не изменяются.
Если вы намеренно хотите прочитать самое последнее состояние из какого-либо асинхронного обратного вызова, вы могли бы сохранить его в ref, изменить его и прочитать из него.
Наконец, ещё одна возможная причина, по которой вы видите устаревшие props или состояние, — это если вы используете оптимизацию «массива зависимостей», но не указали все зависимости. Например, если эффект указывает [] в качестве второго аргумента, но считывает someProp внутри, он будет продолжать «видеть» начальное значение someProp. Решение заключается в том, чтобы либо убрать массив зависимостей, либо исправить его. Вот как вы можете работать с функциями, и вот другие распространённые стратегии запуска эффектов реже без неправильного пропуска зависимостей.
Примечание
Мы предоставляем
exhaustive-depsправило ESLint как часть пакетаeslint-plugin-react-hooks. Оно предупреждает, когда зависимости указаны неправильно, и предлагает исправление.
Как реализовать getDerivedStateFromProps?
Хотя вам, вероятно, не нужно это, в редких случаях, когда вам нужно (например, реализуя компонент <Transition>), вы можете обновить состояние прямо во время отрисовки. React повторно запустит компонент с обновлённым состоянием сразу после выхода из первого рендера, поэтому это не будет затратно.
Здесь мы сохраняем предыдущее значение свойства row в переменной состояния, чтобы мы могли сравнить:
function ScrollView({row}) {
const [isScrollingDown, setIsScrollingDown] = useState(false);
const [prevRow, setPrevRow] = useState(null);
if (row !== prevRow) {
// Row changed since last render. Update isScrollingDown.
setIsScrollingDown(prevRow !== null && row > prevRow);
setPrevRow(row);
}
return `Scrolling down: ${isScrollingDown}`;
} Это может показаться странным на первый взгляд, но обновление во время отрисовки — это именно то, что getDerivedStateFromProps всегда было концептуально.
Есть ли что-то вроде forceUpdate?
Как useState так и useReducer хуки отказываются от обновлений, если следующее значение совпадает с предыдущим. Изменение состояния на месте и вызов setState не вызовет повторной отрисовки.
Обычно вы не должны изменять локальное состояние в React. Однако, в качестве «отдушины» вы можете использовать нарастающий счётчик, чтобы принудительно вызвать повторную отрисовку, даже если состояние не изменилось:
const [ignored, forceUpdate] = useReducer(x => x + 1, 0);
function handleClick() {
forceUpdate();
} Старайтесь избегать этого шаблона, если это возможно.
Можно ли создать ссылку на компонент-функцию?
Хотя вам это часто не понадобится, вы можете предоставить родительскому компоненту некоторые императивные методы с помощью хука useImperativeHandle.
Как измерить узел DOM?
Один из простых способов измерить положение или размер узла DOM — использовать ссылку с обратным вызовом. React вызовет этот обратный вызов всякий раз, когда ссылка будет привязана к другому узлу. Вот небольшая демонстрация:
function MeasureExample() {
const [height, setHeight] = useState(0);
const measuredRef = useCallback(node => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
}, []);
return (
<>
<h1 ref={measuredRef}>Hello, world</h1>
<h2>The above header is {Math.round(height)}px tall</h2>
</>
);
} Мы не выбрали useRef в этом примере, потому что ссылка на объект не уведомляет нас об изменениях текущего значения ссылки. Использование ссылки с обратным вызовом гарантирует, что даже если дочерний компонент отобразит измеренный узел позже (например, в ответ на щелчок), мы всё ещё получаем уведомление о нём в родительском компоненте и можем обновить измерения.
Обратите внимание, что мы передаём [] в качестве массива зависимостей к useCallback. Это гарантирует, что наш обратный вызов ссылки не изменится между повторными отрисовками, и поэтому React не будет вызывать его излишне.
В этом примере обратный вызов ссылки будет вызываться только при монтировании и размонтировании компонента, поскольку рендер <h1> компонента остаётся на месте во время всех повторных отрисовок. Если вы хотите получать уведомления всякий раз, когда компонент изменяет размер, вы можете использовать ResizeObserver или сторонний хук, построенный на нём.
Если хотите, можете выделить эту логику в переиспользуемый хук:
function MeasureExample() {
const [rect, ref] = useClientRect();
return (
<>
<h1 ref={ref}>Hello, world</h1>
{rect !== null &&
<h2>The above header is {Math.round(rect.height)}px tall</h2>
}
</>
);
}
function useClientRect() {
const [rect, setRect] = useState(null);
const ref = useCallback(node => {
if (node !== null) {
setRect(node.getBoundingClientRect());
}
}, []);
return [rect, ref];
} Что означает const [thing, setThing] = useState()?
Если вы не знакомы с этой синтаксической конструкцией, ознакомьтесь с объяснением в документации хука состояния.
Оптимизация производительности
Можно ли пропустить эффект при обновлениях?
Да. См. условный запуск эффекта. Обратите внимание, что частое забывание обработки обновлений приводит к ошибкам, поэтому это поведение не по умолчанию.
Можно ли безопасно опустить функции из списка зависимостей?
В общем случае, нет.
function Example({ someProp }) {
function doSomething() {
console.log(someProp);
}
useEffect(() => {
doSomething();
}, []); // 🔴 This is not safe (it calls `doSomething` which uses `someProp`)
} Трудно помнить, какие props или состояние используются функциями за пределами эффекта. Вот почему обычно вы захотите объявлять необходимые функции эффекта внутри его. Тогда легко увидеть, от каких значений из области видимости компонента зависит эффект:
function Example({ someProp }) {
useEffect(() => {
function doSomething() {
console.log(someProp);
}
doSomething();
}, [someProp]); // ✅ OK (our effect only uses `someProp`)
} Если после этого мы все еще не используем никаких значений из области видимости компонента, безопасно указать []:
useEffect(() => {
function doSomething() {
console.log('hello');
}
doSomething();
}, []); // ✅ OK in this example because we don't use *any* values from component scope
В зависимости от вашего случая использования, ниже описаны несколько дополнительных вариантов.
Примечание
Мы предоставляем правило ESLint
exhaustive-depsв качестве части пакетаeslint-plugin-react-hooks. Оно помогает обнаружить компоненты, которые не обрабатывают обновления последовательно.
Давайте посмотрим, почему это важно.
Если вы указываете список зависимостей в качестве последнего аргумента для useEffect, useLayoutEffect, useMemo, useCallback, или useImperativeHandle, он должен включать все значения, используемые внутри обратного вызова и участвующие в потоке данных React. Это включает props, состояние и все, что от них получено.
Безопасно опустить функцию из списка зависимостей только в том случае, если в ней (или функциях, вызываемых ею) ничего не ссылается на props, состояние или значения, полученные от них. В этом примере есть ошибка:
function ProductPage({ productId }) {
const [product, setProduct] = useState(null);
async function fetchProduct() {
const response = await fetch('http://myapi/product/' + productId); // Uses productId prop
const json = await response.json();
setProduct(json);
}
useEffect(() => {
fetchProduct();
}, []); // 🔴 Invalid because `fetchProduct` uses `productId`
// ...
} Рекомендуемое исправление — перемещение этой функции внутрь вашего эффекта. Это упрощает определение используемых props или состояния, и гарантирует, что они все объявлены:
function ProductPage({ productId }) {
const [product, setProduct] = useState(null);
useEffect(() => {
// By moving this function inside the effect, we can clearly see the values it uses.
async function fetchProduct() {
const response = await fetch('http://myapi/product/' + productId);
const json = await response.json();
setProduct(json);
}
fetchProduct();
}, [productId]); // ✅ Valid because our effect only uses productId
// ...
} Это также позволяет обрабатывать ответы в неправильном порядке с помощью локальной переменной внутри эффекта:
useEffect(() => {
let ignore = false;
async function fetchProduct() {
const response = await fetch('http://myapi/product/' + productId);
const json = await response.json();
if (!ignore) setProduct(json);
}
fetchProduct();
return () => { ignore = true };
}, [productId]); Мы перенесли функцию внутрь эффекта, поэтому она не должна быть в списке зависимостей.
Совет
Посмотрите данную демонстрацию и эту статью, чтобы узнать больше о получении данных с помощью хуков.
Если по какой-то причине вы не можете переместить функцию внутрь эффекта, есть несколько дополнительных вариантов:
- Вы можете попробовать переместить эту функцию за пределы вашего компонента. В этом случае функция гарантированно не будет ссылаться на какие-либо props или состояние, и ей также не нужно быть в списке зависимостей.
- Если вызываемая вами функция является чистым вычислением и безопасно вызывается во время отрисовки, вы можете вызвать её вне эффекта, а сделать эффект зависимым от возвращаемого значения.
- В качестве крайнего средства, вы можете добавить функцию в зависимости эффекта, но оборатить её определение в
useCallbackхук. Это гарантирует, что она не изменится при каждой отрисовке, если не изменятся собственные зависимости:
function ProductPage({ productId }) {
// ✅ Wrap with useCallback to avoid change on every render
const fetchProduct = useCallback(() => {
// ... Does something with productId ...
}, [productId]); // ✅ All useCallback dependencies are specified
return <ProductDetails fetchProduct={fetchProduct} />;
}
function ProductDetails({ fetchProduct }) {
useEffect(() => {
fetchProduct();
}, [fetchProduct]); // ✅ All useEffect dependencies are specified
// ...
} Обратите внимание, что в приведенном выше примере нам необходимо сохранить функцию в списке зависимостей. Это гарантирует, что изменение productId prop компонента ProductPage автоматически запускает повторный запрос в компоненте ProductDetails.
Что делать, если зависимости моего эффекта меняются слишком часто?
Иногда ваш эффект может использовать состояние, которое меняется слишком часто. Вы можете захотеть опустить это состояние из списка зависимостей, но это обычно приводит к ошибкам:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // This effect depends on the `count` state
}, 1000);
return () => clearInterval(id);
}, []); // 🔴 Bug: `count` is not specified as a dependency
return <h1>{count}</h1>;
} Пустой набор зависимостей, [], означает, что эффект будет выполняться только один раз при подключении компонента, а не при каждой отрисовке. Проблема в том, что внутри обратного вызова setInterval, значение count не меняется, потому что мы создали замыкание со значением count установленным в 0, как было при выполнении обратного вызова эффекта. Каждую секунду этот обратный вызов вызывает setCount(0 + 1), поэтому счетчик никогда не поднимается выше 1.
Указание [count] в качестве списка зависимостей исправит ошибку, но приведет к сбросу интервала при каждом изменении. По существу, каждый setInterval получит один шанс на выполнение, прежде чем будет очищен (подобно setTimeout). Это может быть нежелательно. Чтобы исправить это, мы можем использовать функциональную форму обновления setState.
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1); // ✅ This doesn't depend on `count` variable outside
}, 1000);
return () => clearInterval(id);
}, []); // ✅ Our effect doesn't use any variables in the component scope
return <h1>{count}</h1>;
} (Идентификатор функции setCount гарантированно стабилен, поэтому его безопасно опустить.)
Теперь обратный вызов setInterval выполняется раз в секунду, но каждый раз внутренний вызов setCount может использовать актуальное значение для count (называемое c в обратном вызове здесь).
В более сложных случаях (например, если одно состояние зависит от другого), попробуйте перенести логику обновления состояния за пределы эффекта с помощью useReducer хука. Эта статья предлагает пример того, как это сделать. Идентификатор функции dispatch из useReducer всегда стабилен — даже если функция редуктора объявлена внутри компонента и считывает его props.
В качестве крайнего средства, если вам нужно что-то вроде this в классе, вы можете использовать ref для хранения изменяемой переменной. Затем вы можете записывать и считывать в неё. Например:
function Example(props) {
// Keep latest props in a ref.
const latestProps = useRef(props);
useEffect(() => {
latestProps.current = props;
});
useEffect(() => {
function tick() {
// Read latest props at any time
console.log(latestProps.current);
}
const id = setInterval(tick, 1000);
return () => clearInterval(id);
}, []); // This effect never re-runs
} Делайте это только в том случае, если не нашли лучшей альтернативы, так как полагание на мутацию делает компоненты менее предсказуемыми. Если есть конкретный шаблон, который плохо переводится, отправьте запрос с демонстрационным кодом, и мы попробуем помочь.
Как реализовать shouldComponentUpdate?
Вы можете обернуть функциональный компонент с помощью React.memo для поверхностного сравнения его props:
const Button = React.memo((props) => {
// your component
}); Это не хук, потому что он не композирует как хуки. React.memo эквивалентно PureComponent, но сравнивает только props. (Вы также можете добавить второй аргумент для указания пользовательской функции сравнения, которая принимает старые и новые props. Если она возвращает true, обновление пропускается.)
React.memo не сравнивает состояние, потому что нет единственного объекта состояния для сравнения. Но вы можете сделать дочерние элементы чистыми или даже оптимизировать отдельные дочерние элементы с помощью useMemo.
Как кешировать вычисления?
Хук useMemo позволяет кешировать вычисления между несколькими отрисовками, «запоминая» предыдущее вычисление:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
Этот код вызывает computeExpensiveValue(a, b). Но если зависимости [a, b] не изменились с момента последнего значения, useMemo пропускает повторный вызов и просто использует последнее возвращённое значение.
Помните, что функция, переданная в useMemo, выполняется во время отрисовки. Не делайте там ничего, что вы обычно не делали бы при отрисовке. Например, побочные эффекты должны быть в useEffect, а не в useMemo.
Вы можете полагаться на useMemo в качестве оптимизации производительности, а не как на семантическую гарантию. В будущем React может «забыть» некоторые ранее кешированные значения и перевычислить их при следующей отрисовке, например, для освобождения памяти для компонентов, не отображаемых на экране. Напишите код так, чтобы он работал и без useMemo, а затем добавьте его для оптимизации производительности. (В редких случаях, когда значение никогда не должно перевычисляться, вы можете инициализировать его лениво.)
Удобно, useMemo также позволяет пропустить дорогостоящую переотрисовку дочернего элемента:
function Parent({ a, b }) {
// Only re-rendered if `a` changes:
const child1 = useMemo(() => <Child1 a={a} />, [a]);
// Only re-rendered if `b` changes:
const child2 = useMemo(() => <Child2 b={b} />, [b]);
return (
<>
{child1}
{child2}
</>
)
} Обратите внимание, что этот подход не будет работать в цикле, потому что вызовы хуков не могут быть помещены внутрь циклов. Но вы можете выделить отдельный компонент для элемента списка и вызвать useMemo там.
Как создавать дорогостоящие объекты лениво?
useMemo позволяет кешировать дорогостоящее вычисление, если зависимости одинаковы. Однако это только подсказка, и не гарантирует, что вычисление не будет выполняться повторно. Но иногда вам нужно быть уверенным, что объект создаётся только один раз.
Первый распространённый случай — когда создание начального состояния дорогостоящее:
function Table(props) {
// ⚠️ createRows() is called on every render
const [rows, setRows] = useState(createRows(props.count));
// ...
} Чтобы избежать повторного создания проигнорированного начального состояния, мы можем передать функцию в useState:
function Table(props) {
// ✅ createRows() is only called once
const [rows, setRows] = useState(() => createRows(props.count));
// ...
} React вызовет эту функцию только во время первой отрисовки. См. useState ссылку на API.
Иногда вы также можете захотеть избежать повторного создания начального значения useRef(). Например, может быть, вам нужно убедиться, что экземпляр какого-то императивного класса создаётся только один раз:
function Image(props) {
// ⚠️ IntersectionObserver is created on every render
const ref = useRef(new IntersectionObserver(onIntersect));
// ...
} useRef не принимает специальную перегрузку функции, подобную useState. Вместо этого вы можете написать свою собственную функцию, которая создаёт и устанавливает её лениво:
function Image(props) {
const ref = useRef(null);
// ✅ IntersectionObserver is created lazily once
function getObserver() {
if (ref.current === null) {
ref.current = new IntersectionObserver(onIntersect);
}
return ref.current;
}
// When you need it, call getObserver()
// ...
} Это позволяет избежать создания дорогостоящего объекта до тех пор, пока он действительно не потребуется впервые. Если вы используете Flow или TypeScript, вы также можете указать getObserver() непустой тип для удобства.
Замедлены ли хуки из-за создания функций во время отрисовки?
Нет. В современных браузерах существенной разницы в производительности замыканий по сравнению с классами нет, за исключением крайних случаев.
Кроме того, учтите, что дизайн хуков более эффективен по нескольким причинам:
- Хуки избегают большого количества накладных расходов, необходимых для классов, например, затрат на создание экземпляров классов и привязки обработчиков событий в конструкторе.
- Идиоматичный код, использующий хуки, не нуждается в глубокой вложенности дерева компонентов, которая характерна для кода, использующего высшие компоненты, render props и контекст. С меньшими древовидными структурами компонентов React выполняет меньше работы.
Традиционно проблемы производительности, связанные с функциями во время отрисовки в React, связаны с тем, как передача новых обратных вызовов при каждой отрисовке нарушает оптимизации shouldComponentUpdate в дочерних компонентах. Хуки подходят к этой проблеме с трёх сторон.
-
Крючок
useCallbackпозволяет сохранять одну и ту же ссылку на коллбэк между перерендерами, чтобыshouldComponentUpdateпродолжал работать:// Will not change unless `a` or `b` changes const memoizedCallback = useCallback(() => { doSomething(a, b); }, [a, b]); - Крючок
useMemoупрощает управление тем, когда отдельные дочерние компоненты обновляются, уменьшая необходимость в чистых компонентах. - Наконец, крючок
useReducerуменьшает необходимость передачи коллбэков глубоко, как объяснено ниже.
Как избежать передачи коллбэков вниз?
Мы обнаружили, что большинство людей не любят вручную передавать коллбэки через каждый уровень дерева компонентов. Несмотря на то, что это более явно, это может показаться большим количеством «трубопроводов».
В больших деревьях компонентов альтернативой, которую мы рекомендуем, является передача функции dispatch из useReducer через контекст:
const TodosDispatch = React.createContext(null);
function TodosApp() {
// Note: `dispatch` won't change between re-renders
const [todos, dispatch] = useReducer(todosReducer);
return (
<TodosDispatch.Provider value={dispatch}>
<DeepTree todos={todos} />
</TodosDispatch.Provider>
);
} Любой дочерний элемент в дереве внутри TodosApp может использовать функцию dispatch для передачи действий вверх к TodosApp:
function DeepChild(props) {
// If we want to perform an action, we can get dispatch from context.
const dispatch = useContext(TodosDispatch);
function handleClick() {
dispatch({ type: 'add', text: 'hello' });
}
return (
<button onClick={handleClick}>Add todo</button>
);
} Это более удобно с точки зрения обслуживания (нет необходимости постоянно передавать коллбэки) и полностью избегает проблемы с коллбэками. Передача dispatch вниз таким образом — рекомендуемая схема для глубоких обновлений.
Обратите внимание, что вы по-прежнему можете выбирать, передавать ли прикладное состояние вниз как свойства (более явно) или как контекст (более удобно для очень глубоких обновлений). Если вы также используете контекст для передачи состояния вниз, используйте два разных типа контекста — контекст dispatch никогда не меняется, поэтому компоненты, которые его читают, не нуждаются в перерендеринге, если им также не нужно прикладное состояние.
Как получить часто изменяемое значение из useCallback?
Примечание
Мы рекомендуем передавать
dispatchв контексте, а не отдельные коллбэки в свойствах. Приведённый ниже подход упоминается здесь только для полноты и в качестве выхода.
В некоторых редких случаях вам может потребоваться мемоизировать коллбэк с помощью useCallback, но мемоизация не работает очень хорошо, потому что внутренняя функция должна создаваться слишком часто. Если функция, которую вы мемоизируете, является обработчиком событий и не используется во время рендеринга, вы можете использовать ref как экземпляр переменной и вручную сохранить последнее применённое значение в неё:
function Form() {
const [text, updateText] = useState('');
const textRef = useRef();
useEffect(() => {
textRef.current = text; // Write it to the ref
});
const handleSubmit = useCallback(() => {
const currentText = textRef.current; // Read it from the ref
alert(currentText);
}, [textRef]); // Don't recreate handleSubmit like [text] would do
return (
<>
<input value={text} onChange={e => updateText(e.target.value)} />
<ExpensiveTree onSubmit={handleSubmit} />
</>
);
} Это довольно запутанная схема, но она показывает, что вы можете сделать эту оптимизацию выхода, если вам это нужно. Она более приемлема, если вы вынесете её в пользовательский крючок:
function Form() {
const [text, updateText] = useState('');
// Will be memoized even if `text` changes:
const handleSubmit = useEventCallback(() => {
alert(text);
}, [text]);
return (
<>
<input value={text} onChange={e => updateText(e.target.value)} />
<ExpensiveTree onSubmit={handleSubmit} />
</>
);
}
function useEventCallback(fn, dependencies) {
const ref = useRef(() => {
throw new Error('Cannot call an event handler while rendering.');
});
useEffect(() => {
ref.current = fn;
}, [fn, ...dependencies]);
return useCallback(() => {
const fn = ref.current;
return fn();
}, [ref]);
} В любом случае, мы не рекомендуем эту схему и показываем её здесь только для полноты. Вместо этого предпочтительнее избегать передачи коллбэков глубоко вниз.
Под капотом
Как React связывает вызовы крючков с компонентами?
React отслеживает компонент, который в настоящее время рендерится. Благодаря Правилам крючков, мы знаем, что крючки вызываются только из компонентов React (или пользовательских крючков — которые также вызываются только из компонентов React).
Есть внутренний список «ячеек памяти», связанных с каждым компонентом. Это просто объекты JavaScript, куда мы можем поместить какие-то данные. Когда вы вызываете крючок, например, useState(), он считывает текущую ячейку (или инициализирует её во время первого рендеринга), а затем перемещает указатель на следующую. Так у каждого вызова useState() получается независимое локальное состояние.
Какой был опыт до крючков?
Крючки синтезируют идеи из нескольких разных источников:
- Наши старые эксперименты с функциональными API в хранилище react-future.
- Эксперименты сообщества React с API render prop, включая эксперименты Райана Флоренса с компонентом Reactions.
-
Предложение Доминика Ганнавея по ключевому слову
adoptв качестве синтаксического сахара для render props. - Переменные состояния и ячейки состояния в DisplayScript.
- Компоненты-редукторы в ReasonReact .
- Подписки в Rx .
- Алгебраические эффекты в Multicore OCaml .
Себастьян Маркбåге разработал исходный дизайн для крючков, позднее усовершенствованный Эндрю Кларком, Софи Альперт, Домиником Ганнавеем и другими членами команды React.
Полезная ли эта страница?
© 2013–present Facebook Inc.
Licensed under the Creative Commons Attribution 4.0 International Public License.
https://17.reactjs.org/docs/hooks-faq.html