Реагируйте на пользовательские хуки и хуки useMemo
У меня есть две "дорогие" функции, которые я хотел бы запомнить в своем приложении для реагирования, так как на их рендеринг уходит много времени. В карте массива используются дорогостоящие функции. Я хотел бы запомнить каждый результат карты массива, чтобы при изменении одного элемента массива были пересчитаны только дорогостоящие функции этого элемента. (И чтобы независимо запоминать дорогостоящие функции, поскольку иногда требуется пересчитать только одну.) Я борюсь с тем, как запоминать и передавать текущее значение массива.
Вот рабочая демонстрация без мемоизации:
import React, { useMemo, useState } from "react";
const input = ["apple", "banana", "cherry", "durian", "elderberry", "apple"];
export default function App() {
const output = input.map((msg, key) => createItem(msg, key));
return <div className="App">{output}</div>;
}
const createItem = (message, key) => { // expensive function 1
console.log("in createItem:", key, message);
return (
<div key={key}>
<strong>{"Message " + (1 + key)}: </strong>
{message} =>{" "}
<span
id={"preview-" + key}
dangerouslySetInnerHTML={{ __html: msgSub(message) }}
/>
</div>
);
};
const msgSub = message => { // expensive function 2
const messageSub = message.replace(/[a]/g, "A").replace(/[e]/g, "E");
console.log("in msgSub:", message, messageSub);
return messageSub;
};
(У меня он не работает в редакторе SO, поэтому просмотрите и запустите его в codeandbox.)
Вот одна из моих попыток использования пользовательских хуков и хуков useMemo.
Приветствуются любые рекомендации!
И бонусные баллы за иллюстрацию того, как реагировать на работу в редакторе SO!
2 ответа
Моим первым шагом было изменить createItem
так что Item был его единственным функциональным компонентом. Это означало, что я мог запомнить<Item/>
компонент, чтобы он отображался только в случае изменения реквизита, что важно для сообщения и ключа / индекса (как вы ранее отображали значение).
Рабочий пример этого https://codesandbox.io/s/funny-chaplygin-pvfoj.
const Item = React.memo(({ message, index }) => {
console.log("Rendering:", index, message);
const calculatedMessage = msgSub(message);
return (
<div>
<strong>{"Message " + (1 + index)}: </strong>
{message} =>{" "}
<span
id={"preview-" + index}
dangerouslySetInnerHTML={{ __html: calculatedMessage }}
/>
</div>
);
});
const msgSub = message => {
// expensive function 2
const messageSub = message.replace(/[a]/g, "A").replace(/[e]/g, "E");
console.log("in msgSub:", message, messageSub);
return messageSub;
};
Вы можете видеть, что при первоначальном рендеринге он отображает все элементы с их message
но особенно apple
дважды.
Если два компонента визуализируются независимо друг от друга и используют одни и те же свойства, эти два компонента будут визуализированы. React.memo не сохраняет рендеры компонентов.
Он должен отображать компонент Item дважды <Item message="apple" />
, однажды для apple
при индексе 0 и снова при индексе 5 apple
.
Вы заметили, что я разместил кнопку в приложении, при нажатии на которую изменяется содержимое массива.
Немного изменив исходный массив, я поместил carrot
с индексом 4 в исходном массиве и переместил его в индекс 2 в новом массиве, когда я его обновил.
const [stringArray, setStringArray] = React.useState([
"apple",
"banana",
"cherry",
"durian",
"carrot",
"apple" // duplicate Apple
]);
const changeArray = () =>
setStringArray([
"apple",
"banana",
"carrot", // durian removed, carrot moved from index 4 to index 2
"dates", // new item
"elderberry", // new item
"apple"
]);
Если вы посмотрите на консоль, вы увидите, что при первом рендере in msgSub: carrot cArrot
но когда мы обновляем массив, in msgSub: carrot cArrot
вызывается снова. Это потому, что ключ на<Item />
поэтому он вынужден повторно выполнить рендеринг. Это правильно, потому что наш ключ основан на индексе, а позиция моркови изменилась. Однако вы сказалиmsgSub
это дорогая функция...
Местная мемоизация
Я заметил в вашем массиве у вас apple
дважды.
const input = ["яблоко", "банан", "вишня", "дуриан", "бузина", "яблоко"];
Я почувствовал, что вы хотите запомнить расчет, чтобы apple
не рассчитывалась заново, если apple
был рассчитан ранее.
Мы можем сохранить вычисленное значение в нашем собственном состоянии мемоизации, чтобы мы могли найти значение сообщения и посмотреть, рассчитывали ли мы его ранее.
const [localMemoization, setLocalMemoization] = useState({});
Мы хотим, чтобы мы обновляли эту localMemoization, когда stringArray
изменения.
React.useEffect(() => {
setLocalMemoization(prevState =>
stringArray.reduce((store, value) => {
const calculateValue =
prevState[value] ?? store[value] ?? msgSub(value);
return store[value] ? store : { ...store, [value]: calculateValue };
})
);
}, [stringArray]);
Эта линия const calculateValue = prevState[value] ?? store[value] ?? msgSub(value);
- проверяет предыдущее состояние, чтобы увидеть, имело ли оно предыдущее значение (для случая, когда морковь перемещает первый массив во второй массив).
- проверяет текущий магазин, чтобы узнать, есть ли в нем значение (для второго случая яблока).
- наконец, если его никто не видел, мы используем дорогостоящую функцию для вычисления значения в первый раз.
Используя ту же логику, что и в предыдущем рендере, мы теперь ищем вычисленное значение и передаем его в <Item>
так что React.memo предотвратит повторную визуализацию этого компонента, если приложение снова будет отображать.
const output = stringArray.map((msg, key) => {
const expensiveMessage = localMemoization[msg];
return (
<Item
key={key}
message={msg}
calculatedValue={expensiveMessage}
index={key}
/>
);
});
Рабочий пример здесь https://codesandbox.io/s/attempt-with-custom-hooks-and-usememo-y3lm5?file=/src/App.js:759-1007
Отсюда в консоли мы видим, что при первом рендеринге apple
рассчитывается только один раз.
При изменении массива мы не вычисляем carrot
снова и только измененные элементы dates
а также elderberry
.
Сделайте элемент чистым компонентом:
const id = ((id) => () => id++)(1); //IIFE creating id
//pure component
const Item = React.memo(function Item({ increase, item }) {
console.log('rendering id:', item.id);
return (
<ul>
<button onClick={() => increase(item.id)}>
{item.count}
</button>
</ul>
);
});
const App = () => {
const [items, setItems] = React.useState([]);
//use callback, increase only created on app mount
const increase = React.useCallback(
(id) =>
setItems((items) =>
//use state setter callback
//no dependencies for useCallback and no stale
//closures
items.map((item) =>
item.id === id
? { ...item, count: item.count + 1 }
: item
)
),
[] //no dependencies
);
return (
<div>
<div>
<button
onClick={() =>
setItems((items) => [
{ id: id(), count: 0 },
...items,
])
}
>
add counter
</button>
</div>
<ul>
{items.map((item) => (
<Item
key={item.id}
item={item}
increase={increase}
/>
))}
</ul>
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>