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!
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();
});
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.
render: function() { this.drawGrid(); return......
render()
... sytolk's answer should be the accepted one
setState
should be used a last resort. I agree with most people here that it makes sense to use that approach.
react
the vdom would take care of not doing too much work in most cases, this is interop with another library you want to minimize
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.
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>
))}
</>
);
};
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
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.
Although this question is approached by a class component since the new recommended way of creating components is by functions, this answer addresses the problem from the functional hooks that react introduced on React v16
import { useState, useEffect } from "react";
const App = () => {
const [count, setCount] = useState(0);
useEffect(() => console.log(count), [count]);
return (
<div>
<span>{count}</span>
<button onClick={() => setCount(count + 1)}>Click Me</button>
</div>
);
};
as you can see in this example this is a simple counter component. but the useEffect
hook of this example has a second argument as an array of dependencies (a dependent state that it might rely on). so the hook only runs if the count
is updated. when an empty array is passed, useEffect
runs only once since there are no dependent state variables for it to listen to.
A simple but an effective guide to react hooks - 10 React Hooks Explained | Fireship
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();
});
if(oldState !== newValue) setState(newValue)
gennerally it sounds like wrong architecture and lack of performance
Success story sharing
useState
hook doesn't have it and we should useuseEffect
and many dirty codes to settle it.index.js:1451 Warning: State updates from the useState() and useReducer() Hooks don't support the second callback argument. To execute a side effect after rendering, declare it in the component body with useEffect().