如何比较 React Hooks useEffect 上的旧值和新值?多次重新渲染

How to compare oldValues and newValues on React Hooks useEffect? Multiple re-renders

与此处描述的问题有点相同

但就我而言,usePrevious 钩子没有帮助。

想象一下具有多个输入、selects 等的表单。您可能想查看 https://app.uniswap.org/#/swap 以进行类似的可视化。几乎任何更改都会发生一些操作和数据更新,这将导致多次重新渲染,至少 4 次。例如。

我有 2 个输入,每个代表一个令牌。 Base(第一个)和 Quote(第二个)。

这是基地的状态

const [base, setBase] = useState({
    balance: undefined,
    price: undefined,
    value: initState?.base?.value,
    token: initState?.base?.token,
    tokenId: initState?.base?.tokenId,
});

和报价

const [quote, setQuote] = useState({
    balance: undefined,
    price: undefined,
    value: initState?.quote?.value,
    token: initState?.quote?.token,
    tokenId: initState?.quote?.tokenId,
});

他们会组成一对,例如 BTC/USD

通过更改 token(而不是 BTC 我将在 select 菜单中选择 ETH)将触发几个动作:获取钱包余额、获取价格,并且还会有一些带有输入视图更新和模式 window 关闭的重新渲染。所以现在至少有 4 个正在发生。我希望能够将 base.tokenbasePrvconst basePrv = usePrevious(base?.token); 但在第二次重新渲染时 base.tokenbasePrv 已经具有相同的标记 属性,这是一个问题。

我也有输入之间的交换功能,我应该用引号改变基数,像这样用基数改变引号

setBase(prevState => ({
    ...prevState,
    base: quote
}));

setQuote(prevState => ({
    ...prevState,
    quote: base
}));

在这种情况下,没有应该触发的额外请求。

现在我有 useEffect 并且 token 依赖它。但是每次更改令牌时都会触发它,这将导致额外的异步调用和 'tail' 的请求(如果您要快速单击)。这就是为什么我需要比较更改之前的 token 属性 以了解我是否应该因为新对的形成而进行额外的调用和请求 (BTC/USD 变成 ETH/USD) 或者我应该忽略它,因为它只是一个“交换”(BTC/USD变为 USD/BTC) 并且不需要进行额外的调用和提取。我只需要,嗯,交换它们,而不是更多。

所以在我的故事中,usePrevious hook 只会 return 之前的 token 属性 一次,在第二次和第三次时,它会被多次重新渲染覆盖(将获取其他属性)到新属性。所以当 useEffect 被触发时,我将没有机会比较前一个令牌 属性 和当前的令牌,因为它们将显示相同的

对于如何解决它,我有几个想法,但我不确定它是对还是错,因为在我看来,这些决定看起来比声明更命令。

  1. 我可以让一切保持原样(无论发生什么变化,请求总是会在任何变化时触发。是交换还是用户改变了一对)。我可以禁用交换按钮,直到完成所有请求。它将解决请求 'tail' 的问题。但它有点像修补程序,它会起作用,但我不喜欢它,因为它会导致额外的不必要的请求,而且它会很慢并且不利于用户体验。

  2. 我可以在 setBasesetQuote 更新之前使用一个状态来保持前一对。它将允许我使用 useEffect 并将前一对与当前一对进行比较,以了解这对是否已更改,或者只是交换并决定我是否进行提取和调用。

  3. 我可以摆脱 useEffectbase.tokenquote.token 依赖关系,并处理 onClick 处理程序中的所有内容。因此,交换功能不会触发 useEffect,并且仅当用户单击并选择不同的内容时才会触发调用和提取。但正如我所说,这个选项对我来说似乎有点奇怪。

  4. 我在这里尝试使用闭包来“记住”令牌的先前状态,但这有点类似于使用当前组件状态。此外,您必须在功能组件主体之外初始化闭包,我看不到以这种方式将 init 状态传输到其中的可能性,因此代码变得更加面条化。

那么大家还有其他想法吗?我肯定错过了什么。也许很多重新渲染是一种反模式,但我不确定如何避免这种情况。

您的问题可能有多种解决方案。我建议选择一个更容易理解的。

1。修改usePrevious钩子

您可以修改 usePrevious 挂钩以在多个渲染中存活。

提示:如果您认为该值将是一个复杂的对象并且即使对于相同的实际值也可能更改引用,请使用JSON.stringify进行比较。

function usePrevious(value) {
  const prevRef = useRef();
  const curRef = useRef();

  if (value !== curRef.current){
  // or, use
  // if ( JSON.stringify(value) !== JSON.stringify(curRef.current)){
    prevRef.current = curRef.current;
    curRef.current = value;
  }
  
  return prevRef.current;
}

2。排序 useEffect 依赖数组

由于您使用标记(字符串)作为 useEffect 的依赖数组,并且您不介意它们的顺序(交换不应该改变任何东西),因此对依赖数组进行排序

useEffect(
  () => {
    // do some effect
  },
  [base.token, quote.token].sort()
)

3。存储当前获取的令牌。

在存储 API 响应数据的同时,还存储与该数据关联的令牌(请求的一部分)。现在,您将拥有 2 组令牌。

  1. 当前选择的代币
  2. 当前获取的令牌

您可以选择仅在当前获取的令牌不能满足您的需求时才获取。您还可以扩展它并存储以前的 API request/responses 并在可能的情况下从中选择结果。

判决

在所有这些中,第 3 种方法对我来说似乎是一种不错且更标准化的方法,但对您的需求来说有点矫枉过正(除非您想缓存以前的结果)。

由于简单和极简主义,我会选择第二名。不过,还是要看你最后觉得什么比较容易。