Kan ik een functie uitvoeren nadat setState klaar is met updaten?

Ik ben erg nieuw bij ReactJS (zoals in, ben net vandaag begonnen). Ik begrijp niet helemaal hoe setStatewerkt. Ik combineer React en Easel JS om een ​​raster te tekenen op basis van gebruikersinvoer. Hier is mijn JS-bak:
http://jsbin.com/zatula/edit?js,output

Hier is de 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")
    );

U kunt in de JSbin zien dat wanneer u het aantal rijen of kolommen wijzigt met een van de vervolgkeuzemenu’s, er de eerste keer niets zal gebeuren. De volgende keer dat u een dropdown-waarde wijzigt, wordt het raster naar de rij- en kolomwaarden van de vorige staat getrokken. Ik vermoed dat dit gebeurt omdat mijn functie this.drawGrid()wordt uitgevoerd voordat setStateis voltooid. Misschien is er een andere reden?

Bedankt voor je tijd en hulp!


Antwoord 1, autoriteit 100%

setState(updater[, callback])is een asynchrone functie:

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

Je kunt een functie uitvoeren nadat setState klaar is met de tweede parameter callbackzoals:

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

Hetzelfde kan worden gedaan met haken in de functionele component React:

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

Kijk naar useStateWithCallbackLazy:

import { useStateWithCallbackLazy } from 'use-state-with-callback';
const [count, setCount] = useStateWithCallbackLazy(0);
setCount(count + 1, () => {
   afterSetCountFinished();
});

Antwoord 2, autoriteit 5%

renderwordt aangeroepen elke keer dat u setStategebruikt om de component opnieuw te renderen als er wijzigingen zijn. Als je je aanroep naar drawGriddaar verplaatst in plaats van het aan te roepen in je update*-methoden, zou je geen probleem moeten hebben.

Als dat niet voor u werkt, is er ook een overbelasting van setStatedie een callback als tweede parameter neemt. Daar zou je als laatste redmiddel van moeten kunnen profiteren.


Antwoord 3, autoriteit 3%

Door setStateeen Promise

te laten retourneren

Naast het doorgeven van een callbacknaar setState()-methode, kunt u deze ook om een ​​async-functie wikkelen en de then()methode — die in sommige gevallen een schonere code kan produceren:

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

En hier kun je nog een stap verder gaan en een herbruikbare setState-functie maken die naar mijn mening beter is dan de bovenstaande versie:

const promiseState = async state =>
    new Promise(resolve => this.setState(state, resolve));
promiseState({...})
    .then(() => promiseState({...})
    .then(() => {
        ...  // other code
        return promiseState({...});
    })
    .then(() => {...});

Dit werkt prima in React16.4, maar ik heb het nog niet getest in eerdere versies van React.

Het is ook de moeite waard om te vermelden dat het in de meeste – waarschijnlijk alle gevallen – beter is om uw callback-code in de componentDidUpdate-methode te houden.


Antwoord 4, autoriteit 2%

wanneer nieuwe rekwisieten of toestanden worden ontvangen (zoals je setStatehier aanroept), zal React enkele functies aanroepen, die componentWillUpdateen componentDidUpdate

voeg in jouw geval gewoon een functie componentDidUpdatetoe om this.drawGrid()

aan te roepen

hier is werkende code in JS Bin

zoals ik al zei, in de code, wordt componentDidUpdateaangeroepen na this.setState(...)

dan gaat componentDidUpdateinside this.drawGrid()

aanroepen

lees meer over component Levenscyclusin React https://facebook.github.io/react/docs/component-specs.html#updating-componentwillupdate


Antwoord 5, autoriteit 2%

Met haken vanaf React 16.8 is het gemakkelijk om dit te doen met useEffect

Ik heb een CodeSandboxgemaakt om demonstreer dit.

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

Kortom, useEffectsynchroniseert met statuswijzigingen en dit kan worden gebruikt om het canvas weer te geven

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>
      ))}
    </>
  );
};

Antwoord 6

Ik moest een functie uitvoeren na het updaten van de staat en niet bij elke update van de staat.
Mijn scenario:

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

Hier is sendUpdatedStateToServer()de vereiste functie die moet worden uitgevoerd na het bijwerken van de status.
Ik wilde useEffect() niet gebruiken omdat ik sendUpdatedStateToServer()niet wil uitvoeren na elke statusupdate.

Wat voor mij werkte:

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

Ik heb zojuist een nieuw object gemaakt dat door de functie moet worden uitgevoerd na de statusupdates en heb het gewoon gebruikt. Hier blijft de setState-functie de status bijwerken en de sendUpdatedStateToServer()ontvangt de bijgewerkte status, wat ik wilde.


Antwoord 7

Hier is een betere implementatie

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];
};

Gebruik:

const [count, setCount] = useStateWithCallbackLazy(0);
setCount(count + 1, () => {
   afterSetCountFinished();
});

Other episodes