Lance
Lance

Reputation: 741

How to use react suspense with typescript

I havn't found any documentation or examples to explain this but from what I understand to use suspense you must add a Suspense boundary to the parent component and remove async/await from the child component and lazy load it. But when removing async/await it breaks types because everything is a promise.

For example changing await (await fetch('some-api')).json(); to fetch('some-api').json() gives the error .json is not a function.

Values also cannot be typed because const data: MyData = fetchData() will error because fetchData returns Promise<MyData>.

With Suspense

function Parent() {
  return (
    <Suspense fallback={<Spinner />}>
      <Child />
    </Suspense>
  );
}

function Child() {
  const data: MyData[] = fetchData(); //Error: Type 'Promise<MyData[]>' is missing the following properties from type 'MyData[]': length, pop, push, concat, and 29 more
  return (
    <div>{data.map(d => <p>d.name</p>)}</data>
  );
}

Before Suspense

function Child() {
  const [data, setData] = useState<MyData[]>([]);
  
  useEffect(() => {
     async function loadData() {
         setData(await fetchData());
     }

     loadData();
  }, []);

  if (!data.length) return <Spinner />;

  return (
    <div>{data.map(d => <p>d.name</p>)}</data>
  );
}

Upvotes: 4

Views: 3446

Answers (1)

Sean Vieira
Sean Vieira

Reputation: 160043

TL;DR - fetchData's type signature would need to be just Data, not Promise<Data> (because it's actually fetchData(): Data with effect ReactSuspense or fetchData(): Data throws Promise<Data>)

The details

Suspense doesn't make async code block, it hides the async behind indirection. Effectively, it's trying to introduce preemptive scheduling (where the runtime can stop your code at any time and switch to running a different bit of code) into a cooperatively scheduled environment (where the runtime won't stop your code until your code hits a point you've explicitly annotated with "and I yield the floor").

How it works at a hand-wavey level:

  • Run your component <Child />
  • Inside of Child you call fetchData
  • fetchData must signal React in some way "hey, I need you to interrupt Child because we need to wait for some condition before Child can be resumed".
    • throw and catch are the only signaling mechanism that JavaScript provides
    • So fetchData throws a promise and React's runtime catches it. This means that Child stops executing (it's preempted). React adds a then listener to fetchData's promise that will put Child back on the queue of items to be "rendered" later.
    • Later, that promise is fulfilled. React re-render's Child. fetchData needs to have cached the result of the promise so that when it is next called with the same arguments it will return the cached results synchronously.
    • This means that the type of fetchData is not (as you would expect) Promise<Data> but instead JUST Data.

Upvotes: 1

Related Questions