Определите, какая переменная массива зависимостей вызвала срабатывание хука useEffect

Есть ли простой способ определить, какая переменная в массиве зависимостей useEffect запускает повторный запуск функции?

Простой выход из системы для каждой переменной может ввести в заблуждение, если a - функция, а b - объект, они могут выглядеть одинаково при регистрации, но на самом деле отличаться друг от друга и вызывать срабатывание useEffect.

Например:

React.useEffect(() => {
  // which variable triggered this re-fire?
  console.log('---useEffect---')
}, [a, b, c, d])

Мой текущий метод удаляет переменные зависимости одну за другой, пока я не заметил поведение, которое вызывает чрезмерные вызовы useEffect, но должен быть лучший способ сузить это.


person Cumulo Nimbus    schedule 15.03.2019    source источник


Ответы (5)


В итоге я взял немного из разных ответов, чтобы сделать свой собственный крючок для этого. Я хотел иметь возможность просто отбросить что-нибудь вместо useEffect для быстрой отладки, какая зависимость запускает useEffect.

const usePrevious = (value, initialValue) => {
  const ref = useRef(initialValue);
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
};
const useEffectDebugger = (effectHook, dependencies, dependencyNames = []) => {
  const previousDeps = usePrevious(dependencies, []);

  const changedDeps = dependencies.reduce((accum, dependency, index) => {
    if (dependency !== previousDeps[index]) {
      const keyName = dependencyNames[index] || index;
      return {
        ...accum,
        [keyName]: {
          before: previousDeps[index],
          after: dependency
        }
      };
    }

    return accum;
  }, {});

  if (Object.keys(changedDeps).length) {
    console.log('[use-effect-debugger] ', changedDeps);
  }

  useEffect(effectHook, dependencies);
};

Ниже приведены два примера. Для каждого примера я предполагаю, что dep2 изменяется с «foo» на «bar». В примере 1 показан результат без передачи dependencyNames, а в примере 2 показан пример с dependencyNames.

Пример 1

До:

useEffect(() => {
  // useEffect code here... 
}, [dep1, dep2])

После:

useEffectDebugger(() => {
  // useEffect code here... 
}, [dep1, dep2])

Вывод в консоль:

{
  1: {
    before: 'foo',
    after: 'bar'
  }
}

Ключ объекта «1» представляет собой индекс зависимости, которая изменилась. Здесь dep1 изменилось и является вторым элементом в зависимости или индексом 1.

Пример 2

До:

useEffect(() => {
  // useEffect code here... 
}, [dep1, dep2])

После:

useEffectDebugger(() => {
  // useEffect code here... 
}, [dep1, dep2], ['dep1', 'dep2'])

Вывод в консоль:

{
  dep2: {
    before: 'foo',
    after: 'bar'
  }
}
person Brad Ryan    schedule 21.01.2020
comment
Вы должны опубликовать это в NPM! - person goldylucks; 29.08.2020
comment
Это круто. - person Ryan; 31.08.2020

Эта библиотека ... @simbathesailor/use-what-changed , работает как очарование!

  1. Install с npm/yarn и --dev или --no-save
  2. Добавить импорт:
import { useWhatChanged } from '@simbathesailor/use-what-changed';
  1. Назови это:
// (guarantee useEffect deps are in sync with useWhatChanged)
let deps = [a, b, c, d]

useWhatChanged(deps, 'a, b, c, d');
useEffect(() => {
  // your effect
}, deps);

Создает эту красивую диаграмму в консоли:

image loaded from github

Есть два распространенных виновника:

  1. Некоторые объекты проходят так:
// Being used like:
export function App() {
  return <MyComponent fetchOptions={{
    urlThing: '/foo',
    headerThing: 'FOO-BAR'
  })
}
export const MyComponent = ({fetchOptions}) => {
  const [someData, setSomeData] = useState()
  useEffect(() => {
    window.fetch(fetchOptions).then((data) => {
      setSomeData(data)
    })

  }, [fetchOptions])

  return <div>hello {someData.firstName}</div>
}

Исправление в случае объекта, если вы можете, разбить статический объект за пределы рендеринга компонента:

const fetchSomeDataOptions = {
  urlThing: '/foo',
  headerThing: 'FOO-BAR'
}
export function App() {
  return <MyComponent fetchOptions={fetchSomeDataOptions} />
}

Вы также можете обернуть useMemo:

export function App() {
  return <MyComponent fetchOptions={
    useMemo(
      () => {
        return {
          urlThing: '/foo',
          headerThing: 'FOO-BAR',
          variableThing: hash(someTimestamp)
        }
      },
      [hash, someTimestamp]
    )
  } />
}

Та же самая концепция применима к функциям до некоторой степени, за исключением того, что вы можете получить устаревшие закрытия.

