Как сравнить 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 обработчика для этого случая?

Спасибо


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

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

Как именно componentDidUpdate должен помочь? Вам все равно нужно будет написать эти 3 условия.

Estus Flask 23.11.2018 13:05

Я добавил ответ с двумя дополнительными решениями. Один из них вам подходит?

Ben Carp 29.06.2019 14:45
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Навигация по приложениям React: Исчерпывающее руководство по React Router
Навигация по приложениям React: Исчерпывающее руководство по React Router
React Router стала незаменимой библиотекой для создания одностраничных приложений с навигацией в React. В этой статье блога мы подробно рассмотрим...
Массив зависимостей в React
Массив зависимостей в React
Все о массиве Dependency и его связи с useEffect.
283
2
235 919
13
Перейти к ответу Данный вопрос помечен как решенный

Ответы 13

Ответ принят как подходящий

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

Благодарим за замечание об использовании двух вызовов useEffect по отдельности. Не знал, что можно использовать его несколько раз!

JasonH 22.01.2019 16:24

Я пробовал приведенный выше код, но eslint предупреждает меня, что у useEffect отсутствуют зависимости prevAmount

Littlee 20.05.2019 11:53

Поскольку prevAmount содержит значение для предыдущего значения state / props, вам не нужно передавать его как зависимость для useEffect, и вы можете отключить это предупреждение для конкретного случая. Вы можете прочитать этот пост для более подробной информации?

Shubham Khatri 20.05.2019 12:10

Люблю это - useRef всегда приходит на помощь.

Fernando Rojo 16.07.2019 18:29

@ShubhamKhatri: В чем причина обертывания useEffect ref.current = value?

curly_brackets 30.01.2020 07:26

Ссылки @curly_brackets не изменяют ссылки между рендерингами, и поскольку мы изменяем объект ref, устанавливая ключ current, если он не вызывает повторного рендеринга, тем самым обеспечивая предыдущее значение во время следующего повторного рендеринга

Shubham Khatri 30.01.2020 12:28

также хорошо объяснено здесь blog.logrocket.com/…

IsmailS 31.01.2020 22:10

Будет ли && работать или будет запускаться useEffect один раз для каждой переменной в разное время? if (prevAmount.receiveAmount !== receiveAmount && prevAmount.sendAmount !== sendAmount)

Ryan Walker 17.02.2020 22:56

В usePrevious, разве useEffect не должен зависеть от value? В противном случае, если компонент будет повторно визуализирован из-за другого изменения состояния, в следующем рендере previousValue будет равен value, верно? Или я что-то упускаю?

azizj 19.03.2020 22:31

это круто! но это не сработает только в первый раз, когда используется usePrevious?

Ido Bleicher 26.04.2020 18:21
react становится все более ужасным: люди используют useRef только для получения предыдущих реквизитов (!) И не заботятся о том, сколько это будет стоить.
puchu 01.06.2021 00:40

Черт, это похоже на колдовство. Красивый.

djfm 28.06.2021 17:09

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

Kavindu VIndika 15.08.2021 03:52

