React/React Context API: Wait for context values when using useEffect() hook
Asked Answered
B

4

6

I am developing a web app using React and here is what I am trying to do:

  1. In the highest-order component, check whether the user is logged in and if logged in, update 'userinfo' Context value.
  2. In Profile.js, get the 'userinfo' Context value.
  3. Check whether 'userinfo' is null or not, and call different APIs accordingly.

I wrote the below code. The problem is that there is apparently a time lag for the userinfo context value to be delivered to the component. So when using useEffect() hook, Profile.js will render twice when userinfo is not null.

Is there a way to fix this code so that it waits for 'userinfo' variable, and then call relevant APIs accordingly, not before?

Below is the code. Any advice? Thanks a lot in advance!

import React, {useEffect, useContext} from 'react';
import Userinfo from "../context/Userinfo";


function Profile(props) {

   const {userinfo, setuserinfo}=useContext(Userinfo);

   useEffect(()=>{      
       ((userinfo==null)?
       /*API call A (When user is not logged in) */ :
       /*API call B (When user is logged in) */
       )},[userinfo])  

   return (
       (userinfo==null)?
       /*Elements to render when user is not logged in) */ :
       /*Elements to render when user is  logged in) */
   );
}

export default Profile;
Beverly answered 28/4, 2020 at 11:26 Comment(0)
K
12

The best solution here is to add a loading state in the context provider which is reset once the value is updated.

function Profile(props) {

   const {userinfo, loading, setuserinfo}=useContext(Userinfo);

   useEffect(()=>{      
       if(!loading) {
            ((userinfo==null)?
             /*API call A (When user is not logged in) */ :
            /*API call B (When user is logged in) */
            )
       }
    )},[loading, userinfo])  

   if(loading) return <Loader />
   return (
       (userinfo==null)?
       /*Elements to render when user is not logged in) */ :
       /*Elements to render when user is  logged in) */
   );
}
Kamerad answered 28/4, 2020 at 11:46 Comment(1)
Beware, if you are trying to use values from your context for downstream hooks, you will run into hook hell. So if your context is mandatory you may be better off delaying the loading of this child component until the context is fully baked.Kimmi
S
2

Lets assume you are setting up a context like below:

import React from 'react';
const AuthContext = React.createContext({
    userInfo: null,
    isLoggedIn: false,
    loading: true,
    setUser: () => {} 
});

export default AuthContext;

And you are setting up your Global Provider like this:

import React from 'react';
import  AuthContext  from './AuthContext'; // path to the context

export default class GlobalUserContext extends React.Component{
    constructor(props){
        super(props);
        this.state = {
            userInfo: null,
            isLoggedIn: false,
            loading: true
            }
    }

    componentDidMount(){
        ( async() => {
            const currentUser = await getUser(); // say you are trying to log user in
            if(currentUser){
                this.setState({
                    userinfo: currentUser,
                    isLoggedIn: true,
                    loading: false
                });  
            }

        } )();
    }
    
    setUser = (user, check) => {
        this.setState({userInfo: user, isLoggedIn: check});
    };
    
    render(){
        return (
            <AuthContext.Provider 
            value={{
                user: this.state.user,
                setUser: this.setUser,
                isLoggedIn: this.state.isLoggedIn,
                loading: this.state.loading
            }}>
            {this.props.children} 
            </AuthContext.Provider>
        );
    }

}

Then in your functional component using useEffect,you can do something like this:

import React, { useContext, useEffect} from 'react';
import AuthContext                     from '../../contexts/AuthContext'; // your content path

export const MyComponent = () => {
const {userInfo, isLoggedIn, loading} = useContext(AuthContext);
useEffect(() => {
// here you can now ask if the user data is still loading like so:
if(!loading){
// do something 
// UserInfo is available
}
}, [userInfo, loading]);
return (
<View>
{anything}
</View>
);
}
Stereotropism answered 21/8, 2021 at 15:11 Comment(0)
A
1

I have this problem today, I like what Shubham does. But the way I solve it is just getting the userinfo from the previous component and pass it to this profile component.

However, make sure you render the Profile component like this so that you can make sure the userinfo exists:

function ParentComponent(){
 // do sth to get the userinfo here
return(
  <ParentComponent>
     {userinfo? <Profile userinfo={userinfo} /> : null}       
 <ParentComponent/>
)
}
Annabelleannabergite answered 19/8, 2020 at 0:14 Comment(0)
B
0

I'm using Typescript and I was able to get around this issue by allowing my context object's typing to be either that object type itself, undefined, or null. I was trying to conditionally render a child component but I wanted to make sure it didn't even attempt to render that component, and run any of it's useEffect() code, until the asynchronous context code had run (default value of context object set to undefined), at which point the context object would equal either the object in storage or be manually set to null (if storage call failed).

Not sure if this is good practice or not, but it was a simple way of using undefined to represent the context's pre-loaded state of the object, and thus, only conditionally render some things if the context object was specifically null or not.

Bombycid answered 10/3, 2024 at 23:6 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.