ИспользоватьMemo или не использоватьMemo?
Вы могли заметить, что наша кнопка CoolButton неправильно отображает знак (+):
<CoolButton clickHandler={increment}>+</CoolButton>
Когда мы вставляем что-либо внутрь Jsx-элемента, например CoolButton, мы не показываем это пользователю, а передаем в качестве параметра под названием children.
const CoolButton = React.memo(({ clickHandler,children }) => {
const handler = () => {
ReallyImportantCalculation();
clickHandler();
};
return <button onClick={handler}></button>;
});
Вместо того чтобы ничего не делать, давайте отобразим дочерние элементы:
return <button onClick={handler}>{children}</button>;
Как и раньше, давайте добавим немного сложности в наш дизайн.
Вместо того чтобы показывать пользователю знак (+), давайте создадим игру “Кликер”, которая будет состоять из кнопки, меняющей свой внешний вид в зависимости от количества нажатий на нее.
Мы можем начать с передачи <img/>
вместо знака (+) в наш CoolButton:
<CoolButton clickHandler={increment}>
<img/>
</CoolButton>
При нажатии на кнопку мы замечаем, что наша мемоизация была снова потеряна; повторное отображение кнопки при каждом нажатии…
Давайте вспомним, что в JSX <img/>
не является html-тегом, это сокращение для React.createElement('img',props, ...children)
.
Превращаем наш код в:
{createElement(CoolButton,{clickHandler:increment},
createElement('img',null, null)
)}
Теперь легко понять, в чем именно проблема: выполнение createElement при каждом рендере создает нового ребенка, который передается нашей CoolButton в качестве prop.
Сначала нам нужно убрать создание дочернего элемента из CoolButton:
const CurrentImage = <img/>;
<CoolButton clickHandler={increment}>
{CurrentImage}
</CoolButton>
У вас может возникнуть соблазн поместить CurrentImage вне нашего счетчика, что сработает, но поскольку CurrentImage будет иметь состояние, основанное на нашем счетчике, мы должны использовать другой способ:
const CurrentImage = useCallback(<img/>,[]);
<CoolButton clickHandler={increment}>
{CurrentImage}
</CoolButton>
Как и раньше, на помощь приходит useCallback!
Хотя это выглядит немного странно, так как наш CurrentImage на самом деле не обратный вызов, а значение, которое мы хотим запомнить.
useMemo
(Из документации React)
useMemo, как и useCallback, принимает функцию, которая что-то мемоизирует, и массив зависимостей, который перезапускает эту функцию только при изменении зависимостей, в нашем случае мы хотим мемоизировать JsxElement.
Как мы уже говорили, реквизит Children, который мы передаем нашей CoolButton, меняется при каждом рендере, потому что мы каждый раз создаем новое изображение CurrentImage.
Мы можем использоватьMemo для запоминания CurrentImage и предотвращения повторных рендеров:
const CurrentImage = useMemo(() => <img/>,[]);
<CoolButton clickHandler={increment}>
{CurrentImage}
</CoolButton>
Чтобы сделать это немного интереснее, давайте добавим новое состояние phaseImgUrl, которое сообщит нам, какое изображение мы должны представить для каждой фазы нашего кликера:
const [phaseImgUrl, setPhaseImgUrl] = useState('');
const CurrentImage = useMemo(() => <img src={phaseImgUrl}/>,[phaseImgUrl]);
<CoolButton clickHandler={increment}>
{CurrentImage}
</CoolButton>
Вот некоторая дополнительная логика, которая будет обрабатывать изменение фаз при достижении определенного порога:
const phases = [
"https://media4.giphy.com...phase1",
"https://media4.giphy.com...phase2",
"https://media4.giphy.com...phase3",
];
useEffect(() => {
if (count != null) {
const phaseThreshold = 30;
const numPhases = phases.length;
const nextPhaseImgUrl =
phases[parseInt(count / phaseThreshold, 10) % numPhases];
if (nextPhaseImgUrl !== phaseImgUrl) {
setPhaseImgUrl(nextPhaseImgUrl);
}
}
}, [count]);
Сначала мы проверяем, действителен ли счетчик, а затем важно убедиться, что фаза отличается от предыдущей, чтобы не вызывать лишних setStates и повторных рендеров.
Вот и все!
В следующей части мы поговорим о дополнительных эффектах и связанных с ними опасностях.