Как создать атомарный процесс в React (async / await)?

#javascript #reactjs #react-native #asynchronous #async-await

#javascript #reactjs #react-native #асинхронный #async-await

Вопрос:

Представьте пост, который может понравиться при нажатии кнопки. Эта кнопка изменяет удаленную базу данных, поэтому потребуется некоторое время, чтобы связать like с конкретным сообщением.

Теперь, если пользователь начинает так быстро нажимать кнопку с помощью этого кода:

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

Поскольку все асинхронно, можно увидеть эту ситуацию:

Нравится

Не нравится

ГОТОВО <———- Нелюбовь к сделанному

ГОТОВО <———- Симпатия сделана

Я подумал создать состояние «isLiking», чтобы избежать запуска кода до завершения всего асинхронного задания. Что-то вроде этого:

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

С этим все идет нормально, но если пользователь быстро нажмет кнопку, он не сможет увидеть изменения в графическом интерфейсе (цвет кнопки «Нравится» (красный, если нравится, белый, если нет)), пока не завершится весь процесс, описанный в приведенном выше коде.

Я также подумал о том, чтобы создать деактивированную функцию (для ручного нажатия) следующим образом:

 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 amp;amp; !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
  

Но при этом единственное, что я делаю, это уменьшаю вероятность получения беспорядочных результатов. То есть у меня все та же проблема, что и с первым кодом.

Есть идеи делать то, что я хочу, таким образом, чтобы результаты, которые я получаю, упорядочивались и избегали небольшого ожидания записи в базу данных?

Спасибо.

Ответ №1:

Решение состоит в том, чтобы немедленно отключить кнопку. Используя setState , вы не можете ожидать немедленного обновления isLinking , и именно поэтому вы раздражены. Одним из решений является использование flag variable вместо использования state .

Вы можете исправить это таким образом.

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

Ответ №2:

Ответ @Prime работает, но он не работает, когда у вас есть действия, которые разбросаны по всему приложению, и было бы сложно все синхронизировать.

В моем случае это было обновление токена API. Поскольку запросы API разбросаны по всему приложению, заблокировать вызов с помощью переменной состояния практически невозможно.

Поэтому я представляю другое решение:

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