Can I execute a function after setState is finished updating?
JavascriptReactjsSetstateJavascript 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
setState
return a Promise
Making 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();
});