0

There is a Profile component that creates a 3D face using an async function. To inform the user to wait, two dispatches is fired before and after the heavy task inside the UseEffect. The problem is both of these dispatched only fire after the heavy task is done.

Profile.js

const Profile = (props) => {
  const [face3D, setFace3D] = useState(null)
  const { state, dispatch } = useContext(UserContext)

  const drawFace = async () => {
    // a heavy calculation
  }

  useEffect(() => {
    // show user loading
    dispatch({
      type: "USER_LOADING",
      payload: true
    })

    drawFace().then( face => {
      setFace3D(face)

      // clear user loading
      dispatch({
        type: "USER_LOADING",
        payload: false
      })
    })

  }, [props.data])

  return (
    <Canvas>
      { face3D }
    </Canvas>
  )
}

 export default Profile

UserContext.js

import React, { createContext, useReducer } from 'react'
import { createReducer } from 'react-use'
import logger from 'redux-logger'
import thunk from 'redux-thunk'

export const UserContext = createContext()
const useThunkReducer = createReducer(thunk, logger);

const UserContextProvider = (props) => {
const initialState = {
  loading: false
}

const [state, dispatch] = useThunkReducer(UserReducer, initialState)

return (
  <UserContext.Provider value={{ state, dispatch }}>
    { props.children }
  </UserContext.Provider>
)
}

export default UserContextProvider

PS: redux-logger shows a correct sequence of dispatching, even logging the reducer shows changing the state to true and finally to false but the state only reflects the last dispatch (a situation similar to mutating the state, but it is not mutated)

Saeid
  • 83
  • 8

3 Answers3

0

Use finally block and then add loading false dispatch inside that block.

0

Because you've noted that drawFace is a "heavy calculation", I suspect that it is not actually asynchronous. What I think is happening is:

  1. The first dispatch is called
  2. drawFace is executed and calculates its return value. Because the function is async, the result is wrapped in a promise.
  3. .then() adds a task to the event loop that makes the call to setFace3D and the second call to dispatch.

Try calling drawFace after a timeout:

dispatch(...)

setTimeout(() => {
  drawFace().then(face => {
    setFace3D(face);
    dispatch(...)
  })
}, 0)

Marking a function with async doesn't automatically run the function body on a background thread or anything like that. Instead, it does two things:

  • Allows the await keyword to be used
  • Automatically wraps the returned value (or a thrown error) in a promise.

Notice that in the following example, "calculate" is logged before "after calling calculate", even though the function is marked async.

async function calculate() {
  console.log("calculate")
}

console.log("before calling calculate")
calculate().then(() => { 
  console.log("calculation done")
})
console.log("after calling calculate")

// Result:
// "before calling calculate"
// "calculate"
// "after calling calculate"
// "calculation done"
Stephen Jennings
  • 12,494
  • 5
  • 47
  • 66
  • Thanks. I used a `setTimeout` but did not work. Only the change in the state became a function of the delay time. I also removed the `async` and executed it as a normal function. No improvement! Will try using `await` – Saeid Jun 08 '21 at 19:03
0

Thanks to @stephen-jennings explanation, async function is removed and the code changed as below:

const Profile = (props) => {

  const [face3D, setFace3D] = useState(null)
  const [isLoading, setIsLoading] = useState(true)
  const { state, dispatch } = useContext(UserContext)

  const drawFace = () => {
    // a heavy calculation
    // setFace3D( face )
  }

  useEffect(() => {
    
    dispatch({
      type: "USER_LOADING",
      payload: isLoading
    })
    setIsLoading(false)

  }, [face3D])


  useEffect(() => {
    const delay = setTimeout(() => {
      drawFace()
    }, 0)
  }, [props.data])

  return (
    <Canvas>
      { face3D }
    </Canvas>
  )
}

export default Profile
Saeid
  • 83
  • 8