person Devin Rhode    schedule 10.09.2020
comment
(Точка означает, что значение не изменилось. Зеленая галочка означает, что оно действительно изменилось.) Есть даже плагин babel (серьезно, помните, этот проект, ребята!) github.com/simbathesailor/use-what-changed - person Devin Rhode; 11.09.2020
comment
я не знаю почему, но он ничего не регистрирует для меня - person Jamil Alisgenderov; 19.10.2020
comment
@JamilAlisgenderov Я думаю, что useWhatChanged должен использовать console.table ... поэтому, если вы пытаетесь протестировать в старом браузере, который не поддерживает console.table, вы можете проверить, определена ли console.table. Вы также можете проверить обычный console.log («что-то изменилось», «таблица определена?», !! console.table); внутри ваших журналов хуков useEffect. В противном случае ... возможно, сообщите о проблеме на github с помощью вашей версии реакции + браузера - person Devin Rhode; 19.10.2020
comment
@JamilAlisgenderov Вы когда-нибудь выясняли, из-за чего функция use-what-changed не регистрирует для вас ничего? - person Devin Rhode; 10.12.2020

ОБНОВЛЕНИЕ

После небольшого использования в реальной жизни мне пока нравится следующее решение, которое заимствует некоторые аспекты решения Retsam:

const compareInputs = (inputKeys, oldInputs, newInputs) => {
  inputKeys.forEach(key => {
    const oldInput = oldInputs[key];
    const newInput = newInputs[key];
    if (oldInput !== newInput) {
      console.log("change detected", key, "old:", oldInput, "new:", newInput);
    }
  });
};
const useDependenciesDebugger = inputs => {
  const oldInputsRef = useRef(inputs);
  const inputValuesArray = Object.values(inputs);
  const inputKeysArray = Object.keys(inputs);
  useMemo(() => {
    const oldInputs = oldInputsRef.current;

    compareInputs(inputKeysArray, oldInputs, inputs);

    oldInputsRef.current = inputs;
  }, inputValuesArray); // eslint-disable-line react-hooks/exhaustive-deps
};

Затем это можно использовать, скопировав литерал массива зависимостей и просто изменив его на литерал объекта:

useDependenciesDebugger({ state1, state2 });

Это позволяет журналу знать имена переменных без какого-либо отдельного параметра для этой цели.

Изменить useDependenciesDebugger

person Ryan Cogswell    schedule 15.03.2019
comment
Мне тоже нравится этот ответ. По сравнению с моим ответом, нужно настроить на бит больше работы, но это даст лучший результат, поскольку каждая зависимость получает имя, тогда как мой только говорит, какой индекс изменился. - person Retsam; 15.03.2019
comment
Вы можете переключиться с удержания ссылки true и false на удержание null и {prevValue: value}, если вы хотите регистрировать старое значение, а также новое значение при его изменении. - person Retsam; 15.03.2019

Насколько я знаю, нет действительно простого способа сделать это из коробки, но вы можете добавить собственный хук, который отслеживает его зависимости и регистрирует, какие из них были изменены:

// Same arguments as useEffect, but with an optional string for logging purposes
const useEffectDebugger = (func, inputs, prefix = "useEffect") => {
  // Using a ref to hold the inputs from the previous run (or same run for initial run
  const oldInputsRef = useRef(inputs);
  useEffect(() => {
    // Get the old inputs
    const oldInputs = oldInputsRef.current;

    // Compare the old inputs to the current inputs
    compareInputs(oldInputs, inputs, prefix)

    // Save the current inputs
    oldInputsRef.current = inputs;

    // Execute wrapped effect
    func()
  }, inputs);
};

Бит compareInputs может выглядеть примерно так:

const compareInputs = (oldInputs, newInputs, prefix) => {
  // Edge-case: different array lengths
  if(oldInputs.length !== newInputs.length) {
    // Not helpful to compare item by item, so just output the whole array
    console.log(`${prefix} - Inputs have a different length`, oldInputs, newInputs)
    console.log("Old inputs:", oldInputs)
    console.log("New inputs:", newInputs)
    return;
  }

  // Compare individual items
  oldInputs.forEach((oldInput, index) => {
    const newInput = newInputs[index];
    if(oldInput !== newInput) {
      console.log(`${prefix} - The input changed in position ${index}`);
      console.log("Old value:", oldInput)
      console.log("New value:", newInput)
    }
  })
}

Вы можете использовать это так:

useEffectDebugger(() => {
  // which variable triggered this re-fire?
  console.log('---useEffect---')
}, [a, b, c, d], 'Effect Name')

И вы получите такой результат:

Effect Name - The input changed in position 2
Old value: "Previous value"
New value: "New value"
person Retsam    schedule 15.03.2019

Существует еще один поток переполнения стека, в котором говорится, что вы можете использовать useRef для просмотра предыдущего значения.

https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state.

person arcanereinz    schedule 30.09.2019
comment
Может быть полезно разместить ссылку на другую ветку StackOverflow. - person jknotek; 01.10.2019