Skip to content

Can’t update state to change display on page during api call

I have a page called Dashboard and on this page I have a component called Dropzone which is used for uploading a file to the page.

When a file is uploaded, it calls the onDrop() callback where I POST the file to my api, and I then try to sent a GET request to my api at /api/machines/{hash}. I want to set my state.pcapAnalysing variable to TRUE before this GET request, and set it to FALSE after it finishes. The idea of this is to show an ‘analysing’ message on the Dashboard page while the api call is running, this is likely to take a few seconds to return.

Due to the state not being refreshed, the message does not show as the state never updates to show state.pcapAnalysing as anything but false. Does anyone have any idea how I can achieve the effect I’m looking for? Essentially, I’m trying to show a message on the Dashboard page for the duration of the api call action, but have this triggered by the component residing in Dropzone.js. Thanks.

Dashboard.js

...

export default function Dashboard() {

  const [currentTime, setCurrentTime] = useState(0);
  const [state, dispatch] = useContext(Context);

  useEffect(() => {
    fetch('/api/time').then(res => res.json()).then(data => {
      setCurrentTime(data.time);
    });
  }, []);

  return (
    <PageBase navigation={navigation}>
        <div className="flex place-content-center mt-10">
          <div className="flex w-1/2 place-content-center">
              { !state.pcapUploaded ? 
                state.pcapAnalysing ? 
                  <p>uploading</p> // Want to show this when state.pcapAnalysing is TRUE
                :
                  <TwoCard numcols="2">
                    <div>
                      <h5 className="">Upload a PCAP file to analyse</h5>
                      <p>Accepted file types: .pcap, .pcapng, .tcpdump</p>
                    </div>
                    <div className="mt-5 lg:ml-2 lg:mt-0 md:mt-0">
                      <MyDropzone/>
                    </div>
                  </TwoCard> 
              : 
                <TwoCard numcols="3">
                  <div className='col-span-1'>
                    <img src={require("../Assets/pcap.png")}/>
                  </div>
                  <div className='col-span-2'>
                    <h5 className="">Selected File:</h5>
                    <p className="break-all">{state.pcapFileName}</p>
                    <p className="break-all">{state.pcapHash}</p>
                    <button type="button" onClick={clearPcap}>
                      Change
                    </button>
                  </div>
                </TwoCard>
              }
          </div>
        </div>
        <div>
          { state.pcapUploaded ? <TileGrid tiles={tiles}/> : null } 
        </div>
      </PageBase>
  );
}

Dropzone.js

import { useCallback, useEffect, useContext } from 'react';
import { useDropzone } from 'react-dropzone';
import { Context } from '../Helpers/Provider';


export default function MyDropzone() {

  const [state, dispatch] = useContext(Context);

  const onDrop = useCallback(acceptedFiles => {
    const formData = new FormData();
    formData.append('file', acceptedFiles[0]);

    fetch('/api/upload',
      {
        method: 'POST',
        body: formData,
      }
    )
    .then(res => res.json())
    .then(result => {
      console.log('Success: ', result);
      console.log("dispatching...");

      dispatch({
        type: 'HASH',
        payload: result['hash'],
      });

      dispatch({
        type: 'ANALYSING', // Want to use this to set state.pcapAnalysing to TRUE
      });

      console.log("before: " + state.pcapAnalysing);

      if (state.machineIPs == null) {
        console.log("Machines: ", state.pcapHash);
        fetch(`/api/machines/${result['hash']}`)   // This request may take a few seconds
          .then(res => res.json())
          .then(data => {
            console.log(data);
            dispatch({
              type: 'MACHINES',
              payload: result,
            });
          });
      };
  
      dispatch({
        type: 'ANALYSING', // Want to use this to set state.pcapAnalysing to false after request completes
      });
  
      console.log("after: " + state.pcapAnalysing);
    })
  }, [state.pcapHash])

  const {getRootProps, getInputProps, isDragActive} = useDropzone({
    onDrop,
    maxFiles: 1,
    accept: '.pcap, .pcapng, .tcpdump'
  })

  return (
    <div {...getRootProps()} className="..<shortened>..">
      Upload PCAP
      <input {...getInputProps()} />
      {
        isDragActive ?
          <p>Drop the file here ...</p> :
          <p className="ml-2 mr-2">Drag 'n' drop a file here, or click to select file</p>
      }
    </div>
  )
}

Answer

Looks like you should be setting the final state within the then close of your /api/machines call. Not after it.

So the whole call should look like this:

fetch('/api/upload',
      {
        method: 'POST',
        body: formData,
      }
    )
    .then(res => res.json())
    .then(result => {
      console.log('Success: ', result);
      console.log("dispatching...");

      dispatch({
        type: 'HASH',
        payload: result['hash'],
      });

      dispatch({
        type: 'ANALYSING', // Want to use this to set state.pcapAnalysing to TRUE
      });

      console.log("before: " + state.pcapAnalysing);

      if (state.machineIPs == null) {
        console.log("Machines: ", state.pcapHash);
        fetch(`/api/machines/${result['hash']}`)   // This request may take a few seconds
          .then(res => res.json())
          .then(data => {
            console.log(data);

            dispatch({
                type: 'ANALYSING', // Want to use this to set state.pcapAnalysing to false after request completes
            });

            dispatch({
              type: 'MACHINES',
              payload: result,
            });
          });
      };
    })
  }, [state.pcapHash])

Notice I removed that line altogether

console.log("after: " + state.pcapAnalysing);

Because even if I moved it within the then close, the state wouldn’t have been updated yet, because the component re-render hasn’t occurred yet.