score:1

Accepted answer

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

score:0

You can initialize the Context with some temporary value instead of null.

Instead of Doing

const MyContext = createContext(null);

Do

const MyContext = createContext({id: -1, text: 'yet to be initialized'});

So you child component will consume this values until the promise updates the context value.

score:0

I would solve it like this:

const ASYNC_STATUS = {
  Idle: 'idle',
  Pending: 'pending',
  Success: 'success',
  Failed: 'failed',
};

export default function App() {
  console.log('App');

  const [myObj, setMyObj] = useState({
    value: null,
    status: ASYNC_STATUS.Idle,
  });

  useEffect(() => {
    try {
      setMyObj((prev) => ({
        ...prev,
        status: ASYNC_STATUS.Pending,
      }));
      Promise.resolve({ id: 42, text: 'some value from promise' }).then(
        (res) => {
          console.log('resolved: ', res);
          setMyObj({
            value: res,
            status: ASYNC_STATUS.Success,
          });
        }
      );
    } catch {
      setMyObj((prev) => ({
        ...prev,
        status: ASYNC_STATUS.Success,
      }));
    }
  }, []);

  return (
    <MyContext.Provider value={myObj}>
      <div>
        <h1>Hello StackBlitz!</h1>
        <AnotherPage />
      </div>
    </MyContext.Provider>
  );
}

const AnotherPage = () => {
  console.log('AnotherPage');

  const { value, status } = useContext(MyContext);
  console.log('myObj', value);

  return (
    <div>
      {status === ASYNC_STATUS.Pending && <div>Loading...</div>}
      {status === ASYNC_STATUS.Failed && <div>Failed!</div>}
      {status === ASYNC_STATUS.Success && value && <div>here what you want to show</div>}
    </div>
  );
};

score:1

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>;
};

Related Query

More Query from same tag