Can I execute a function after setState is finished updating?

JavascriptReactjsSetstate

Javascript Problem Overview


I am very new to ReactJS (as in, just started today). I don't quite understand how setState works. I am combining React and Easel JS to draw a grid based on user input. Here is my JS bin: http://jsbin.com/zatula/edit?js,output

Here is the code:

    var stage;
   
    var Grid = React.createClass({
        getInitialState: function() {
            return {
                rows: 10,
                cols: 10
            }
        },
        componentDidMount: function () {
            this.drawGrid();
        },
        drawGrid: function() {
            stage = new createjs.Stage("canvas");
            var rectangles = [];
            var rectangle;
            //Rows
            for (var x = 0; x < this.state.rows; x++)
            {
                // Columns
                for (var y = 0; y < this.state.cols; y++)
                {
                    var color = "Green";
                    rectangle = new createjs.Shape();
                    rectangle.graphics.beginFill(color);
                    rectangle.graphics.drawRect(0, 0, 32, 44);
                    rectangle.x = x * 33;
                    rectangle.y = y * 45;

                    stage.addChild(rectangle);

                    var id = rectangle.x + "_" + rectangle.y;
                    rectangles[id] = rectangle;
                }
            }
            stage.update();
        },
        updateNumRows: function(event) {
            this.setState({ rows: event.target.value });
            this.drawGrid();
        },
        updateNumCols: function(event) {
            this.setState({ cols: event.target.value });
            this.drawGrid();
        },
        render: function() {
            return (
                <div>
                    <div className="canvas-wrapper">
                        <canvas id="canvas" width="400" height="500"></canvas>
                        <p>Rows: { this.state.rows }</p>
                        <p>Columns: {this.state.cols }</p>
                    </div>
                    <div className="array-form">
                        <form>
                            <label>Number of Rows</label>
                            <select id="numRows" value={this.state.rows} onChange={ this.updateNumRows }>
                                <option value="1">1</option>
                                <option value="2">2</option>
                                <option value ="5">5</option>
                                <option value="10">10</option>
                                <option value="12">12</option>
                                <option value="15">15</option>
                                <option value="20">20</option>
                            </select>
                            <label>Number of Columns</label>
                            <select id="numCols" value={this.state.cols} onChange={ this.updateNumCols }>
                                <option value="1">1</option>
                                <option value="2">2</option>
                                <option value="5">5</option>
                                <option value="10">10</option>
                                <option value="12">12</option>
                                <option value="15">15</option>
                                <option value="20">20</option>
                            </select>
                        </form>
                    </div>    
                </div>
            );
        }
    });
    ReactDOM.render(
        <Grid />,
        document.getElementById("container")
    );

You can see in the JSbin when you change the number of rows or columns with one of the dropdowns, nothing will happen the first time. The next time you change a dropdown value, the grid will draw to the previous state's row and column values. I am guessing this is happening because my this.drawGrid() function is executing before setState is complete. Maybe there is another reason?

Thanks for your time and help!

Javascript Solutions


Solution 1 - Javascript

setState(updater[, callback]) is an async function:

https://facebook.github.io/react/docs/react-component.html#setstate

You can execute a function after setState is finishing using the second param callback like:

this.setState({
    someState: obj
}, () => {
    this.afterSetStateFinished();
});

The same can be done with hooks in React functional component:

https://github.com/the-road-to-learn-react/use-state-with-callback#usage

Look at useStateWithCallbackLazy:

import { useStateWithCallbackLazy } from 'use-state-with-callback';

const [count, setCount] = useStateWithCallbackLazy(0);

setCount(count + 1, () => {
   afterSetCountFinished();
});

Solution 2 - Javascript

render will be called every time you setState to re-render the component if there are changes. If you move your call to drawGrid there rather than calling it in your update* methods, you shouldn't have a problem.

If that doesn't work for you, there is also an overload of setState that takes a callback as a second parameter. You should be able to take advantage of that as a last resort.

Solution 3 - Javascript

Making setState return a Promise

In addition to passing a callback to setState() method, you can wrap it around an async function and use the then() method -- which in some cases might produce a cleaner code:

