Async Await некорректно работает с useStateCallback

#javascript #reactjs #async-await #es6-promise

#javascript #reactjs #async-await #es6-обещание

Вопрос:

Мне нужно обновить то же состояние в последовательности:

 // lets say this is the state
const [someState, setSomeState] = useState({a: 1, b: 1})

// and i need to update it like so
// from an external function     
setSomeState({a: 2})
// wait until the first one is actually updates
// and do the next one  
setSomeState({b: 2})
// wait until the second one is actually updates
// "tell" the parent function that both are updated
  

Теперь я делаю это:

 // i use a customized hook useStateCallback
function useStateCallback(initialState) {
  const [state, setState] = useReducer((state, newState) => ({ ...state, ...newState }), initialState)
  const cbRef = useRef(null)

  const setStateCallback = (state, cb) => {
    cbRef.current = cb
    setState(state)
  }

  useEffect(() => {
    if (cbRef.current) {
      cbRef.current(state)
      cbRef.current = null
    }
  }, [state])

  return [state, setStateCallback]
}

// and execute it like so
// from outside of a component
export const someFunction = async setState => {
    await setState({ a: 2 }, () => Promise.resolve())
    await setState({ b: 2 }, () => Promise.resolve())
}
  

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

Итак, мои вопросы:

  1. Как перехват useStateCallback работает в сочетании с Promise.resolve() ?
  2. Как Promise.resolve() работает в этом конкретном случае?
  3. Как мне обновлять одно и то же состояние одно за другим, когда второе будет обновляться только при обновлении первого (с помощью перехватов) и «сообщать» родительской функции, что они оба завершили свои обновления?

Комментарии:

1. Нам нужно больше информации о том, что setState() такое. Что это возвращает? Какие аргументы он принимает?

Ответ №1:

Проблема с вашей настройкой заключается в том, что setStateCallback устанавливает ссылку обратного вызова, а затем возвращает. И затем вы вызываете этот обратный вызов из useEffect. Проблема здесь в том, что к моменту вызова useEffect исходная функция вызова уже завершена.

Если мы просто хотим убедиться, что дождемся завершения setState, то мы бы решили это как таковое, добавив useEffect в setStateCallback и обернув его в обещание. В основном это будет означать, разрешится только в том случае, если useEffect завершится. В противном случае просто подождите

   const setStateCallback = (state, cb) => {
    return new Promise((resolve) => {
       cbRef.current = cb
       setState(state)
       useEffect(async () => {
          if (cbRef.current) {
            await cbRef.current(state) // Calls your wrap callback promise here in case there's anything you want to do. Otherwise you don't need that wrapper callback anymore and we can remove this await.
            cbRef.current = null
          }
          resolve();  --> This will basically resolve once useEffect is done calling the functions it needs to call. 
       }, [state])
  });
  

Ответ №2:

Ваш setStateCallback не возвращает Promise , но принимает обратный вызов; вот почему он не может быть вызван напрямую с помощью await синтаксиса. Что вам нужно, так это просто оболочка, чтобы обещать это.

Что-то вроде:

 function useStateCallback(initialState) {
  const [state, setState] = useReducer((state, newState) => ({ ...state,
    ...newState
  }), initialState)
  const cbRef = useRef(null)

  const setStateCallback = (state, cb) => {
    cbRef.current = cb
    setState(state)
  }

  useEffect(() => {
    if (cbRef.current) {
      cbRef.current(state)
      cbRef.current = null
    }
  }, [state])

  const setStateAsync = state => new Promise((resolve, reject) => setStateCallback(state, resolve))

  return [state, setStateCallback, setStateAsync]
}
  

Теперь вы можете инициализировать свое состояние как:

 const [state, setStateCallback, setStateAsync] = useStateCallback({})
  

С этого момента вы могли бы использовать async форму setStateAsync . Это привело к исправлению clear функции (на этот раз она принимает async функцию):

 export const clear = async setState => {
    await setState({ isWritable: false })
    await setState({ isVisible: false })
}
  

должно позволить вам использовать:

 await clear(setStateAsync);
  

с ожидаемым async поведением.

Комментарии:

1. Your setStateCallback does not returns a Promise, but it accept a callback; that's why it can't be directly called with the await syntax но почему он перенастраивает обещание, которое я могу видеть в консоли и даже получить его значение (если я передаю его через обратный вызов)? Я действительно хочу понять, как это работает и чего мне не хватает

2. Способ, которым OP написал свою оболочку, уже возвращает обещание. Поэтому нет необходимости выполнять еще один перенос поверх него. Я согласен, что мы могли бы возвращать обещание в useStateCallback, чтобы нам не приходилось каждый раз создавать оболочку. Я думаю, что реальная проблема заключается в чем-то другом. Вот его оболочка для справки. await setState({ a: 2 }, () => Promise.resolve()) Он в основном вызывает setState и выдает ему обратный вызов, который возвращает обещание.