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

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

  • Если sendAmount изменился, я рассчитываю receiveAmount = sendAmount * rate
  • Если значение receiveAmount изменилось, я рассчитываю 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 также полезно для отслеживания изменений только с локального компьютера, поэтому я могу предотвратить ненужные сетевые вызовы из-за изменений с сервера.


person rwinzhang    schedule 23.11.2018    source источник
comment
Как именно componentDidUpdate должен помочь? Вам все равно нужно будет записать эти 3 условия.   -  person Estus Flask    schedule 23.11.2018
comment
Я добавил ответ с двумя дополнительными решениями. Один из них вам подходит?   -  person Ben Carp    schedule 29.06.2019


Ответы (12)


Вы можете написать собственный хук, чтобы предоставить вам предыдущие реквизиты с использованием 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 отдельно для каждого идентификатора изменения, который вы хотите обрабатывать их отдельно.

person Shubham Khatri    schedule 23.11.2018
comment
Благодарим за замечание об использовании двух вызовов useEffect по отдельности. Не знал, что вы можете использовать его несколько раз! - person JasonH; 22.01.2019
comment
Я пробовал приведенный выше код, но eslint предупреждает меня, что useEffect отсутствуют зависимости prevAmount - person Littlee; 20.05.2019
comment
Поскольку prevAmount содержит значение для предыдущего значения state / props, вам не нужно передавать его как зависимость для useEffect, и вы можете отключить это предупреждение для конкретного случая. Вы можете прочитать этот пост для более подробной информации? - person Shubham Khatri; 20.05.2019
comment
Люблю это - useRef всегда приходит на помощь. - person Fernando Rojo; 16.07.2019
comment
@ShubhamKhatri: В чем причина обертывания useEffect ref.current = value? - person curly_brackets; 30.01.2020
comment
Ссылки @curly_brackets не изменяют ссылки между рендерами, и поскольку мы изменяем объект ref, устанавливая ключ current, если он не вызывает повторного рендеринга, тем самым обеспечивая предыдущее значение во время следующего повторного рендеринга - person Shubham Khatri; 30.01.2020
comment
также хорошо объяснено здесь blog.logrocket.com / - person IsmailS; 01.02.2020
comment
Будет ли && работать или будет запускаться useEffect один раз для каждой переменной в разное время? if (prevAmount.receiveAmount !== receiveAmount && prevAmount.sendAmount !== sendAmount) - person Ryan Walker; 18.02.2020
comment
В usePrevious, разве useEffect не должен зависеть от value? В противном случае, если компонент будет повторно отрисован из-за другого изменения состояния, в следующем рендере previousValue будет равно value, верно? Или я что-то упускаю? - person azizj; 20.03.2020
comment
это круто! но это не сработает только в первый раз, когда используется usePrevious? - person Ido Bleicher; 26.04.2020
comment
react становится все более и более ужасным: люди используют useRef только для получения предыдущих реквизитов (!) И не заботятся о том, сколько это будет стоить. - person puchu; 01.06.2021
comment
Черт, это похоже на колдовство. Красивый. - person djfm; 28.06.2021

Если кто-то ищет версию использования TypeScript Назад:

В модуле .tsx:

import { useEffect, useRef } from "react";

const usePrevious = <T extends unknown>(value: T): T | undefined => {
  const ref = useRef<T>();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
};

Или в модуле .ts:

import { useEffect, useRef } from "react";

const usePrevious = <T>(value: T): T | undefined => {
  const ref = useRef<T>();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
};
person SeedyROM    schedule 29.08.2019
comment
Обратите внимание, что это не будет работать в файле TSX, чтобы заставить это работать в файле TSX, измените значение на const usePrevious = <T extends any>(..., чтобы интерпретатор увидел, что ‹T› не JSX и является общим ограничением. - person apokryfos; 23.10.2019
comment
Вы можете объяснить, почему <T extends {}> не работает. Кажется, это работает для меня, но я пытаюсь понять сложность его использования. - person Karthikeyan_kk; 12.12.2019
comment
.tsx файлы содержат jsx, который должен быть идентичен html. Возможно, общий <T> является незакрытым тегом компонента. - person SeedyROM; 12.12.2019
comment
А как насчет типа возврата? Я получаю Missing return type on function.eslint(@typescript-eslint/explicit-function-return-type) - person Saba Ahang; 21.02.2020
comment
@SabaAhang Извините! TS будет использовать вывод типов для обработки возвращаемых типов по большей части, но если у вас включен линтинг, я добавил возвращаемый тип, чтобы избавиться от предупреждений. - person SeedyROM; 09.03.2020
comment
@SabaAhang Я настоятельно рекомендую использовать VSCode для работы TS, также если вы удерживаете контроль и нажимаете на имена функций, вы получаете их ТОЧНЫЕ типы, поэтому такие вещи тривиально найти. Языковой сервер TS отлично справляется с документами и подписями типов. - person SeedyROM; 09.03.2020
comment
Лучше расширить unknown или просто поместить ловушку в .ts файл. Если вы расширите {}, вы получите ошибки, если пропустите указание T в usePrevious<T>. - person fgblomqvist; 19.08.2020
comment
@fgblomqvist С момента написания этого поста я многое узнал о TS. Мне нужно обновить это, чтобы отразить это. Спасибо за комментарий! - person SeedyROM; 19.08.2020
comment
np, и да, всегда приятно держать его свежим / правильным / актуальным, поскольку многие тысячи людей используют эти ответы как ссылки :) - person fgblomqvist; 19.08.2020
comment
@fgblomqvist Обновил мой ответ. Еще раз спасибо за отзыв. - person SeedyROM; 20.08.2020
comment
Чтобы он работал в файле TSX, вы также можете написать ‹T,› для отличия от синтаксиса JSX (сфокусируйтесь на запятой в конце) - person Corvince; 05.05.2021

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

