React Hooks:从普通事件处理程序启动时如何丢弃异步结果?

React Hooks: how to discard async results when launched from normal event handler?

我正在尝试了解新的 React 钩子 API(我目前正在使用 React 16.8.x)。

我发现 useEffect() 钩子可以很容易地在用户执行导致组件不再显示的操作时丢弃服务器调用的结果,根据 (一):

useEffect(()=>{
  let mounted = true;
  setInvocation("processing");
  MuiServiceApi.instance.
    invokeProcessingMethod(details.endpoint, parsedPayload).
    then(result=> mounted && setInvocation(result)).
    catch(e=> setInvocation({message: "while updating DB", problem: e}));
  return ()=>{mounted = false};
}, []);

但是,根据 (B):

,当我从正常形式的事件中调用时,如何实现类似的行为
<form onSubmit={()=>{
  setInvocation("processing");
  MuiServiceApi.instance.
    invokeProcessingMethod(details.endpoint, parsedPayload).
    then(result=> setInvocation(result)).
    catch(e=> setInvocation({message: "while updating DB", problem: e}));
}}>

如果用户在组件首次显示时执行调用时将其关闭(即 (A) 逻辑),则结果将被彻底丢弃。

如果用户在处理过程中关闭组件,在点击实际的提交按钮后((B) 逻辑),将会出现如下控制台警告:

Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

这没什么大不了的 - 事实上,在预挂钩 class API 下,我从来没有费心丢弃未安装组件的结果,因为这太麻烦了。

但是这个练习的目的是了解 Hooks API,所以我想知道如何为 onSubmit 处理程序执行此操作。

我尝试用 useState() 定义 mounted,但我看不出它是如何工作的,如:

const [mounted, setMounted] = React.useState(false);
useEffect(()=>{
  setMounted(true);
  setInvocation("processing");
  MuiServiceApi.instance.
    invokeProcessingMethod(details.endpoint, parsedPayload).
    then(result=> {
      if( mounted ){
        console.log("result used");
        setInvocation(result);
      }
      else {
        console.log("result ignored because dismounted");
      }
    }).
    catch(e=> setInvocation({message: "while updating DB", problem: e}));
  return ()=>{
    console.log("dismounted");
    setMounted(false)
  };
}, []);

事后我意识到它无法工作,因为 falsemounted 值被闭包捕获了;所以 then 处理程序永远不会看到 mounted == true.

这是应该使用 "reducers" 或 "callbacks" 的地方吗?文档在 "basic" 挂钩之后变得非常模糊,所以我不确定我是否应该这样做。

重述问题:应如何重构下面的组件,以便 form onSubmit 中的 then() 处理程序不会导致有关组件更新状态的警告已经卸载了吗?


下面的完整组件(在 Typescript 中)

function InvokeEndpoint(props:{}){
  const [details, setDetails] = React.useState(
    {endpoint: "testPayload", payload: '{"log":["help"]}'} );
  const [invocation, setInvocation] = React.useState
    <"init"|"processing"|ErrorInfo|ProcessingLogV1>("init");

  let isValidEndpoint = !!details.endpoint;
  let isValidPayload = true;
  let payloadErrorText = "";
  let parsedPayload = {};
  try {
    parsedPayload = JSON.parse(details.payload);
  }
  catch( e ) {
    isValidPayload = false;
    payloadErrorText = e.toString();
  }

  useEffect(()=>{
    let mounted = true;
    setInvocation("processing");
    MuiServiceApi.instance.
      invokeProcessingMethod(details.endpoint, parsedPayload).
      then(result=> mounted && setInvocation(result)).
      catch(e=> setInvocation({message: "while updating DB", problem: e}));
    return ()=>{mounted = false};
  }, []);

  const isProcessing = invocation == "processing";
  let result = undefined;
  if( invocation != "init" && invocation != "processing" ){
    if( isErrorInfo(invocation) ){
      result = <MuiCompactErrorPanel error={invocation}/>
    }
    else {
      result = <ul>{
        invocation.log.map((it,index)=> <li key={index}>{it}</li>)
      }</ul>
    }
  }

  return <Card><CardContent> <form onSubmit={()=>{
    setInvocation("processing");
    MuiServiceApi.instance.
      invokeProcessingMethod(details.endpoint, parsedPayload).
      then(result=> {
        console.log("resulted", result);
        setInvocation(result);
      }).
      catch(e=> {
        console.log("errored");
        setInvocation({message: "while updating DB", problem: e});
      } );
  }}>
    <Typography variant={"h5"}>Invoke endpoint</Typography>
    <TextField id="endpointInput" label="Endpoint"
      margin="normal" variant="outlined" autoComplete="on" fullWidth={true}
      inputProps={{autoCapitalize:"none"}}
      value={details.endpoint}
      onChange={( event: ChangeEvent<HTMLInputElement> )=>{
        setDetails({...details, endpoint: event.currentTarget.value});
      }}
      disabled={isProcessing}
      error={!isValidEndpoint}
    />
    <TextField id="payloadInput" label="Payload"
      margin="normal" variant="outlined" autoComplete="on" fullWidth={true}
      inputProps={{autoCapitalize:"none"}}
      multiline={true}
      value={details.payload}
      onChange={( event: ChangeEvent<HTMLInputElement> )=>{
        setDetails({...details, payload: event.currentTarget.value});
      }}
      disabled={isProcessing}
      error={!isValidPayload}
      helperText={payloadErrorText}
    />
    <PrimaryButton type="submit" color="primary"
      disabled={isProcessing || !isValidPayload || !isValidEndpoint}
    >
      <ButtonLabel isLoading={isProcessing}>Invoke</ButtonLabel>
    </PrimaryButton>
    { result }
  </form> </CardContent></Card>
}

处理此问题的一种方法是通过 React useRef API.

mounted 从变量更改为 ref

使用 const mounted = React.useRef(false) 声明 ref,然后在之前代码使用该变量的任何地方使用 mounted.current

始终通过 current 访问 Ref 值意味着组件的效果和事件处理程序都绑定到 Ref 指向的单个 "current" 值,而不是渲染组件时变量的副本。