简体   繁体   English

我怎样才能在 useEffect 钩子体内获取一个新的 state?

[英]How can I grab a fresh state inside a useEffect hook body?

I am trying to build a debounce hook.我正在尝试构建一个去抖动钩子。 I have seen several implementations before but none of them suit my needs: usually they delay executing a handler until the attempts to call the handler stop long enough.我以前见过几个实现,但没有一个适合我的需要:通常它们会延迟执行处理程序,直到调用处理程序的尝试停止足够长的时间。

useEffect(() => {
  timeout = setTimeout(handler, 500);

  return () => {
    if (timeout){
      clearTimeout(timeout);    
    }
  }
}, [handler]);

(or something like that.) I think this is flawed, because if the intent is to avoid spamming a long-running function, it doesn't take into account whether the function returns within the timeout or not. (或类似的东西。)我认为这是有缺陷的,因为如果意图是避免向长时间运行的 function 发送垃圾邮件,它不会考虑 function 是否在超时内返回。 What if fetching search results takes longer than 500ms in this case?如果在这种情况下获取搜索结果的时间超过 500 毫秒怎么办?

Instead, I want to try and run a long running function (the handler.) If there isn't one running, execute the handler and return its promise. Also, use the finally block to check to see if the input has changed, and if so, fire the handler again.相反,我想尝试运行一个长时间运行的 function(处理程序)。如果没有运行,则执行处理程序并返回其 promise。此外,使用 finally 块检查输入是否已更改,并且如果是这样,再次解雇处理程序。

My desired usage:我想要的用法:

const [input, setInput] = useState<string>("");
const debouncedPromise = useDebounce(() => asyncFunction(input), [input]);

Anytime the input changes, the handler could be queued up if it isn't already running.只要输入发生变化,如果处理程序尚未运行,它就可以排队。

This is the code I've written:这是我写的代码:

import { DependencyList, useEffect, useRef, useState } from "react";

interface IState<T> {
  handler?: () => Promise<T>;
  promise?: Promise<T>;
  isWaiting: boolean;
}

export const useDebounce = <T>(handler: () => Promise<T>, deps: DependencyList = []): Promise<T> | undefined => {
  const [state, setState] = useState<IState<T>>({
    handler,
    isWaiting: false
  });

  const stopWaiting = () => {
    console.log("stopWaiting");
    setState(previousState => ({ ...previousState, waiting: false }));
  };

  const run = () => {
    const promise = handler();
    promise.finally(stopWaiting);

    setState({
      handler,
      isWaiting: true,
      promise,
    });
  };

  useEffect(() => {
    console.log("\nuseEffect");
    console.log(`deps: ${deps}`)
    console.log(`state.isWaiting: ${state.isWaiting}`);
    console.log(`state.handler: ${state.handler}`);
    console.log(`state.promise: ${state.promise}`);

    if (state.isWaiting){
      console.log(">>> state.isWaiting")
      return;
    }
    
    if (handler === state.handler){
      console.log(">>> handler === state.handler")
      return;
    } 
    
    if (state.isWaiting && state.promise && state.handler !== handler){
      console.log(">>> state.isWaiting && state.promise && state.handler !== handler")
      state.promise.finally(run);
      return;
    }

    if (handler !== state.handler){
      console.log(">>> handler !== state.handler")
      run();
    }

    console.log("end useEffect");
  }, [...deps, state.isWaiting]);

  return state.promise;
};

It works for the first invocation, but it never seems to free up the state.isWaiting to allow subsequence, pending handlers to be fired:它适用于第一次调用,但它似乎永远不会释放 state.isWaiting 以允许触发子序列、挂起的处理程序:

useEffect UseDebounce.ts:32
deps: T UseDebounce.ts:33
state.isWaiting: false UseDebounce.ts:34
state.handler: function () {
    return asyncFunction(input);
  } UseDebounce.ts:35
