Asynchronous calls with React.useMemo

青春壹個敷衍的年華 提交于 2021-01-02 05:30:15

问题


Scenario is relatively simple: we have a long-running, on-demand calculation that occurs on a remote server. We want to memoize the result. Even though we are fetching asychronously from a remote resource, this isn't a side effect because we just want the result of this calculation to display to the user and we definitely don't want to do this on every render.

Problem: it seems that React.useMemo does not directly support Typescript's async/await and will return a promise:

//returns a promise: 
let myMemoizedResult = React.useMemo(() => myLongAsyncFunction(args), [args])
//also returns a promise:
let myMemoizedResult = React.useMemo(() => (async () => await myLongAsyncFunction(args)), [args])

What is the correct way to wait on the result from an asynchronous function and memoize the result using React.useMemo? I've used regular promises with plain JS but still struggle with them in these types of situations.

I've tried other approaches such as memoize-one, but the issue seems to be that the this context changes due to the way that React function components work break the memoization, which is why I'm trying to use React.useMemo.

Maybe I'm trying to fit a square peg in a round hole here - if that's the case it would be good to know that too. For now I'm probably just going to roll my own memoizing function.

Edit: I think part of it was that I was making a different silly mistake with memoize-one, but I'm still interested to know the answer here wrt React.memo.

Here's a snippet - the idea is not to use the memoized result directly in the render method, but rather as something to reference in an event-driven way i.e. on a Calculate button click.

export const MyComponent: React.FC = () => {
    let [arg, setArg] = React.useState('100');
    let [result, setResult] = React.useState('Not yet calculated');

    //My hang up at the moment is that myExpensiveResultObject is 
    //Promise<T> rather than T
    let myExpensiveResultObject = React.useMemo(
        async () => await SomeLongRunningApi(arg),
        [arg]
    );

    const getResult = () => {
        setResult(myExpensiveResultObject.interestingProperty);
    }

    return (
        <div>
            <p>Get your result:</p>
            <input value={arg} onChange={e => setArg(e.target.value)}></input>
            <button onClick={getResult}>Calculate</button>
            <p>{`Result is ${result}`}</p>
        </div>);
}

回答1:


Edit: My original answer below seems to have some unintended side effects due to the asynchronous nature of the call. I would instead try and either think about memoizing the actual computation on the server, or using a self-written closure to check if the arg hasn't changed. Otherwise you can still utilize something like useEffect as I described below.

I believe the problem is that async functions always implicitly return a promise. Since this is the case, you can directly await the result to unwrap the promise:

const getResult = async () => {
  const result = await myExpensiveResultObject;
  setResult(result.interestingProperty);
};

See an example codesandbox here.

I do think though that a better pattern may be to utilize a useEffect that has a dependency on some state object that only gets set on the button click in this case, but it seems like the useMemo should work as well.




回答2:


The correct solution is to use useEffect. I understand you didn't want to:

useEffect checks the args on every render, which is not the desired behavior here because the user may want to change more than one input before calling the long running calculation.

But all hooks check the args on every render - even if you could get useMemo to work the way you want, it would still update every time the user changed an input. The correct solution depends on exactly when you want the calculation to be made, which you don't mention, but there are two options:

One, if the calculation should be triggered by a specific user action like clicking a "Calculate" button, then it should be useCallback:

const [result, setResult] = React.useState('Not yet calculated');
const handleSubmit = React.useCallback(
  () => SomeLongRunningApi(arg).then(setResult),
  [arg]
);
return <CalculationForm onSubmit={handleSubmit} />;

Or, if it should happen automatically but only once all inputs are valid, use useEffect but add a validation check:

const [result, setResult] = React.useState('Not yet calculated');
React.useEffect(
  () => {
    if (isValid(arg)) {
      SomeLongRunningApi(arg).then(setResult);
    }
  },
  [arg]
);

So either way useMemo isn't the right hook for this job. But since this question was about async useMemo and ranks highly for that on search engines, here it is:

function useMemoAsync<T>(factory: () => Promise<T>, deps: any[] = []) {
  const [value, setValue] = useState<T | undefined>(undefined);

  useEffect(() => {
    factory().then(setValue);
  }, deps);

  return value;
};


来源:https://stackoverflow.com/questions/61751728/asynchronous-calls-with-react-usememo

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!