假设我有3个输入:rate、sendAmount和receiveAmount。我把这3个输入放到useEffect上。规则如下:

如果sendAmount改变了,我计算receiveAmount = sendAmount * rate 如果receiveAmount改变了,我计算sendAmount = receiveAmount / rate 如果速率发生变化,当sendAmount > 0时,我计算receiveAmount = sendAmount * rate;当receiveAmount > 0时,我计算sendAmount = receiveAmount / rate

下面是代码和框https://codesandbox.io/s/pkl6vn7x6j来演示这个问题。

是否有一种方法来比较oldValues和newValues,如componentDidUpdate,而不是为这种情况下做3处理程序?

谢谢


下面是我使用usePrevious的最终解决方案 https://codesandbox.io/s/30n01w2r06

在本例中,我不能使用多个useEffect,因为每次更改都会导致相同的网络调用。这就是为什么我还使用changeCount来跟踪更改。这个changeCount还有助于仅从本地跟踪更改,因此可以防止由于服务器更改而引起的不必要的网络调用。


当前回答

由于状态与功能组件中的组件实例不是紧密耦合的,因此如果不先保存之前的状态,例如使用useRef,则无法在useEffect中到达之前的状态。这也意味着状态更新可能在错误的位置被错误地实现,因为之前的状态在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
});
...

其他回答

下面是我使用的一个自定义钩子,我认为它比使用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])

希望这能有所帮助。

离开公认的答案,一个不需要自定义钩子的替代解决方案:

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

这假设您实际上需要引用前面“process here”位中的任何值。否则,除非你的条件超出了一个直接的!==比较,这里最简单的解决方案是:

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

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

我刚刚发布了react-delta,它解决了这种情况。在我看来,useEffect有太多的责任。

责任

它使用Object.is比较其依赖数组中的所有值 它基于#1的结果运行effect/cleanup回调

分解责任

react-delta将useEffect的职责分解为几个更小的钩子。

责任# 1

usePrevious(值) useLatest(值) useDelta(价值,选项) useDeltaArray (valueArray选项) useDeltaObject (valueObject选项) 一些(deltaArray) 每一个(deltaArray)

责任# 2

布尔useConditionalEffect(回调)

根据我的经验,这种方法比useEffect/useRef解决方案更灵活、干净和简洁。

如果有人正在寻找usePrevious的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;
};

选项1 -当值改变时运行useEffect

const Component = (props) => {

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

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

Demo

选项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;
}


Demo