state.promise: undefined UseDebounce.ts:36
>>> handler !== state.handler UseDebounce.ts:55
asyncFunction called with T UseDebounce.tsx:9
end useEffect UseDebounce.ts:59

useEffect UseDebounce.ts:32
deps: T UseDebounce.ts:33
state.isWaiting: true UseDebounce.ts:34
state.handler: function () {
    return asyncFunction(input);
  } UseDebounce.ts:35
state.promise: [object Promise] UseDebounce.ts:36
>>> state.isWaiting UseDebounce.ts:39
asyncFunction resolved with T UseDebounce.tsx:12
stopWaiting UseDebounce.ts:16

useEffect UseDebounce.ts:32
deps: Ti UseDebounce.ts:33
state.isWaiting: true UseDebounce.ts:34 // This should be false at this point

I think I'm stuck with a stale state. How can I resolve this?我想我被陈旧的 state 困住了。我该如何解决这个问题? Is there a better hook I can use to get my desired results?有没有更好的钩子可以用来获得我想要的结果? And how can I stop firing the handler once the input "settles down"?一旦输入“稳定下来”,我如何停止触发处理程序?

It never seems to free up the state.isWaiting .它似乎永远不会释放state.isWaiting I think I'm stuck with a stale state.我想我被陈旧的 state 困住了。

I think your problem is rather too fresh state. In particular, you are calling the hook with a new, different handler every time, so the only time that handler === state.handler could be true is that initial that initialises the state with the same handler.我认为你的问题太新鲜了 state。特别是,你每次都用一个新的、不同的handler调用钩子,所以handler === state.handler可能是真的是初始化 state 的初始值同一个处理程序。 You might need to use useCallback on the function you're passing.您可能需要在传递的 function 上使用useCallback

But even then your code has a problem.但即便如此,你的代码也有问题。 If the input changes multiple times while the function still running, you will call state.promise.finally(run);如果在 function 仍在运行时输入多次更改,您将调用state.promise.finally(run); multiple times, which schedules run (with, indeed, some stale handler s) multiples times on the same promise, causing them to execute at the same time.多次,哪些调度在同一个 promise 上run (实际上是一些陈旧的handler )多次,导致它们同时执行。

Is there a better hook I can use to get my desired results?有没有更好的钩子可以用来获得我想要的结果?

I would not (only) use a state but rather a simple shared mutable ref for the input that's waiting to be used in the next function invocation.我不会(仅)使用 state,而是为等待在下一次 function 调用中使用的输入使用一个简单的共享可变引用。 Also let the run function continue with the latest input by itself, instead of scheduling a fixed handler that's hard to cancel.还让run function 自己继续最新的输入,而不是安排一个难以取消的固定处理程序。

So I'd write所以我会写

interface ResultState<T> {
  running: boolean;
  result?: PromiseSettledResult<T>;
}
function useAsyncEvaluation<T, Args extends DependencyList>(fn: (...args: Args) => Promise<T>, input: Args): ResultState<T> {
  const args = useRef<Args | undefined>();
  const [state, setState] = useState<ResultState<T>>({running: false});
  
  function run() {
    const next = args.current;
    if (!next) return false;
    args.current = undefined;
    fn(...next).then(
      value => ({status: "fulfilled", value}),
      reason => ({status: "rejected", reason})
    ).then(result => {
      setState({running: run(), result});
    );
    return true;
  }
  useEffect(() => {
    args.current = input;
    setState(old => {
      if (old.running) {
        return old;
      } else {
        // calling run() inside the state update prevents race conditions
        return {running: run(), result: old.result};
      }
    });
    return () => {
      args.current = undefined;
    };
  }, input)
  return state;
}

Notice that this has the slightly undesirable behaviour of updating state in an effect during the first render and in response to an input change.请注意,这会在第一次渲染期间更新效果中的 state并响应input更改,这有点不合需要。 You might not actually want to do this anyway - rather, return the function from the hook and call it from your event handlers.无论如何,您可能实际上并不想这样做——而是从挂钩中返回 function 并从您的事件处理程序中调用它。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM