I'm trying to understand the new React hooks API (I'm using React 16.8.x at the moment).

I've found the useEffect() hook makes it very easy to discard the results of a server call when the user does something that causes a component to no longer be displayed, as per (A):

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};
}, []);

But how do I achieve similar behaviour when I do a call from a normal form event, as per (B):

<form onSubmit={()=>{
  setInvocation("processing");
  MuiServiceApi.instance.
    invokeProcessingMethod(details.endpoint, parsedPayload).
    then(result=> setInvocation(result)).
    catch(e=> setInvocation({message: "while updating DB", problem: e}));
}}>

If the user dismisses the component while it's doing the invocation when it's first displayed (i.e. the (A) logic), then the result will be discarded cleanly.

If the user dismisses the component while processing, after having clicked the actual submit button ((B) logic), there will be a console warning like:

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.

This isn't really a big deal - in fact, under the pre-hooks class API, I never bothered discarding results for unmounted components because it was too much hassle.

But the point of this exercise was to learn about the Hooks API, so I'd like to know how I could do this for the onSubmit handler.

I tried defining mounted with useState(), but I can't see how it would work, as per:

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)
  };
}, []);

I realise now in hindsight it can't work because because the mounted value of false is captured by the closure; so the then handler won't ever see mounted == true.

Is this where "reducers" or "callbacks" are supposed to be used? The documentation gets pretty vague past the "basic" hooks, so I'm not sure if that's how I'm supposed to make this work.

To restate the question: How should the component below be refactored such that the then() handler inside the form onSubmit will not result in a warning about updating state if the component has already been unmounted?


Full component below (in 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>
}

0 Answers