Issue with async value for React context not available from child component
I am trying to consume a React context that is populated from a js promise.
The issue I have is that the child component consuming the context (i.e. AnotherPage
) is rendered before the value from the context is available from the useEffect
called in the root App
page. So that if I try to access one of the context properties, I will get an error since it is undefined/null
.
How can I refactor/redesign my app so that the context is available from the child component?
Here is the code for my app:
import React from 'react';
import { createContext, useEffect, useState, useContext } from 'react';
import './style.css';
const MyContext = createContext(null);
export default function App() {
console.log('App');
const [myObj, setMyObj] = useState();
useEffect(() => {
Promise.resolve({ id: 42, text: 'some value from promise' }).then((res) => {
console.log('resolved: ', res);
setMyObj(res);
});
}, []);
return (
<MyContext.Provider value={myObj}>
<div>
<h1>Hello StackBlitz!</h1>
<AnotherPage />
</div>
</MyContext.Provider>
);
}
const AnotherPage = () => {
console.log('AnotherPage');
const obj = useContext(MyContext);
console.log('myObj', obj);// undefined/null at first page render...
return <div>Hello from another page</div>;
};
Stackblitz here: https://stackblitz.com/edit/react-xkxhxk?file=src/App.js
FYI, in my real app the actual object resolved from a promise is much more complex that the myObj
below.
Solution 1:
You can't escape it, you must check nullability somewhere:
- Within every consumer (
useContext
) user, the easiest way is to write a custom hook and check for nullability there. - Or, within the provider itself, conditionally render children
Notice that it isn't a "first render" problem since its a promise, you can delay it for enough time that it will update after the first render.
export default function App() {
const [myObj, setMyObj] = useState();
useEffect(() => {
Promise.resolve({ id: 42, text: 'some value from promise' }).then((res) => {
setTimeout(() => {
setMyObj(res);
}, 2000);
});
}, []);
const children = useMemo(
() => (
<div>
<h1>Hello StackBlitz!</h1>
<AnotherPage />
</div>
),
[]
);
return (
<MyContext.Provider value={myObj}>{myObj && children}</MyContext.Provider>
);
}
https://stackblitz.com/edit/react-a4s9yr?file=src/App.js
Solution 2:
I think you can use a loader until the promise resolves. Such that the child components doesn't even get rendered before the promise resolves.
import React from 'react';
import { createContext, useEffect, useState, useContext } from 'react';
import './style.css';
const MyContext = createContext(null);
export default function App() {
console.log('App');
const [myObj, setMyObj] = useState();
useEffect(() => {
Promise.resolve({ id: 42, text: 'some value from promise' }).then((res) => {
console.log('resolved: ', res);
setMyObj(res);
});
}, []);
return (
<MyContext.Provider value={myObj}>
{
myObj ? <div>
<h1>Hello StackBlitz!</h1>
<AnotherPage />
</div> : <div>Loading....</div>
}
</MyContext.Provider>
);
}
const AnotherPage = () => {
console.log('AnotherPage');
const obj = useContext(MyContext);
console.log('myObj', obj);// undefined/null at first page render...
return <div>Hello from another page</div>;
};