(async () => new Promise(resolve => this.setState({dummy: true}), resolve)()
    .then(() => { console.log('state:', this.state) });

And here you can take this one more step ahead and make a reusable setState function that in my opinion is better than the above version:

const promiseState = async state =>
    new Promise(resolve => this.setState(state, resolve));

promiseState({...})
    .then(() => promiseState({...})
    .then(() => {
        ...  // other code
        return promiseState({...});
    })
    .then(() => {...});

This works fine in React 16.4, but I haven't tested it in earlier versions of React yet.

Also worth mentioning that keeping your callback code in componentDidUpdate method is a better practice in most -- probably all, cases.

Solution 4 - Javascript

With hooks in React 16.8 onward, it's easy to do this with useEffect

I've created a CodeSandbox to demonstrate this.

useEffect(() => {
  // code to be run when state variables in
  // dependency array changes
}, [stateVariables, thatShould, triggerChange])

Basically, useEffect synchronises with state changes and this can be used to render the canvas

import React, { useState, useEffect, useRef } from "react";
import { Stage, Shape } from "@createjs/easeljs";
import "./styles.css";

export default function App() {
  const [rows, setRows] = useState(10);
  const [columns, setColumns] = useState(10);
  let stage = useRef()

  useEffect(() => {
    stage.current = new Stage("canvas");
    var rectangles = [];
    var rectangle;
    //Rows
    for (var x = 0; x < rows; x++) {
      // Columns
      for (var y = 0; y < columns; y++) {
        var color = "Green";
        rectangle = new Shape();
        rectangle.graphics.beginFill(color);
        rectangle.graphics.drawRect(0, 0, 32, 44);
        rectangle.x = y * 33;
        rectangle.y = x * 45;

        stage.current.addChild(rectangle);

        var id = rectangle.x + "_" + rectangle.y;
        rectangles[id] = rectangle;
      }
    }
    stage.current.update();
  }, [rows, columns]);

  return (
    <div>
      <div className="canvas-wrapper">
        <canvas id="canvas" width="400" height="300"></canvas>
        <p>Rows: {rows}</p>
        <p>Columns: {columns}</p>
      </div>
      <div className="array-form">
        <form>
          <label>Number of Rows</label>
          <select
            id="numRows"
            value={rows}
            onChange={(e) => setRows(e.target.value)}
          >
            {getOptions()}
          </select>
          <label>Number of Columns</label>
          <select
            id="numCols"
            value={columns}
            onChange={(e) => setColumns(e.target.value)}
          >
            {getOptions()}
          </select>
        </form>
      </div>
    </div>
  );
}

const getOptions = () => {
  const options = [1, 2, 5, 10, 12, 15, 20];
  return (
    <>
      {options.map((option) => (
        <option key={option} value={option}>
          {option}
        </option>
      ))}
    </>
  );
};

Solution 5 - Javascript

when new props or states being received (like you call setState here), React will invoked some functions, which are called componentWillUpdate and componentDidUpdate

in your case, just simply add a componentDidUpdate function to call this.drawGrid()

here is working code in JS Bin

as I mentioned, in the code, componentDidUpdate will be invoked after this.setState(...)

then componentDidUpdate inside is going to call this.drawGrid()

read more about component Lifecycle in React https://facebook.github.io/react/docs/component-specs.html#updating-componentwillupdate

Solution 6 - Javascript

I had to run some function after updating the state and not on every update of state.
My scenario:

const [state, setState] = useState({
		matrix: Array(9).fill(null),
		xIsNext: true,
	});

...
...

setState({
    matrix: squares,
	xIsNext: !state.xIsNext,
})
sendUpdatedStateToServer(state);

Here sendUpdatedStateToServer() is the required function to run after updating the state. I didn't want to use useEffect() as I do not want to run sendUpdatedStateToServer() after every state updates.

What worked for me:

const [state, setState] = useState({
		matrix: Array(9).fill(null),
		xIsNext: true,
	});

...
...
const newObj = {
    matrix: squares,
	xIsNext: !state.xIsNext,
}
setState(newObj);
sendUpdatedStateToServer(newObj);

I just created a new object which is required by the function to run after the state updates and and simply used it. Here the setState function will keep on updating the state and the sendUpdatedStateToServer() will receive the updated state, which is what I wanted.

Solution 7 - Javascript

Here is a better implementation

import * as React from "react";

const randomString = () => Math.random().toString(36).substr(2, 9);

const useStateWithCallbackLazy = (initialValue) => {
  const callbackRef = React.useRef(null);
  const [state, setState] = React.useState({
    value: initialValue,
    revision: randomString(),
  });

  /**
   *  React.useEffect() hook is not called when setState() method is invoked with same value(as the current one)
   *  Hence as a workaround, another state variable is used to manually retrigger the callback
   *  Note: This is useful when your callback is resolving a promise or something and you have to call it after the state update(even if UI stays the same)
   */
  React.useEffect(() => {
    if (callbackRef.current) {
      callbackRef.current(state.value);

      callbackRef.current = null;
    }
  }, [state.revision, state.value]);

  const setValueWithCallback = React.useCallback((newValue, callback) => {
    callbackRef.current = callback;

    return setState({
      value: newValue,
      // Note: even if newValue is same as the previous value, this random string will re-trigger useEffect()
      // This is intentional
      revision: randomString(),
    });
  }, []);

  return [state.value, setValueWithCallback];
};

Usage:

const [count, setCount] = useStateWithCallbackLazy(0);

setCount(count + 1, () => {
   afterSetCountFinished();
});

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questionmonalisa717View Question on Stackoverflow
Solution 1 - JavascriptsytolkView Answer on Stackoverflow
Solution 2 - JavascriptJustin NiessnerView Answer on Stackoverflow
Solution 3 - JavascriptMahdiView Answer on Stackoverflow
Solution 4 - Javascriptsudo bangbangView Answer on Stackoverflow
Solution 5 - JavascriptKennedy YuView Answer on Stackoverflow
Solution 6 - Javascriptgg-dev-05View Answer on Stackoverflow
Solution 7 - JavascriptabhijithvijayanView Answer on Stackoverflow