303

假设我有 3 个输入:rate、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 。

有没有办法比较oldValuesnewValues喜欢componentDidUpdate而不是为这种情况制作3个处理程序?

谢谢


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

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

4

13 回答 13

408

您可以编写一个自定义钩子,使用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但是,如果您对每个要单独处理的更改 id 分别使用两个,则它更清晰,可能更好更清晰地阅读和理解

于 2018-11-23T12:22:38.230 回答
107

如果有人正在寻找 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;
};
于 2019-08-29T09:06:31.830 回答
57

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


演示

于 2019-06-29T11:58:08.563 回答
32

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

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]);
};
于 2020-04-08T20:09:11.377 回答
7

我刚刚发布了react-delta,它解决了这种确切的场景。在我看来,useEffect责任太多了。

责任

  1. 它使用比较其依赖数组中的所有值Object.is
  2. 它根据 #1 的结果运行效果/清理回调

分拆责任

react-deltauseEffect将的职责分解为几个较小的钩子。

责任#1

责任#2

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

于 2019-12-12T15:20:39.840 回答
5

由于状态与功能组件中的组件实例没有紧密耦合,因此如果不先保存它就无法达到先前的状态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
});
...
于 2018-11-23T12:45:14.813 回答
5

如果您更喜欢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将与您的初始输入值相同。仅当您想在值不变的情况下做某事时,这才对您有意义

于 2020-10-12T14:24:04.937 回答
4

对于非常简单的道具比较,您可以使用它useEffect来轻松检查道具是否已更新。

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

useEffect然后仅在道具更改时才运行您的代码。

于 2019-09-20T09:29:50.940 回答
4

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

希望有帮助。

于 2020-05-08T13:22:19.343 回答
3

使用 Ref 会在应用程序中引入一种新的错误。

usePrevious让我们使用之前有人评论过的案例来看看这个案例:

  1. prop.minTime: 5 ==> ref.current = 5 | 设置参考电流
  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

于 2019-11-28T15:32:23.563 回答
0

您可以使用与 useState相反的 useImmer 并访问状态。示例:https ://css-tricks.com/build-a-chat-app-using-react-hooks-in-100-lines-of-code/

于 2020-10-18T03:57:38.183 回答
0

我不喜欢上面的任何答案,我希望能够传递一组布尔值,如果其中一个是真的,那么重新渲染

/**
 * 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.log('test!');
}, [false, true]);
于 2021-07-25T10:12:38.667 回答
-1

在您的情况下(简单对象):

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

在其他情况下(复杂对象)

const {cityInfo} = props;
useEffect(()=>{
  // some logic
}, [cityInfo.cityId])
于 2020-12-28T09:00:02.767 回答