Как сравнить oldValues ​​и newValues ​​в React Hooks useEffect?

151

Допустим, у меня есть 3 входа: скорость, sendAmount и receiveAmount. Я поставил эти 3 входа на использование различных параметров. Правила таковы:

  • Если sendAmount изменился, я рассчитываю receiveAmount = sendAmount * rate
  • Если полученная сумма изменилась, я рассчитываю sendAmount = receiveAmount / rate
  • Если ставка изменилась, я рассчитываю receiveAmount = sendAmount * rateкогда sendAmount > 0или я вычисляю sendAmount = receiveAmount / rateкогдаreceiveAmount > 0

Вот коды и окно https://codesandbox.io/s/pkl6vn7x6j, чтобы продемонстрировать проблему.

Есть ли способ сравнить oldValuesи newValuesнравится componentDidUpdateвместо 3 обработчиков для этого случая?

Спасибо


Вот мое окончательное решение с usePrevious https://codesandbox.io/s/30n01w2r06

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

rwinzhang
источник
Как именно компонент componentDidUpdate должен помочь? Вам все равно нужно будет написать эти 3 условия.
Настой Эстус
Я добавил ответ с двумя дополнительными решениями. Один из них работает на вас?
Бен Карп

Ответы:

210

Вы можете написать собственный хук, чтобы предоставить вам предыдущий реквизит, используя useRef

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

а затем использовать его в useEffect

const Component = (props) => {
    const {receiveAmount, sendAmount } = props
    const prevAmount = usePrevious({receiveAmount, sendAmount});
    useEffect(() => {
        if(prevAmount.receiveAmount !== receiveAmount) {

         // process here
        }
        if(prevAmount.sendAmount !== sendAmount) {

         // process here
        }
    }, [receiveAmount, sendAmount])
}

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

Шубхам Хатри
источник
8
Спасибо за примечание об использовании двух useEffectвызовов отдельно. Не знал, что вы можете использовать его несколько раз!
Джейсон
3
Я попробовал приведенный выше код, но Эслинт предупреждает меня о том, что useEffectотсутствуют зависимостиprevAmount
Littlee
2
Поскольку prevAmount содержит значение для предыдущего значения state / props, вам не нужно передавать его как зависимость для использованияEffect, и вы можете отключить это предупреждение для конкретного случая. Вы можете прочитать этот пост для более подробной информации?
Шубхам Хатри
Любите это - useRef всегда приходит на помощь.
Фернандо Рохо,
@ShubhamKhatri: В чем причина использования useEffect для ref.current = value?
curly_brackets
40

Incase кто-нибудь ищет версию использования TypeScriptPrevious:

import { useEffect, useRef } from "react";

