question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

React 18 - infinite loop and function as child issue with transition and suspense with useMemo

See original GitHub issue

I’ve turned off strict mode, and tried to create a simple example that breaks in v18 rc3.

I’m trying to use useMemo to detect when some state changes and create a new memoized promise. Another useMemo call detects when the promise changes and wraps it in a “resource” object. I am passing the resource down to a child. There are multiple suspense boundaries between where I am creating the “resource” and where I’m calling .read(). I expect this to load once with no errors, and when I click the button I expect a single transition. Instead, although the app loads I get this error react-dom.development.js:86 Warning: Functions are not valid as a React child. This may happen if you return a Component instead of <Component /> from render. Or maybe you meant to call this function. When I click the button, it then goes into an infinite loop. This is unexpected because <App> should not be suspending, only <Child>. I’m also not returning any components without calling them from what I can tell.

If I set the “resource” into state it works fine. This only seems to happen if I try to create the “resource” with useMemo.

Minimal reproduction case

import {
  startTransition,
  Suspense,
  useRef,
  useEffect,
  useState,
  useMemo,
} from "react";
import logo from "./logo.svg";
import "./App.css";

function App() {
  const ref = useRef(0);
  const [validQueryParams, setValidQueryParams] = useState();

  const promise = useMemo(() => {
    console.log("making promise that resolves in 3s", ref.current);
    ref.current++;
    return new Promise((res) => {
      setTimeout(() => {
        console.log(validQueryParams);
        return res();
      }, 3000);
    });
  }, [validQueryParams]);

  const query = useMemo(() => {
    console.log("wrapping promise in resource object");
    return wrapPromise(promise);
  }, [promise]);

  useEffect(() => {
    console.log("mount");
    return () => console.log("unmount");
  }, []);

  console.log({ query });
  return (
    <>
      <Suspense fallback={() => <div>fallback</div>}>
        <Child query={query} />
        <button
          onClick={() => {
            startTransition(() => {
              setValidQueryParams(Math.random());
            });
          }}
        >
          Start transition
        </button>
      </Suspense>
    </>
  );
}

function Child({ query }) {
  if (!query) return null;
  return (
    <Suspense fallback={() => <div>fallback</div>}>
      <div>{JSON.stringify(query.read())}</div>
    </Suspense>
  );
}

export default App;

export function wrapPromise(promise) {
  let status = "pending";
  let result;
  const suspender = promise.then(
    (r) => {
      status = "success";
      result = r;
    },
    (e) => {
      status = "error";
      result = e;
    }
  );
  return {
    read() {
      if (status === "pending") {
        throw suspender;
      } else if (status === "error") {
        throw result;
      } else if (status === "success") {
        return result;
      }
    },
  };
}

Issue Analytics

  • State:closed
  • Created a year ago
  • Comments:9 (5 by maintainers)

github_iconTop GitHub Comments

1reaction
gaearoncommented, Mar 24, 2022

Here is a smaller example of the same loop:

https://codesandbox.io/s/serverless-wave-469uso?file=/src/App.js

The issue here is that when you suspend and React doesn’t have anything useful to commit (because you’re in a transition, so there is no fallback loading state to show), the whole result of that render gets thrown away. This includes anything that happened during render, no matter how high in the tree. This includes any new memoized results. So useMemo — even in the parent component — doesn’t get a chance to actually “get saved”. It’s like this useMemo never ran.

I assumed that to satisfy the requirement of storing the Promise “externally”, it would suffice to do so above the suspense boundary

This can work in a limited way, but any information you rely on has to be calculated outside of render. This is why storing a resource in state works (it won’t get dropped) but creating a resource during render doesn’t (it will get dropped).

1reaction
gaearoncommented, Mar 24, 2022

This is your original example:

https://codesandbox.io/s/dazzling-cherry-5bsiv6?file=/src/App.js

I can reproduce this error:

Warning: Functions are not valid as a React child. This may happen if you return a Component instead of <Component /> from render. Or maybe you meant to call this function rather than return it.

Like the error says, the issue is that you are trying to render a function:

    <Suspense fallback={() => <div>fallback</div>}>

This line should be instead:

    <Suspense fallback={<div>fallback</div>}>

This is the version where this mistake is fixed (in both places where the original code had it):

https://codesandbox.io/s/vigorous-cloud-clwtvt?file=/src/App.js

I can observe that after clicking “Start transition”, there is a log that repeats every three seconds. I assume this is the issue you are referring to as the loop. I’ll need to look a bit more to understand why this doesn’t work. However, in general, Suspense relies on having an external cache. If you’re creating “resources” during render (whether with useMemo or useState), you will likely run into issues.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Concurrent UI Patterns (Experimental) - React
Transitions Are Everywhere. As we learned from the Suspense walkthrough, any component can “suspend” any time if some data it needs is not...
Read more >
"Error: Too many re-renders. React limits the number of ...
The reason for the infinite loop is because something (most likely setState ) in the event callback is triggering a re-render.
Read more >
CHANGELOG.md - facebook/react - Sourcegraph
This solves an issue that already exists in React 17 and below, but it's even more important in React 18 because of how...
Read more >
What are the biggest issues you see with React in its current ...
- It's far too easy to work yourself into infinite loops with hooks. The easiest example of this is a setState call that...
Read more >
Creating better user experiences with React 18 Suspense and ...
In this tutorial we reimplement the awesome Solid Suspense Transitions demo with React 18 Suspense and Transitions (useTransition).
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found