How to wait for React useEffect hook finish before moving to next screen

Viewed 10270

I'm having issue where hook is being used in multiple files and it is being called twice for useEffect before the 1st one's async method finish (which should block the 2nd hook call, but it's not). See below 2 scenarios:

Stack Navigator

const { context, state } = useLobby(); // Hook is called here 1st, which will do the initial render and checks

return (
  <LobbyContext.Provider value={context}>
    <LobbyStack.Navigator>
      {state.roomId
        ? <LobbyStack.Screen name="Lobby" component={LobbyScreen} />
        : <LobbyStack.Screen name="Queue" component={QueueScreen} />
      }
    </LobbyStack.Navigator>
  </LobbyContext.Provider>
)

Lobby Hooks

export const useLobby = () => {
  const [state, dispatch] = React.useReducer(...)

  //
  // Scenario 1
  // This get called twice (adds user to room twice)
  //
  React.useEffect(() => {
    if (!state.isActive) assignRoom();
  }, [state.isActive])

  const assignRoom = async () => {
    // dispatch room id
  }

  const context = React.useMemo(() => ({
    join: () => { assignRoom(); }
  })
}

Queue Screen

const { context, state } = useLobby(); // Hook is called here 2nd right after checking state from stack navigator

//
// Scenario 2
// Only does it once, however after state is changed to active
// the stack navigator didn't get re-render like it did in Scenario 1
//
React.useEffect(() => {
  roomLobby.join();
}, []);

return (
...
  {state.isActive
    ? "Show the room Id"
    : "Check again"
...
)

In scenario 1, I guess while 1st hook is called and useEffect is doing async to add user to the room and set active to true. Meanwhile the conditional render part is moving straight to Queue screen which calls the hook again and doing the useEffect (since 1st haven't finished and isActive is still false).

How can I properly setup useReducer and useMemo so that it renders the screen base on the state.

Edited codes based on the answer

/* LobbyProvider */

const LobbyContext = React.createContext();

const lobbyReducer = (state, action) => {
  switch (action.type) {
    case 'SET_LOBBY':
      return {
        ...state,
        isActive: action.active,
        lobby: action.lobby
      };
    case 'SET_ROOM':
      return {
        ...state,
        isQueued: action.queue,
        roomId: action.roomId,
      };
    default:
      return state;
  }
}

const LobbyProvider = ({ children }) => {
  const [state, dispatch] = React.useReducer(lobbyReducer, initialState);

  React.useEffect(() => {
    console.log("Provider:", state)
    if (!state.isActive) joinRoom();
  }, [])

  // Using Firebase functions
  const joinRoom = async () => {
    try {
      const response = await functions().httpsCallable('getActiveLobby')();
      if (response) {
        dispatch({ type: 'SET_LOBBY', active: true, lobby: response.data })
        const room = await functions().httpsCallable('assignRoom')({ id: response.data.id });
        dispatch({ type: 'SET_ROOM', queue: false, roomId: room.data.id })
      }
    } catch (e) {
      console.error(e);
    }
  }

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


/* StackNavigator */ 

const {state} = React.useContext(LobbyContext);

return (
  <LobbyProvider>
    // same as above <LobbyStack.Navigator>
    // state doesn't seem to be updated here or to re-render
  </LobbyProvider>
);


/* Queue Screen */

const {state} = React.useContext(LobbyContext);

// accessing state.isActive to do some conditional rendering
// which roomId does get rendered after dispatch
1 Answers

You must note that a custom hook will create a new instance of state everytime its called.

For example, you call the hook in StackNavigator component and then again in QueueScreen, so 2 different useReducers will be invoked instead of them sharing the states.

You should instead use useReducer in StackNavigator's parent and then utilize that as context within useLobby hook

const LobbyStateContext = React.createContext();

const Component =  ({children}) => {
   const [state, dispatch] = React.useReducer(...)
   return (
      <LobbyStateContext.Provider value={[state, dispatch]]>
         {children}
     </LobbyStateContext>
   )
}

and use it like

<Component>
  <StackNavigator />
</Component>

useLobby will then look like

export const useLobby = () => {
  const [state, dispatch] = React.useContext(LobbyStateContext)

  const assignRoom = async () => {
    // dispatch room id
  }

  const context = React.useMemo(() => ({
    join: () => { assignRoom(); }
  })

  return { context, assignRoom, state};
}

StackNavigator will utilize useLobby and have the useEFfect logic

const { context, state, assignRoom } = useLobby(); 

React.useEffect(() => {
    if (!state.isActive) assignRoom();
}, [state.isActive])

return (
  <LobbyContext.Provider value={context}>
    <LobbyStack.Navigator>
      {state.roomId
        ? <LobbyStack.Screen name="Lobby" component={LobbyScreen} />
        : <LobbyStack.Screen name="Queue" component={QueueScreen} />
      }
    </LobbyStack.Navigator>
  </LobbyContext.Provider>
)
Related