const usePrevious = <T extends {}>(value: T): T | undefined => {
  const ref = useRef<T>();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
};
SeedyROM
источник
2
Обратите внимание, что это не будет работать в файле TSX, чтобы заставить его работать в файле TSX, измените его, const usePrevious = <T extends any>(...чтобы заставить интерпретатора думать, что <T> не является JSX и является общим ограничением
apokryfos
1
Можете ли вы объяснить, почему <T extends {}>не будет работать. Кажется, это работает для меня, но я пытаюсь понять сложность такого использования.
Karthikeyan_kk
.tsxфайлы содержат jsx, который должен быть идентичным html. Возможно, что универсальный <T>является незакрытым тегом компонента.
SeedyROM
Что насчет типа возврата? Я получаюMissing return type on function.eslint(@typescript-eslint/explicit-function-return-type)
Саба Аханг
@SabaAhang Извините! TS будет использовать вывод типов для обработки возвращаемых типов по большей части, но если вы включили linting, я добавил возвращаемый тип, чтобы избавиться от предупреждений.
SeedyROM
25

Вариант 1 - использовать крючок

Сравнение текущего значения с предыдущим значением является распространенным шаблоном и оправдывает собственную собственную ловушку, которая скрывает детали реализации.

const useCompare = (val: any) => {
    const prevVal = usePrevious(val)
    return prevVal !== val
}

const usePrevious = (value) => {
    const ref = useRef();
    useEffect(() => {
      ref.current = value;
    });
    return ref.current;
}

const Component = (props) => {
  ...
  const hasVal1Changed = useCompare(val1)
  const hasVal2Changed = useCompare(val2);
  useEffect(() => {
    if (hasVal1Changed ) {
      console.log("val1 has changed");
    }
    if (hasVal2Changed ) {
      console.log("val2 has changed");
    }
  });

  return <div>...</div>;
};

демонстрация

Вариант 2 - запустить useEffect при изменении значения

const Component = (props) => {
  ...
  useEffect(() => {
    console.log("val1 has changed");
  }, [val1]);
  useEffect(() => {
    console.log("val2 has changed");
  }, [val2]);

  return <div>...</div>;
};

демонстрация

Бен Карп
источник
Второй вариант сработал для меня. Можете ли вы объяснить мне, почему я должен написать useEffect Twice?
Тарун Нагпал
@TarunNagpal, вам не обязательно использовать useEffect дважды. Это зависит от вашего варианта использования. Представьте, что мы просто хотим записать, какой val изменился. Если в нашем массиве зависимостей есть и val1, и val2, он будет запускаться каждый раз, когда изменяется любое из значений. Затем внутри функции, которую мы передаем, мы должны выяснить, какой val изменился, чтобы записать правильное сообщение.
Бен Карп
Спасибо за ответ. Когда вы говорите: «внутри функции, которую мы пропустили, нам нужно выяснить, какое значение изменилось». Как мы можем этого добиться. Пожалуйста, руководство
Тарун Нагпал
6

Я только что опубликовал ответную дельту, которая решает именно этот сценарий. На мой взгляд, useEffectслишком много обязанностей.

обязанности

  1. Он сравнивает все значения в своем массиве зависимостей, используя Object.is
  2. Он запускает обратные вызовы эффекта / очистки, основанные на результате # 1

Распределение обязанностей

react-deltaразбивает useEffectобязанности на несколько более мелких зацепок.

Ответственность № 1

Ответственность № 2

По моему опыту, этот подход более гибкий, чистый и лаконичный, чем useEffect/ useRefрешения.

Остин Малерба
источник
Как раз то, что я ищу. Собираюсь попробовать!
hackerl33t
выглядит действительно хорошо, но не слишком ли это излишне?
Хисато
@Hisato это очень хорошо может быть излишним. Это что-то вроде экспериментального API. И, честно говоря, я мало использовал его в своих командах, потому что он не широко известен и не принят. Теоретически это звучит неплохо, но на практике это может не стоить того.
Остин Малерба
3

Поскольку состояние не тесно связано с экземпляром компонента в функциональных компонентах, предыдущее состояние не может быть достигнуто useEffectбез предварительного его сохранения, например, с помощью useRef. Это также означает, что обновление состояния, возможно, было неправильно выполнено в неправильном месте, потому что предыдущее состояние доступно внутриsetState функции обновления.

Это хороший пример использования, useReducerкоторый предоставляет Redux-подобное хранилище и позволяет реализовать соответствующий шаблон. Обновления состояний выполняются явно, поэтому нет необходимости выяснять, какое свойство состояния обновляется; это уже ясно из отправленных действий.

Вот пример того, как это может выглядеть:

function reducer({ sendAmount, receiveAmount, rate }, action) {
  switch (action.type) {
    case "sendAmount":
      sendAmount = action.payload;
      return {
        sendAmount,
        receiveAmount: sendAmount * rate,
        rate
      };
    case "receiveAmount":
      receiveAmount = action.payload;
      return {
        sendAmount: receiveAmount / rate,
        receiveAmount,
        rate
      };
    case "rate":
      rate = action.payload;
      return {
        sendAmount: receiveAmount ? receiveAmount / rate : sendAmount,
        receiveAmount: sendAmount ? sendAmount * rate : receiveAmount,
        rate
      };
    default:
      throw new Error();
  }
}

function handleChange(e) {
  const { name, value } = e.target;
  dispatch({
    type: name,
    payload: value
  });
}

...
const [state, dispatch] = useReducer(reducer, {
  rate: 2,
  sendAmount: 0,
  receiveAmount: 0
});
...
Настой Эстус
источник
Привет @estus, спасибо за эту идею. Это дает мне другой способ мышления. Но я забыл упомянуть, что мне нужно вызывать API для каждого случая. У вас есть какое-то решение для этого?
rwinzhang
Что вы имеете в виду? Внутри ручки изменить? «API» означает удаленную конечную точку API? Можете ли вы обновить коды и коробки из ответа с подробностями?
Настой Эстус
Из обновления я могу предположить, что вы хотите получать sendAmountи т. Д. Асинхронно, а не вычислять их, верно? Это сильно меняется. Это возможно сделать, useReduceно может быть сложно. Если вы имели дело с Redux до того, как узнаете, что асинхронные операции не так просты. github.com/reduxjs/redux-thunk - это популярное расширение для Redux, которое позволяет выполнять асинхронные действия. Вот демонстрация, которая расширяет useReducer с тем же шаблоном (useThunkReducer), codesandbox.io/s/6z4r79ymwr . Обратите внимание, что отправленная функция есть async, вы можете делать запросы там (закомментировано).
Настой Эстус
1
Проблема с вопросом заключается в том, что вы спрашивали о другой вещи, которая не была вашей реальной ситуацией, а затем изменили ее, так что теперь это совсем другой вопрос, в то время как существующие ответы выглядят так, как будто они просто проигнорировали вопрос. Это не рекомендуемая практика для SO, потому что она не поможет вам получить желаемый ответ. Пожалуйста, не удаляйте важные части из вопроса, на который уже есть ответы (формулы расчета). Если у вас возникли проблемы (после моего предыдущего комментария (вы , вероятно , сделать), рассмотреть вопрос задать новый вопрос , который отражает ваше дело и ссылку на этот , как предыдущую попытку.
Estus колбу
Привет, Эстус, я думаю, что коды коды и коробки кодыandbox.io/ s/ 6z4r79ymwr еще не обновлены. Я вернусь к вопросу, извините за это.
Рвинжанг
3

Использование Ref внесет новый вид ошибок в приложение.

Давайте посмотрим на этот случай, используя usePreviousто, что кто-то прокомментировал ранее:

  1. prop.minTime: 5 ==> ref.current = 5 | установить ref.current
  2. prop.minTime: 5 ==> ref.current = 5 | новое значение равно ref.current
  3. prop.minTime: 8 ==> ref.current = 5 | новое значение НЕ равно ref.current
  4. prop.minTime: 5 ==> ref.current = 5 | новое значение равно ref.current

Как мы видим здесь, мы не обновляем внутреннее, refпотому что мы используемuseEffect

santomegonzalo
источник
1
У React есть этот пример, но они используют state... а не props... когда вы заботитесь о старых подпорках, тогда произойдет ошибка.
santomegonzalo
3

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

const myComponent = ({ prop }) => {
  useEffect(() => {
     ---Do stuffhere----
    }
  }, [prop])

}

Затем useEffect будет запускать ваш код только при изменении реквизита.

Чарли Тупман
источник
1
Это работает только один раз, после последовательных propизменений вы не сможете~ do stuff here ~
Каролис Шарапницкис
2
Похоже, вы должны позвонить setHasPropChanged(false)в конце, ~ do stuff here ~чтобы "сбросить" ваше состояние. (Но это сбросило бы в дополнительном повторном рендере)
Кевин Ван
Спасибо за отзыв, вы оба правы, обновленное решение
Чарли Тупман
@ AntonioPavicevac-Ortiz Я обновил ответ и теперь отображаю propHasChanged как true, который затем будет вызывать его один раз при рендеринге. Это может быть лучшим решением, если просто вырвать useEffect и просто проверить реквизит
Чарли Тупман
Я думаю, что мое первоначальное использование этого было потеряно. Оглядываясь назад на код, вы можете просто использовать useEffect
Чарли
2

Отход от принятого ответа, альтернативное решение, которое не требует пользовательского хука:

const Component = (props) => {
  const { receiveAmount, sendAmount } = props;
  const prevAmount = useRef({ receiveAmount, sendAmount }).current;
  useEffect(() => {
    if (prevAmount.receiveAmount !== receiveAmount) {
     // process here
    }
    if (prevAmount.sendAmount !== sendAmount) {
     // process here
    }
    return () => { 
      prevAmount.receiveAmount = receiveAmount;
      prevAmount.sendAmount = sendAmount;
    };
  }, [receiveAmount, sendAmount]);
};
Дражен Бьеловук
источник
1
Хорошее решение, но содержит синтаксические ошибки. useRefне userRef. Забыл использовать токprevAmount.current
Блейк Пламб
0

Вот пользовательский хук, который я использую, который я считаю более интуитивным, чем использование usePrevious.

import { useRef, useEffect } from 'react'

// useTransition :: Array a => (a -> Void, a) -> Void
//                              |_______|  |
//                                  |      |
//                              callback  deps
//
// The useTransition hook is similar to the useEffect hook. It requires
// a callback function and an array of dependencies. Unlike the useEffect
// hook, the callback function is only called when the dependencies change.
// Hence, it's not called when the component mounts because there is no change
// in the dependencies. The callback function is supplied the previous array of
// dependencies which it can use to perform transition-based effects.
const useTransition = (callback, deps) => {
  const func = useRef(null)

  useEffect(() => {
    func.current = callback
  }, [callback])

  const args = useRef(null)

  useEffect(() => {
    if (args.current !== null) func.current(...args.current)
    args.current = deps
  }, deps)
}

Вы бы использовали useTransitionследующим образом.

useTransition((prevRate, prevSendAmount, prevReceiveAmount) => {
  if (sendAmount !== prevSendAmount || rate !== prevRate && sendAmount > 0) {
    const newReceiveAmount = sendAmount * rate
    // do something
  } else {
    const newSendAmount = receiveAmount / rate
    // do something
  }
}, [rate, sendAmount, receiveAmount])

Надеюсь, это поможет.

Аадит М Шах
источник