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:

  1. Within every consumer (useContext) user, the easiest way is to write a custom hook and check for nullability there.
  2. 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>;
};