Як порівнювати старі та нові Values ​​на React Hooks useEffect?


150

Скажімо, у мене є 3 входи: rate, sendAmount і acceptAmount. Я поставив, що 3 входи на useEffect відрізняються парами. Правила такі:

  • Якщо sendAmount змінився, я обчислюю receiveAmount = sendAmount * rate
  • Якщо прийом змінено, я підрахував sendAmount = receiveAmount / rate
  • Якщо показник змінився, я обчислюю, receiveAmount = sendAmount * rateколи sendAmount > 0я обчислю, sendAmount = receiveAmount / rateколиreceiveAmount > 0

Ось codeandbox https://codesandbox.io/s/pkl6vn7x6j, щоб продемонструвати проблему.

Чи є спосіб порівняти oldValuesі newValuesподібне, componentDidUpdateа не зробити 3 обробники для цього випадку?

Дякую


Ось моє остаточне рішення з usePrevious https://codesandbox.io/s/30n01w2r06

У цьому випадку я не можу використовувати декілька, useEffectоскільки кожна зміна веде до одного і того ж мережевого дзвінка. Тому я також використовую changeCountдля відстеження змін. Це changeCountтакож корисно відслідковувати зміни лише від локальних, тому я можу запобігти зайвим мережевим дзвінкам через зміни з боку сервера.


Наскільки саме компонентDidUpdate повинен допомогти? Вам потрібно буде написати ці 3 умови.
колба Естуса

Я додав відповідь з 2 необов'язковими рішеннями. Чи працює один з них для вас?
Бен Карп

Відповіді:


209

Ви можете написати спеціальний гачок, щоб надати вам попередній реквізит за допомогою 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дзвінків окремо. Не знали, що ви можете використовувати його кілька разів!
JasonH

3
Я спробував код вище, але eslint попереджає мене про useEffectвідсутність залежностіprevAmount
Littlee

2
Оскільки prevAmount має значення для попереднього значення стану / реквізиту, вам не потрібно передавати це як залежність від useEffect, і ви можете відключити це попередження для конкретного випадку. Ви можете прочитати цю публікацію для більш детальної інформації?
Шубхам Хатрі

Любіть це - useRef завжди приходить на допомогу.
Фернандо Рохо

@ShubhamKhatri: У чому причина використанняEffect обгортання ref.current = значення?
curly_brackets

40

Якщо ви хочете, щоб хтось шукав TypeScript-версію usePrevious:

import { useEffect, useRef } from "react";

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

2
Зауважте, що це не буде працювати у файлі TSX, щоб змусити це працювати у зміні файлу TSX, const usePrevious = <T extends any>(...щоб змусити інтерпретатора побачити, що <T> не є JSX і є загальним обмеженням
apokryfos

1
Можете пояснити, чому <T extends {}>не вийде. Здається, це працює для мене, але я намагаюся зрозуміти складність його використання.
Karthikeyan_kk

.tsxФайли містять jsx, який повинен бути ідентичним HTML. Цілком можливо, що generic <T>є незакритим тегом компонентів.
SeedyROM

Що з типом повернення? Я отримуюMissing return type on function.eslint(@typescript-eslint/explicit-function-return-type)
Саба Аханг

@SabaAhang Вибачте! TS з більшою частиною використовуватиме умовивід типу для обробки типів повернення, але якщо у вас включена зв'язування, я додав тип повернення для позбавлення від попереджень.
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 двічі?
Тарун Нагпал

@TarunNagpal, вам необов'язково двічі використовувати useEffect. Це залежить від вашого випадку використання. Уявіть, що ми просто хочемо записати, який val змінився. Якщо у нашому масиві залежностей є як val1, так і val2, він запускатиметься кожного разу, коли будь-яке значення змінюється. Тоді всередині функції, яку ми передаємо, нам доведеться з’ясувати, який val змінився, щоб увійти в правильне повідомлення.
Бен Карп

Дякую за відповідь. Коли ви скажете "всередині функції, яку ми передаємо, ми повинні з'ясувати, який val змінився". Як ми можемо цього досягти. Прошу керівництво
Тарун Нагпал

6

Я щойно опублікував react-delta, який вирішує саме такий сценарій. На мою думку, 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? Чи можете ви оновити codeandbox з відповіді детально?
колба Естуса

З оновлення я можу припустити, що ви хочете отримати sendAmountі т.д. асинхронно, а не обчислювати їх, правда? Це сильно змінюється. Це можна зробити, useReduceале це може бути складним. Якщо ви мали справу з Redux раніше, можливо, ви знаєте, що операції з асинхронізацією там не прості. github.com/reduxjs/redux-thunk - популярне розширення для Redux, яке дозволяє виконувати дії асинхронізації. Ось демонстрація, що доданки використовуютьReducer з тим же малюнком (useThunkReducer), codeandbox.io/s/6z4r79ymwr . Зауважте, що відправлена ​​функція async, ви можете робити запити там (коментується).
колба Естуса

1
Проблема з питанням полягає в тому, що ви запитали про іншу річ, яка не була вашою реальною справою, а потім змінили її, тому зараз це зовсім інше питання, тоді як існуючі відповіді з'являються так, ніби вони просто ігнорували питання. Це не рекомендована практика щодо ТА, оскільки вона не допомагає отримати відповідь, яку ви хочете. Будь ласка, не видаляйте важливих частин із запитання, на які вже посилаються відповіді (формули обчислення). Якщо у вас все-таки виникають проблеми (після мого попереднього коментаря (ви, мабуть, і так)), подумайте про те, щоб задати нове запитання, яке відображає вашу справу і
посилайтеся

Вітаю привіт, я думаю, що це кодиandbox кодиandbox.io/ s/ 6z4r79ymwr ще не оновлено. Я поверну запитання, вибачте за це.
rwinzhang

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


1
У React є такий приклад, але вони використовують state..., а не той props... коли ви дбаєте про старий реквізит, тоді станеться помилка.
santomegonzalo

3

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

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

}

useEffect тоді запустить ваш код лише в тому випадку, якщо опора зміниться.


1
Це працює лише один раз, після послідовних propзмін ви не зможете~ do stuff here ~
Karolis Šarapnickis

2
Схоже, вам слід зателефонувати setHasPropChanged(false)наприкінці, ~ do stuff here ~щоб "скинути" свій стан. (Але це призведе до скидання в додатковій показі)
Кевін Ван

Дякуємо за відгук, ви маєте рацію, оновлене рішення
Чарлі Тупман

@ AntonioPavicevac-Ortiz Я оновив відповідь, щоб тепер вивести propHasChanged як істинний, який би потім викликав його один раз на візуалізації, може бути кращим рішенням просто зірвати useEffect і просто перевірити опору
Charlie Tupman

Я думаю, що моє первісне використання цього було втрачено. Озираючись на код, ви можете просто використовувати 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
Blake Plumb

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

Сподіваюся, що це допомагає.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.