Brave Sir Robin
Brave Sir Robin

Reputation: 31

React useState hook returns old state value despite it being updated

I want to:

  1. upload an image into the browser (I do this with React Dropzone. My uploadImage function below runs onDrop of file into Dropzone.)
  2. read the image as an actual image using the FileReader API.
  3. save the image file as well as image metadata into state, using React Hooks.
  4. make an API call that pushes this image to a server.

I am trying to confirm that I have the image at step 4, but I cannot confirm that via my console messages. I'm somehow stuck with the original state values despite log messages showing the state has already been updated.

I've tried adding extra asyncs to everything, and I've wrapped stuff in promises. I can confirm the readImage function is fine. I can confirm the run sequence is readImage, saveImage, setGraphic within saveImage, and then the console.log in uploadImage which should have the updated values but doesn't.

  const [graphic, setGraphic] = useState({ ...nullGraphic });

  const readImage = async (img) => {
    const reader = new FileReader();

    return new Promise((resolve, reject) => {
      reader.onerror = () => reader.abort();
      reader.onabort = () => reject(new DOMException('Problem parsing input file.'));
      reader.onload = () => resolve(reader.result);
      reader.readAsDataURL(img);
    });
  };

  const saveImage = async (img) => {
    const imageRead = await readImage(img);

    console.log(imageRead);

    await setGraphic({
      ...graphic,
      image: imageRead,
      imageName: img.name,
      imageType: img.type,
    });

  };


  const uploadImage = async (img) => {
    await saveImage(img);

    console.log(graphic);
});

render() {

  console.log(graphic);

  return( <some dom> )
}

this is my strange console sequence:

Form.js:112 starting saveImage 
Form.js:95 (snipped value of imageRead)
Form.js:237 {snipped updated values of graphic}
Form.js:114 done with saveImage
Form.js:116 {snipped original values of graphic}

My Theory

The useState hook is doing something funky. The process copies the value when it starts instead of reading it whenever the console.log is being read, so the uploadImage function gets the original values of the useState hook for the lifetime of that process.

Upvotes: 3

Views: 5557

Answers (1)

Jonas Wilms
Jonas Wilms

Reputation: 138527

It might become clear why this would not work if we would simplify all those React magic into :

  function withState(initial, fn) {
    function setState(updated) {
       setTimeout(() => fn(updated, setState), 0);
       //...
    }
    setState(initial);
  }

Now we can illustrate what happens:

  withState(0, (counter, setCount) => {
    console.log("before", counter);
    if(counter < 10) setCount(counter + 1);
    console.log("after", counter);
  });

As you can see, setCount will not change the state (count) directly. It will actually not change the local count variable at all. Therefore "before" and "after" will always log the same (as long as you don't mutate counter directly).

Instead calling setCount will be deffered (and batched in React), and then it will recall the whole component function passing in the new count.

Therefore it makes no sense trying to await the setState call, also it makes no sense to log the state before and after it (as it won't change inbetween). Log it once in a component, if you set the state the whole component executes again and the logging will log the new state.

Upvotes: 1

Related Questions