279

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!

HoldOffHunger
  • 18,769
  • 10
  • 104
  • 133
monalisa717
  • 3,187
  • 3
  • 18
  • 17

9 Answers9

628

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();
});
sytolk
  • 7,223
  • 3
  • 25
  • 38
  • 49
    or just this.setState({ someState: obj }, this.afterSetStateFinished ); – Aleksei Prokopov Jul 17 '18 at 13:21
  • 6
    The lovely feature that the setter function of `useState` hook doesn't have it and we should use `useEffect` and many dirty codes to settle it. – AmerllicA Sep 26 '20 at 09:40
  • 4
    Sadly if you try to use this solution with Hooks, you will get this error: `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().` – Des Mar 31 '21 at 13:05
  • @Des can you write your issue here: https://github.com/the-road-to-learn-react/use-state-with-callback/issues and share the source code. Are you sure that you use useStateWithCallbackLazy Hook? – sytolk Apr 01 '21 at 06:54
  • @sytolk I'm sorry. I was so tired when I was looking for a solution to this that my mind just skipped that part. My comment is for regular useState. That's my bad. – Des Apr 01 '21 at 11:44
  • I checked this hook. Interesting. As React batches `setState` updates, how does this hook behave in cases when multiple `setStates` are called? I suspect that the latest callback will just override the previous one. – tonix Oct 05 '21 at 23:31
30

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.

Justin Niessner
  • 242,243
  • 40
  • 408
  • 536
  • 2
    Thank you - your first suggestion works and (mostly) makes sense. I did this: `render: function() { this.drawGrid(); return......` – monalisa717 Jan 08 '16 at 22:37
  • 3
    Ahem, please don't do this in `render()`... sytolk's answer should be the accepted one – mfeineis Feb 28 '18 at 08:18
  • This is a wrong answer, setState will go for infintie loop and will crash the page. – Maverick Apr 08 '18 at 19:32
  • Justin, I"m interested in why said the callback of `setState` should be used a last resort. I agree with most people here that it makes sense to use that approach. – monalisa717 Sep 14 '18 at 19:31
  • @mfeineis why not do this in render? – tim-phillips Oct 16 '18 at 16:11
  • 1
    @Rohmer this is way to expensive to execute on every render call while it's obviously not needed on every call either. If it was pure `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 – mfeineis Apr 14 '19 at 07:24
20

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.

Mahdi
  • 9,247
  • 9
  • 53
  • 74
20

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>
      ))}
    </>
  );
};
sudo bangbang
  • 27,127
  • 11
  • 75
  • 77
12

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

Kennedy Yu
  • 361
  • 3
  • 4
2

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.

gg-dev-05
  • 353
  • 8
  • 14
1

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

Hyperx837
  • 773
  • 5
  • 13
0

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).

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();
});
isherwood
  • 58,414
  • 16
  • 114
  • 157
abhijithvijayan
  • 835
  • 12
  • 17
  • What is better? and if you think its better why not make a PR here: https://github.com/the-road-to-learn-react/use-state-with-callback/pulls for me this think with revision is overhead – sytolk Jun 30 '21 at 17:03
  • I agree with you but isnt the hook supposed to call the callback once the state is updated (even if there are no visual changes). In my case that was crucial as the callback itself was resolving a promise. – abhijithvijayan Jul 01 '21 at 16:16
  • Simple you can try to check if state is changed before update state and rerender component: `if(oldState !== newValue) setState(newValue)` gennerally it sounds like wrong architecture and lack of [performance](https://felixgerschau.com/react-performance-react-memo/#solving-this-with-areequal) – sytolk Jul 15 '21 at 07:35
  • 1
    If you need to force update functional component this is the oficial React recomendation for this: https://reactjs.org/docs/hooks-faq.html#is-there-something-like-forceupdate – sytolk Sep 01 '21 at 12:51
0

For this issue I prefer to use useEffect

const MyComponent: FC = () => {
 const [count, setCount] = useState(0); 
 const ref = useRef(false)

 const handeIncreaseCount = () => {
  setCount(count => count + 1)
  ref.current = true;
 }

 useEffect(() => {
   if(ref.current) {
     // here your state is updated so you can perform your action that you want to be executed  
     ref.current = flase;
   }
 }, [count])

 return (
    ...
     <button onClick={() => handeIncreaseCount()}>Increment</button>
    ...
 )
}

The problem of using setState call backs, is that you are causing double re-renders that may affect negatively your app performance