const Component = (props) => {

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

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

Демо

Вариант 2 - хук useHasChanged

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

const Component = (props) => {
  const hasVal1Changed = useHasChanged(val1)

  useEffect(() => {
    if (hasVal1Changed ) {
      console.log("val1 has changed");
    }
  });

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

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

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


Демо

person Ben Carp    schedule 29.06.2019
comment
Второй вариант у меня сработал. Не могли бы вы объяснить мне, почему я должен писать useEffect Twice? - person Tarun Nagpal; 27.03.2020
comment
@TarunNagpal, вам не обязательно использовать useEffect дважды. Это зависит от вашего варианта использования. Представьте, что мы просто хотим записать, какая val изменилась. Если у нас есть как val1, так и val2 в нашем массиве зависимостей, он будет запускаться каждый раз, когда любое из значений изменится. Затем внутри функции, которую мы передаем, нам нужно будет выяснить, какой val изменился, чтобы записать правильное сообщение. - person Ben Carp; 28.03.2020
comment
Спасибо за ответ. Когда вы говорите внутри функции, которую мы передаем, нам нужно будет выяснить, у какого val есть изменения. Как мы можем этого добиться. Пожалуйста, направьте - person Tarun Nagpal; 29.03.2020
comment
Спасибо, вариант 2 - именно то, что мне нужно - person bFunc; 07.02.2021

Исходя из принятого ответа, альтернативного решения, не требующего настраиваемого крючка:

const Component = ({ receiveAmount, sendAmount }) => {
  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]);
};

Это предполагает, что вам действительно нужна ссылка на предыдущие значения для чего-либо в битах процесса. В противном случае, если ваши условные выражения не выходят за рамки прямого !== сравнения, самым простым решением здесь будет:

const Component = ({ receiveAmount, sendAmount }) => {
  useEffect(() => {
     // process here
  }, [receiveAmount]);

  useEffect(() => {
     // process here
  }, [sendAmount]);
};
person Drazen Bjelovuk    schedule 08.04.2020
comment
Хорошее решение, но содержит синтаксические ошибки. useRef не userRef. Забыл использовать текущий prevAmount.current - person Blake Plumb; 28.04.2020
comment
Это супер круто, если бы я мог проголосовать больше, я бы сделал это! Также имеет смысл, мой первоначальный ответ пришел из незнания. Я думаю, что это, наверное, самый простой и элегантный узор, который я когда-либо видел. - person SeedyROM; 13.10.2020
comment
Вы, вероятно, могли бы абстрагироваться от этого еще дальше, чтобы сделать сверхпростую утилиту. - person SeedyROM; 13.10.2020
comment
Это даст вам начальные значения, а не предыдущие значения. Причина, по которой хук usePrevious() работает, заключается в том, что он заключен в useEffect() - person Justin; 23.06.2021
comment
@Justin Функция возврата должна обновлять предыдущие значения для каждого рендера. Вы это проверяли? - person Drazen Bjelovuk; 24.06.2021

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

Обязанности

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

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

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

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

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

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

person Austin Malerba    schedule 12.12.2019
comment
Именно то, что я ищу. Собираюсь попробовать! - person hackerl33t; 24.04.2020
comment
@Hisato, это могло бы быть излишним. Это что-то вроде экспериментального API. И, честно говоря, я мало использовал его в своих командах, потому что он не получил широкой известности и не принят. В теории это звучит неплохо, но на практике оно того не стоит. - person Austin Malerba; 10.07.2020

Если вы предпочитаете подход замены useEffect:

const usePreviousEffect = (fn, inputs = []) => {
  const previousInputsRef = useRef([...inputs])

  useEffect(() => {
    fn(previousInputsRef.current)
    previousInputsRef.current = [...inputs]
  }, inputs)
}

И используйте это так:

