ReactJS использует эффект, противоположный deps

#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. это моя ошибка, на самом деле, я должен был опубликовать полный пример с самого начала. Я попытался объяснить это словами…