reactjsasync-awaitreact-suspense

React Suspense is not working as intended


I want to render fallback component when my Powers is being fetched/undefined. I implemented React.Suspense in my logic using the code:

<Suspense fallback={<p>Loading</p>}>
  <RoutesPowers />
</Suspense>

and my RoutesPowers is

const Powers = [ ... ];

const RoutesPowers = () => {

  const [powers, setPowers] = useState(null);
  const fetchPowers = () => setTimeout(() => setPowers(Powers), 3000);

  useEffect(() => fetchPowers(), []);

  return ( powers.map(route => <RoutePowers route={route}/> )

};
            

but it gives me Cannot read property "map" of null probably because powers is null. That means that React.Suspense isn't working as it should. Can anybody help me on this?


Solution

  • For suspense to have any effect, a component farther down the tree needs to throw a promise. When that happens, suspense will catch it and display the fallback until the promise resolves, and then it resumes rendering its normal children. Your code doesn't throw any promises, so suspense doesn't do anything.

    So if you want to use suspense for this, you need to have your component throw a promise during rendering if it detects it doesn't have the data. Then do your fetching, and save data such that when the component mounts again it will have the data (you can't set state, because the component doesn't exist during this time, the fallback does).

    const App = () => (
      <React.Suspense fallback={<p>Loading</p>}>
        <RoutesPowers />
      </React.Suspense>
    )
    
    let savedPowers = null;
    const fetchPowers = () => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          savedPowers = ['first', 'second']
          resolve();
        }, 3000)
      });
    }
    
    const RoutesPowers = () => {
      const [powers, setPowers] = React.useState(savedPowers);
      if (!powers) {
        throw fetchPowers();
      }
    
      return powers.map(value => <div key={value}>{value}</div>);
    }
    
    ReactDOM.render(<App />, document.getElementById("root"));
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.development.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.development.js"></script>
    <div id="root"></div>

    Be aware that it's uncommon to use this pattern. More common is to handle it all in the one component, rendering a placeholder if there is no data, then kicking off the fetch in an effect, then updating state when it's done. Your current code is almost there, you'd just want to delete the <Suspense> and add code inside RoutesPowers to return the loading paragraph.

    const RoutesPowers = () => {
      const [powers, setPowers] = useState(null);
      const fetchPowers = () => setTimeout(() => setPowers(Powers), 3000);
    
      useEffect(() => fetchPowers(), []);
    
      if (!powers) {
        return <p>loading</p>
      }
    
      return ( powers.map(route => <RoutePowers route={route}/> )
    
    };