usePreviousEffect(
  ([prevReceiveAmount, prevSendAmount]) => {
    if (prevReceiveAmount !== receiveAmount) // side effect here
    if (prevSendAmount !== sendAmount) // side effect here
  },
  [receiveAmount, sendAmount]
)

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

person Joe Van Leeuwen    schedule 12.10.2020

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

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

useEffect будет запускать ваш код только в том случае, если свойство изменится.

person Charlie Tupman    schedule 20.09.2019
comment
Это работает только один раз, после последовательных prop изменений вы не сможете ~ do stuff here ~ - person Karolis Šarapnickis; 10.10.2019
comment
Похоже, вам следует позвонить setHasPropChanged(false) в конце ~ do stuff here ~, чтобы сбросить состояние. (Но это будет сброшено при дополнительном повторном рендеринге) - person Kevin Wang; 16.10.2019
comment
Спасибо за отзыв, вы оба правы, обновленное решение - person Charlie Tupman; 02.06.2020
comment
@ AntonioPavicevac-Ortiz Я обновил ответ, чтобы теперь отображать propHasChanged как true, который затем вызывает его один раз при рендеринге, может быть лучшим решением, просто вырвать useEffect и просто проверить опору - person Charlie Tupman; 11.06.2020
comment
Я думаю, что мое первоначальное использование этого было потеряно. Оглядываясь назад на код, вы можете просто использовать useEffect - person Charlie Tupman; 11.06.2020
comment
Это просто и умно. Почему я не подумал об этом, смеется :) Хорошая работа. - person Jay Mayu; 19.07.2020

Поскольку состояние не тесно связано с экземпляром компонента в функциональных компонентах, предыдущее состояние не может быть достигнуто в 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
});
...
person Estus Flask    schedule 23.11.2018
comment
Привет @estus, спасибо за идею. Это дает мне другой образ мышления. Но я забыл упомянуть, что мне нужно вызывать API для каждого случая. У вас есть какое-нибудь решение для этого? - person rwinzhang; 23.11.2018
comment
Что ты имеешь в виду? Внутренняя ручка Означает ли «API» удаленную конечную точку API? Можете ли вы обновить коды и ящик из ответа с подробностями? - person Estus Flask; 23.11.2018
comment
Из обновления я могу предположить, что вы хотите получать sendAmount и т. Д. Асинхронно, а не вычислять их, верно? Это многое меняет. Это можно сделать с помощью useReduce, но это может быть сложно. Если вы имели дело с Redux раньше, вы можете знать, что асинхронные операции там не просты. github.com/reduxjs/redux-thunk - популярное расширение для Redux, которое позволяет выполнять асинхронные действия. . Вот демонстрация, которая дополняет useReducer тем же шаблоном (useThunkReducer), codeandbox.io/s/6z4r79ymwr. Обратите внимание, что отправленная функция - это async, вы можете делать запросы там (прокомментировано). - person Estus Flask; 23.11.2018
comment
Проблема с вопросом в том, что вы спросили о другом, что не было вашим реальным случаем, а затем изменили его, так что теперь это совсем другой вопрос, в то время как существующие ответы выглядят так, как будто они просто проигнорировали вопрос. Это не рекомендуется для SO, потому что это не поможет вам получить желаемый ответ. Пожалуйста, не удаляйте важные части из вопроса, на который уже есть ответы (формулы расчета). Если у вас все еще есть проблемы (после моего предыдущего комментария (а вы, вероятно, так и есть), подумайте о том, чтобы задать новый вопрос, который отражает ваш случай, и укажите ссылку на этот вопрос в качестве своей предыдущей попытки. - person Estus Flask; 23.11.2018
comment
Привет, я думаю, что этот код и ящик codeandbox.io/s/6z4r79ymwr еще не обновлен. Я верну вопрос обратно, извините за это. - person rwinzhang; 24.11.2018
comment
Да, он не сохранялся автоматически после разветвления. Я его обновил. - person Estus Flask; 24.11.2018

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

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

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

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

person santomegonzalo    schedule 28.11.2019
comment
В React есть этот пример, но они используют _1 _..., а не _2 _..., когда вы заботитесь о старых реквизитах, тогда произойдет ошибка. - person santomegonzalo; 28.11.2019

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

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])

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

person Aadit M Shah    schedule 08.05.2020

Вы можете использовать useImmer вместо useState и получить доступ к состоянию. Пример: https://css-tricks.com/build-a-chat-app-using-react-hooks-in-100-lines-of-code/

person joemillervi    schedule 18.10.2020

В вашем случае (простой объект):

useEffect(()=>{
  // your logic
}, [rate, sendAmount, receiveAmount])

В другом случае (сложный объект)

const {cityInfo} = props;
useEffect(()=>{
  // some logic
}, [cityInfo.cityId])
person JChen___    schedule 28.12.2020