2

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?

Muneeb Naveed
  • 83
  • 1
  • 10
  • 1
    Its not an suspense issue It is javascript error caused because default initialization of powers value in useState. Try with empty array like const [powers, setPowers] = useState([]); – Vishal Rajole Aug 21 '20 at 06:45
  • Oh yes my bad. Although it's still not fixed. When I use React DevTools to analyze the suspense of element, it says `suspense: false` all the time and I don't see my callback element instead of , I just see an empty – Muneeb Naveed Aug 21 '20 at 06:55

1 Answers1

6

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 rendernig 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}/> )

};
Nicholas Tower
  • 72,740
  • 7
  • 86
  • 98