Нет необходимости использовать функцию usePrevious (), если код в функции Component должен быть таким, как указано выше. Итак, установка значения useRef в функции Component useEffect выполнит свою работу. const Component = (реквизиты) => {const {someProp} = реквизиты; const ref = useRef (); // значение ref не изменится при изменении состояния. useEffect (() => {if (ref.current! == someProp) {// ... ref.current = someProp;}}, [someProp]); }

Chirag Dave 31.10.2021 05:06

Поскольку состояние не тесно связано с экземпляром компонента в функциональных компонентах, предыдущее состояние не может быть достигнуто в 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 handleChangeEvent(e) {
  const { name, value } = e.target;
  dispatch({
    type: name,
    payload: value
  });
}

...
const [state, dispatch] = useReducer(reducer, {
  rate: 2,
  sendAmount: 0,
  receiveAmount: 0
});
...

Привет @estus, спасибо за идею. Это дает мне другой образ мышления. Но я забыл упомянуть, что мне нужно вызывать API для каждого случая. У вас есть какое-нибудь решение?

Erwin Zhang 23.11.2018 14:05

Что ты имеешь в виду? Внутренняя ручка Означает ли «API» удаленную конечную точку API? Можете ли вы обновить коды из ответа с подробностями?

Estus Flask 23.11.2018 14:13

Из обновления я могу предположить, что вы хотите получать sendAmount и т. д. Асинхронно, а не вычислять их, верно? Это многое меняет. Это можно сделать с useReduce, но это может быть сложно. Если вы имели дело с Redux раньше, вы можете знать, что асинхронные операции там не просты. github.com/reduxjs/redux-thunk - популярное расширение для Redux, которое позволяет выполнять асинхронные действия. Вот демонстрация, которая дополняет useReducer тем же шаблоном (useThunkReducer), codeandbox.io/s/6z4r79ymwr. Обратите внимание, что отправленная функция - это async, вы можете делать запросы там (прокомментировано).

Estus Flask 23.11.2018 16:54

Проблема с вопросом в том, что вы спросили о другом, что не было вашим реальным случаем, а затем изменили его, так что теперь это совсем другой вопрос, в то время как существующие ответы выглядят так, как будто они просто проигнорировали вопрос. Это не рекомендуется для SO, потому что это не поможет вам получить желаемый ответ. Пожалуйста, не удаляйте важные части из вопроса, ответы на который уже относятся (формулы расчета). Если у вас все еще есть проблемы (после моего предыдущего комментария (а вы, вероятно, так и есть), подумайте о том, чтобы задать новый вопрос, который отражает ваш случай, и укажите ссылку на него как на свою предыдущую попытку.

Estus Flask 23.11.2018 17:05

Привет, эстус, я думаю этот кодовый ящик codeandbox.io/s/6z4r79ymwr еще не обновлен. Я верну вопрос обратно, извините за это.

Erwin Zhang 24.11.2018 05:17

Да, после разветвления автоматически не сохранялось. Я его обновил.

Estus Flask 24.11.2018 07:22

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

const Component = (props) => {

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

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

Демо

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

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

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

  useEffect(() => {
    if (hasVal1Changed ) {
      console.info("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;
}


Демо

Второй вариант у меня сработал. Не могли бы вы объяснить мне, почему я должен писать useEffect Twice?

Tarun Nagpal 27.03.2020 17:51

@TarunNagpal, вам не обязательно использовать useEffect дважды. Это зависит от вашего варианта использования. Представьте, что мы просто хотим записать, какая val изменилась. Если у нас есть как val1, так и val2 в нашем массиве зависимостей, он будет запускаться каждый раз, когда любое из значений изменится. Затем внутри функции, которую мы передаем, нам нужно будет выяснить, какой val был изменен, чтобы записать правильное сообщение.

Ben Carp 28.03.2020 17:39

Спасибо за ответ. Когда вы говорите «внутри функции, которую мы передаем, нам нужно выяснить, у какого val есть изменения». Как мы можем этого добиться. Пожалуйста, направьте

Tarun Nagpal 29.03.2020 19:54

Спасибо, вариант 2 - именно то, что мне нужно

bFunc 07.02.2021 21:13

На случай, если кто-то ищет версию использования 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;
};

Обратите внимание, что это не будет работать в файле TSX, чтобы заставить это работать, измените файл TSX на const usePrevious = <T extends any>(..., чтобы интерпретатор увидел, что <T> не JSX и является общим ограничением

apokryfos 23.10.2019 14:23

Вы можете объяснить, почему <T extends {}> не работает. Кажется, это работает для меня, но я пытаюсь понять сложность его использования таким образом.

Karthikeyan_kk 12.12.2019 10:58

Файлы .tsx содержат jsx, который должен быть идентичен html. Возможно, универсальный <T> является незакрытым тегом компонента.

SeedyROM 12.12.2019 21:55

А как насчет возвращаемого типа? Я получаю Missing return type on function.eslint(@typescript-eslint/explicit-function-return-‌​type)

Saba Ahang 21.02.2020 18:59

@SabaAhang Извини! TS будет использовать вывод типа для обработки возвращаемых типов по большей части, но если у вас включен линтинг, я добавил тип возвращаемого значения, чтобы избавиться от предупреждений.

SeedyROM 09.03.2020 19:42

@SabaAhang Я настоятельно рекомендую использовать VSCode для работы TS, также, если вы удерживаете контроль и нажимаете на имена функций, вы получаете их ТОЧНЫЕ типы, поэтому такие вещи тривиально найти. Языковой сервер TS отлично справляется с документами и подписями типов.

SeedyROM 09.03.2020 19:45

Лучше расширить unknown или просто поместить ловушку в файл .ts. Если вы расширите {}, вы получите ошибки, если пропустите указание T в usePrevious<T>.

fgblomqvist 19.08.2020 19:52

@fgblomqvist С момента написания этого поста я многое узнал о TS. Мне нужно обновить это, чтобы отразить это. Спасибо за комментарий!

SeedyROM 19.08.2020 19:53

np, и да, всегда приятно держать его свежим / правильным / актуальным, так как многие тысячи людей используют эти ответы как ссылки :)

fgblomqvist 19.08.2020 20:26

@fgblomqvist Обновил мой ответ. Еще раз спасибо за отзыв.

SeedyROM 20.08.2020 00:50

Чтобы он работал в файле TSX, вы также можете написать <T,>, чтобы отличаться от синтаксиса JSX (сфокусируйтесь на конечной запятой)

Corvince 05.05.2021 16:40

@Corvince Действует ли это в более старых версиях tsc, не думаю, что я видел это

SeedyROM 02.11.2021 09:45

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

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

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

Это работает только один раз, после последовательных изменений prop вы не сможете использовать ~ do stuff here ~.

Karolis Šarapnickis 10.10.2019 09:34

Похоже, вам следует вызвать в setHasPropChanged(false) в конце ~ do stuff here ~, чтобы «сбросить» ваше состояние. (Но это будет сброшено при дополнительном повторном рендеринге)

Kevin Wang 16.10.2019 20:21

Спасибо за отзыв, вы оба правы, обновленное решение

Charlie Tupman 02.06.2020 16:44

@ AntonioPavicevac-Ortiz Я обновил ответ, чтобы теперь отображать propHasChanged как true, который затем вызывает его один раз при рендеринге, может быть лучшим решением, просто вырвать useEffect и просто проверить опору

Charlie Tupman 11.06.2020 18:17

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

Charlie Tupman 11.06.2020 18:20

Это просто и умно. Почему я не подумал об этом, смеется :) Хорошая работа.

Jay Mayu 19.07.2020 14:43

Использование 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.

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

santomegonzalo 28.11.2019 17:27

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

Обязанности

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

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

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

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

  • usePrevious(value)
  • useLatest(value)
  • useDelta(value, options)
  • useDeltaArray(valueArray, options)
  • useDeltaObject(valueObject, options)
  • some(deltaArray)
  • every(deltaArray)

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

  • useConditionalEffect(callback, boolean)

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

Именно то, что я ищу. Собираюсь попробовать!

hackerl33t 24.04.2020 12:34

@Hisato, это могло быть излишним. Это что-то вроде экспериментального API. И, честно говоря, я мало использовал его в своих командах, потому что он не получил широкой известности и не принят. В теории это звучит неплохо, но на практике оно того не стоит.

Austin Malerba 10.07.2020 17:41

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

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

Хорошее решение, но содержит синтаксические ошибки. useRef, а не userRef. Забыл использовать текущий prevAmount.current

Blake Plumb 28.04.2020 16:07

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

SeedyROM 13.10.2020 10:04

Вы, вероятно, могли бы абстрагироваться от этого еще дальше, чтобы сделать сверхпростую утилиту.

SeedyROM 13.10.2020 10:05

Это даст вам значения исходный, а не значения предыдущий. Причина, по которой перехватчик usePrevious() работает, заключается в том, что он упакован в useEffect().

Justin 23.06.2021 22:20

@Justin Функция возврата должна обновлять предыдущие значения для каждого рендера. Вы это проверяли?

Drazen Bjelovuk 24.06.2021 05:19

Это супер чистый ответ, я бы лично выбрал это решение, оно отлично работает в моем проекте. Спасибо !

Tomoe 13.10.2021 16:38

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

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

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

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

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

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

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

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

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

/**
 * effect fires if one of the conditions in the dependency array is true
 */
export const useEffectCompare = (callback: () => void, conditions: boolean[], effect = useEffect) => {
  const shouldUpdate = useRef(false);
  if (conditions.some((cond) => cond)) shouldUpdate.current = !shouldUpdate.current;
  effect(callback, [shouldUpdate.current]);
};

//usage - will fire because one of the dependencies is true.
useEffectCompare(() => {
  console.info('test!');
}, [false, true]);

Другие вопросы по теме