#javascript #reactjs #react-hooks
#javascript #reactjs #реагирующие хуки
Вопрос:
Обычно effect
of React.useEffect
активируется только, если значения в deps
списке изменяются.
Я хочу обратного. Я хочу, чтобы он был активирован, если значения в списке не изменятся.
Кстати, я знаю, что обертки вокруг chart.js уже существует.
Но люди просили конкретный пример, так что вот он.
Например:
import React, { useState, useEffect, memo, useMemo } from "react";
import merge from "lodash/merge";
import {
Chart as ChartJS,
ChartType,
ChartData,
ChartOptions,
ChartConfiguration,
} from "chart.js";
type ChartInstance = InstanceType<typeof ChartJS> | null;
export type ChartProps = Readonly<ChartData> amp;
Readonly<ChartOptions> amp; {
readonly type?: ChartType;
readonly height?: number | string;
};
export const Chart = memo<ChartProps>(
({
type = "line",
height = "20em",
responsive = true,
maintainAspectRatio = false,
animation = { duration: 0 },
hover = { animationDuration: 0 },
responsiveAnimationDuration = 0,
labels = [],
datasets = [],
legend: { display = false, ...legend } = {},
...rest
}) => {
const [canvas, ref] = useState<HTMLCanvasElement | null>(null);
let chart: ChartInstance = null;
const options: ChartConfiguration = {
type,
data: { labels, datasets },
options: {
responsive,
maintainAspectRatio,
animation,
hover,
responsiveAnimationDuration,
legend: { display, ...legend },
...rest,
},
};
const destroy = () => {
if (chart) {
chart.destroy();
chart = null;
}
};
chart = useMemo<ChartInstance>(() => {
destroy();
return canvas ? new ChartJS(canvas, options) : null;
}, [canvas, type]);
useEffect(() => {
if (chart) {
merge(chart, options).update();
}
});
useEffect(() => destroy, []);
return (
<div style={{ position: "relative", height }}>
<canvas ref={ref} />
</div>
);
}
);
Нет смысла обновлять диаграмму, если мы ее только что создали.
Если useMemo
выполняется, то useEffect
следующее за ним не должно. В противном случае его необходимо выполнять каждый раз.
Компонент завернут в React.memo
, поэтому, если происходит рендеринг, это означает, что некоторые реквизиты диаграммы изменены, и диаграмму необходимо либо воссоздать, либо обновить.
Комментарии:
1.
useLayoutEffect
может быть? Я не думаю, что вам следует использоватьuseEffect
хук для вашего решения2. @ZombieChowder в
useLayoutEffect
аргументеdeps
работает точно так же3. Является ли переменная значением prop или state?
4. Можете ли вы привести конкретный вариант использования?
5. Вопрос также в том, хотите ли вы запускать его при каждом рендеринге или после каждого рендеринга, если только … есть несколько способов решить эту проблему, но вариант использования не совсем ясен.
Ответ №1:
Не существует простого способа вернуть useEffect
поведение проверки зависимостей. Это можно сделать, но это было бы ненужным взломом, особенно если вы не используете функцию очистки.
В вашем случае самый простой способ — ввести переменную ( shouldUpdateChart
), которая будет определять, должен ли useEffect
выполняться код. Если выполняется код внутри useMemo
, его значение равно false
, в противном случае это true
:
let shouldUpdateChart = true;
chart = useMemo < ChartInstance > (() => {
shouldUpdateChart = false;
destroy();
return canvas ? new ChartJS(canvas, options) : null;
}, [canvas, type]);
useEffect(() => {
if (shouldUpdateChart) {
merge(chart, options).update();
}
});
Комментарии:
1. Иногда самый простой / лучший вариант также является наименее очевидным, лол. Спасибо!
2. Я понимаю, почему существующие библиотеки сейчас не используют функциональный компонент для этого 🙂 Я думал, что смогу сделать его более элегантным вот так. Но очистка становится кошмаром, поскольку вы неизбежно сталкиваетесь с циклическими зависимостями. Излишне говорить, что это не лучший подход.
Ответ №2:
Одним из вариантов было бы сохранить некоторую внешнюю ссылку на предыдущее значение переменной, запускать эффект при каждом рендеринге и просто отключаться, если эта переменная меняется. Вот пример того, как это может работать. Вы могли бы абстрагировать эту логику в свой собственный пользовательский хук.
let prevA;
export default function App() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
useEffect(() => {
if (a !== prevA) {
prevA = a;
return;
}
console.log("a did not change");
});
return (
<div className="App">
<button
onClick={() => {
setA(a 1);
}}
>
Increment A
</button>
<button
onClick={() => {
setB(b 1);
}}
>
Increment B
</button>
</div>
);
}
Комментарии:
1. это то, чего я пытаюсь избежать. Мой фактический компонент имеет две
deps
переменные. Это означает, что мне придется использоватьuseState
4 раза только для этого.2. useState был только для моего примера. Вам не нужно использовать useState для хранения ссылок на существующие зависимости, просто создайте переменную вне области действия вашего компонента.
3. это моя ошибка, на самом деле, я должен был опубликовать полный пример с самого начала. Я попытался объяснить это словами…