skip to Main Content

I have some very concurrent situation where the state of the app is changed in response to many different events that happen concurrently, so the classic setState(state) doesn’t work, and I have to use setState(state => { return {...state + extra } }) which works on the uncommitted state (it means other events can add alterations to it before it is used for the next re-render), but at the same time I need to return some results back via a promise WHEN THE STATE IS COMMITTED (after all concurrent changes have been applied to it), so I go:

function applyChangesToState(param1, param2) {
    return new Promise(resolve => {
        this.setState(state => {
            // some compuations here
            const extra = calculateSomeExtra(state, param1, param2);
            return {...state, extra };
        , () => {
            // HOW CAN I ACCESS THE COMMITED (MOST RECENT AND FINAL) STATE HERE?
            const { extra } = state; // <-- WHERE SHOULD I READ THE COMMITTED STATE FROM?
            // SO I CAN PUT IT IN RESOLVE:
            resolve(extra);

            // IF I READ FROM this.state HERE IS IT CERTAIN THAT 
            // ALL CONCURRENT CHANGES HAVE BEEN APPLIED ALREADY PRIOR TO
            // THE NEXT RE-RENDER?
        });
    });
}

Basically my question is How, by initiating a state change, can I get the most final state prior to it being sent to re-render via a callback?

2

Answers


  1. Remember that React is a user interface framework. Your component’s props and state are the only authorities when it comes to what data is available for the current render pass: only change them when the user needs to see something new, not blindly "when you have new data" =)

    In this case, you absolutely don’t want to update state just because you data sources pushed new data your way, you want to track all those changes outside of React, so you can update your component state only as fast as makes sense for humans to see.

    For example:

    export class Debouncer() {
      state = {};
      lastRun = 0;
    
      constructor(initialState, setState, interval = 500) {
        this.setState = setState;
        this.interval = interval;
        this.update(initialState);
      }
    
      update(data) {
        // commit the change in this debouncer and reset the debounce timer
        clearTimeout(this.debounce);
        Object.assign(this.state, data);
    
        // Then check if we need to trigger a setState now, or later:
        const now = Date.now();
        const delta = now - this.lastRun;
        if (delta > this.interval) {
          return this.send();
        } else {
          this.debounce = setTimeout(
            () => this.send(),
            this.interval
          );
        }
      }
    
      send() {
        this.lastRun = Date.now();
        this.setState(this.state);
      }
    }
    

    And then you import that and use that as your data handler, rather than overloading setState with data.

    import { useState } from "react";
    import { Debouncer } from "./wherever.js";
    
    export function MyComponent(props) {
      const [state, setState] = useState({});
    
      useEffect(() => {
        const debouncer = new Debouncer(state, setState, 250);
        
        // even if this thing generates updates every millisecond,
        // our component isn't going to update every millisecond,
        // because we're not updating the component state, we're
        // updating the debouncer state.
        highFrequencyUpdatingAPI.realtime.listen(
          `stock price updates or whatever`,
          (data) => debouncer.update(data)
        );
      }, []);
    
      return <div>{things based on state}</div>;
    }
    
    Login or Signup to reply.
  2. As you know, JavaScript runs the program in a single thread, therefore there is no inherent concurrently in it. When it starts executing an event, it does not stop until it finishes the event. Therefore events cannot run concurrently.
    As a result, the states cannot be updated concurrently as well.

    Even in the cases of using asynchronous calls or Worker threads, the main thread only has the direct access to the states. The results from asynchronous calls and Worker threads have to be communicated through the main thread to get the states updated. Therefore no concurrent state updates in such cases as well.

    Besides that, React does not batch state updates across intentional events. It batches state updates whenever it is safe to do. This ensures that, for example, if the first button click disables a form, the second click would not submit it again. This can be read from here : React batches state updates

    Therefore the real difference between a updater function and an expression or "replace with value" comes only when there are multiple or a series of state updates in a single same event. If there is a series of state updates in the same single event, then a updater function is required to get the intended result, otherwise an expression would do the same job.

    Based on the above points, the answer to the question may be to compute locally the series of state updates in the event. This manual computation should yield the same result of the state in the subsequent render. You may be able to refer to a similar Q&A here: Before function flow is not finished yet and setState update is not done, get latest state for debugging purpose

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search