简体   繁体   中英

How to make an atomic process in React (async/await)?

Imagine a post that can be liked when pressing a button. This button modifies a remote database, so it will take a little while to associate the like to the specific post.

Now, if an user starts pressing the button so fast with this code:

 state = {
    isLiked: false,
 }

 handlePress = () => {
    this.setState(
      {
        isLiked: !this.state.isLiked,
      },
      this.handleLike
    );
  };

  handleLike = async () => {
    const { postId } = this.props;

    try {
      console.log(isLiked ? "Liking" : "Disliking")
      await db.processLike(postId);
    } catch (err) {
      // If an error has occurred, reverse the 'isLiked' state
      this.setState({
        isLiked: !this.state.isLiked,
      });

      // TODO - Alert the error to the user in a toast
      console.log(err);
    }

    console.log("DONE");
  };

As all is async, it is possible to see this situation:

Liking

Disliking

DONE <---------- Disliking done

DONE <---------- Liking done

I have thought to create a state "isLiking" to avoid running the code until all the async job has finished. Something like this:

 state = {
    isLiking: false,
    isLiked: false,
 }

 handlePress = () => {

    if (this.state.isLiking) return; <------------------------------------

    this.setState(
      {
        isLiking: true, <------------------------------------
        isLiked: !this.state.isLiked,
      },
      this.handleLike
    );
  };

  handleLike = async () => {
    const { postId } = this.props;

    try {
      console.log(isLiked ? "Liking" : "Disliking"); 
      await db.processLike(postId);
    } catch (err) {
      // If an error has occurred, reverse the 'isLiked' state
      this.setState({
        isLiked: !this.state.isLiked,
      });

      // TODO - Alert the error to the user in a toast
      console.log(err);
    }

    this.setState({ isLiking: false }); <------------------------------------

    console.log("DONE");
  };

With this all is going OK, but if the user press the button fast, he will not be able to see the GUI changes (the like button color (red if is liked, white if not)) until all the process described in the code above finishes.

I have also thought to make a debounced function (for the handlePress) like this:

export const debounce = (func, wait, immediate) => {
  /*
    Returns a function, that, as long as it continues to be invoked, will not
    be triggered. The function will be called after it stops being called for
    N milliseconds. If `immediate` is passed, trigger the function on the
    leading edge, instead of the trailing.
  */

  let timeout;
  return function () {
    let context = this,
      args = arguments;

    let later = function () {
      timeout = null;
      if (!immediate) func.apply(context, args);
    };

    let callNow = immediate && !timeout;

    clearTimeout(timeout);
    timeout = setTimeout(later, wait);

    if (callNow) func.apply(context, args);
  };
};

...

debuncedHandlePress = debounce(this.handlePress, 500); // Now, when the button is pressed, it will call this function, instead of the original handlePress

But with this, the only thing I do is decrease the chance of getting messy results. That is, I still have the same problem as with the first code.

Any idea to do what I want in such a way that the results I get are ordered and avoiding the little wait for writing to the database?

Thank you.

The solution is to disable the button immediately. Using setState , you cannot expect immediate update of isLinking , and that's why you are annoyed. One of the solution is to use flag variable instead of using state .

You can fix in this way.

 state = {
    isLiked: false,
 }

 constructor(props) {
    this.isLiking = false; <------------------------------------
 }
 

 handlePress = () => {
    this.isLiking = true; <------------------------------------
    this.setState(
      {
        isLiked: !this.state.isLiked,
      },
      this.handleLike
    );
  };

  handleLike = async () => {
    const { postId } = this.props;

    try {
      console.log(isLiked ? "Liking" : "Disliking"); 
      await db.processLike(postId);
    } catch (err) {
      // If an error has occurred, reverse the 'isLiked' state
      this.setState({
        isLiked: !this.state.isLiked,
      });

      // TODO - Alert the error to the user in a toast
      console.log(err);
    }

    this.isLiking = false; <------------------------------------

    console.log("DONE");
  };

The @Prime's answer works, but it falls short when you have actions that are scattered all over the app and it would be hard to synchronize everything.

In my case it was API token refreshing. As API requests are scattered all over the app, it is nearly impossible to block the call with a state variable.

Therefore I present another solution:

/*
    The long running operation
*/

const myLongRunningOperation = async () => {
    // Do an API call, for instance
}

/*
    Promise locking-queueing structure
*/

var promiesCallbacks = [];

const resolveQueue = value => {
  promiesCallbacks.forEach(x => x.resolve(value));
  promiesCallbacks = [];
};
const rejectQueue = value => {
  promiesCallbacks.forEach(x => x.reject(value));
  promiesCallbacks = [];
};
const enqueuePromise = () => {
  return new Promise((resolve, reject) => {
    promiesCallbacks.push({resolve, reject});
  });
};

/*
    The atomic function!
*/

var actionInProgress = false;

const doAtomicAction = () => {
    if (actionInProgress) {
      return enqueuePromise();
    }

    actionInProgress = true;

    return myLongRunningOperation()
      .then(({ access }) => {
        resolveQueue(access);
        return access;
      })
      .catch((error) => {
        rejectQueue(error);
        throw error;
      })
      .finally(() => {
        actionInProgress = false;
      });
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM