2

I'm building a new React app, on top of an existing boilerplate. It uses lazy loading, combined with React.Suspense.

Problem is, that like in most React apps, i need to fetch some initial meta data from the server, each time the app loads. Let's call it "getAppMetaData".

So what is the problem? The problem is, that while getAppMetaData is pending, I need to present some loader/spinner. And this is exactly what React.Suspense does: It shows the "fallback" UI. Of course i could run a separate loader(which can actually be the same as the fallback UI), but this creates a UX problem, where the animation of the loader "restarts", between the procedures.

So, the question is, how can i "integrate" other async actions, into this suspension? In simple words: "My fallback UI is already showing, while the chunk(from lazy loading) is loaded- so how do i make it also wait for getAppMetaData?"

This is my router:

<ErrorBoundary>
     <Suspense fallback={<div className={styles.loader}><Loader /></div>}>
        <Switch>
          <ProtectedRoute exact component={Home} path="/">                    
          </ProtectedRoute>    
             <Route path="/lesson">
               <Lesson></Lesson>
             </Route>    
            <Route exact path="/login">
              <Login />
            </Route>
               <Route path="/about">
            <About />
            </Route>
            <Route path="*">
              <NotFound />
          </Route>
        </Switch>
      </Suspense>
    </ErrorBoundary>

React documentation states, that Relay library should be used for this, but i do not want to use any special library for my API calls, just to overcome this simple. It also states:

What If I Don’t Use Relay? If you don’t use Relay today, you might have to wait before you can really try Suspense in your app. So far, it’s the only implementation that we tested in production and are confident in.

All i need is to integrate one little initial API call, into this procedure. How can it be done? Any suggestions will be greatly appreciated.

i.brod
  • 3,993
  • 11
  • 38
  • 74
  • `Suspense` is just a component that catch a promise and display a fallback until the promise is resolved, so you load the data using a promise and throw it so the Suspense component can catch it and display the spinner while the data is loading – Olivier Boissé Sep 14 '21 at 17:28
  • I see, but where exactly do i place this code? How do i make sure it's caught by suspense? If i have some component sitting within the Suspense tree, how do i actually do it there? If i throw a promise from useEffect, i get an uncaught error – i.brod Sep 14 '21 at 17:36

1 Answers1

1

I would move the Suspense's children into a new component and read the data from this component.

The data will be loaded using a custom function fetchData which will create the promise and return a object containing a read method which will throw the promise if the data is not ready.

function fetchData() {
  let status = 'pending';
  let result;

  const promise = fetch('./data.json')
    .then(data => data.json())
    .then(r => {
      status = 'success';
      result = r;
    })
    .catch(e => {
      status = 'error';
      result = e;
    });

  return {
    read() {
      if (status === 'pending') {
        throw promise;
      } else if (status === 'error') {
        throw result;
      } else if (status === 'success') {
        return result;
      }
    }
  };
}

const dataWrapper = fetchData();

function AppBody() {
  const data = dataWrapper.read();

  // you can now manipulate the data
  return (
    <Switch>
      <ProtectedRoute exact component={Home} path="/"/>                    
      <Route path="/lesson" component={Lesson} />
      <Route exact path="/login" component={Login} />
      <Route path="/about" component={About} />
      <Route path="*" component={NotFound} />
    </Switch>
  )
}

function App() {
  return (
    <ErrorBoundary>
     <Suspense fallback={<div className={styles.loader}><Loader /></div>}>
       <AppBody/>
     </Suspense>
    </ErrorBoundary>
  );
}

Here is a stackblitz example

This code is inspired by this codesandbox from the react documentation

Olivier Boissé
  • 15,834
  • 6
  • 38
  • 56
  • 1
    Wow man, this works great. I will certainly learn this code and understand the mechanism. Thank you. – i.brod Sep 14 '21 at 20:32
  • 1
    you just need to understand that the `Suspense` component job is to catch promise and display a fallback component until the promise is resolved. – Olivier Boissé Sep 14 '21 at 20:34
  • How would you go about creating a custom hook from this, that can be used in any component?(all under the same Suspense tree). Something that would allow me to call "useSuspense(somePromise)" from the component. – i.brod Sep 24 '21 at 17:46
  • the simplest way is to use `useMemo` with `[]` inside the custom hook to create the promise and return the object with the read method. A more sophisticated way would be to combine `useState` (to store the promise) and `useMemo` to build the object with the read method, with this method you can refresh the data by updating the state with a new promise. – Olivier Boissé Sep 24 '21 at 19:14
  • Thank you. Can you explain what exactly i need to memoize? – i.brod Sep 24 '21 at 20:45
  • I added an [example on stackblitz](https://stackblitz.com/edit/react-4mx7ja?file=src/App.js) using a custom hook and a HOC (higher order component) to simplify the use of a Component with Suspense. You can just use the custom hook without the HOC if you want. – Olivier Boissé Sep 24 '21 at 23:00
  • Thank you for taking the time to write this! Problem is: I wanna be able to create the data wrapper from *within the component*. Why? Because imagine i have a component that follows the restful convention of /todo/:id . Inside the component, i need to dynamically construct the url from which i fetch data, using the router params. Also, I would like to just pass a promise(or promise factory), and not a url string. I hope this Suspense API will become more friendly soon :D – i.brod Sep 25 '21 at 16:32
  • In simpler words: How to create the resource from within the component, that actually also calls the resource.read() method? Is it possible? – i.brod Sep 25 '21 at 17:14
  • It will not be possible because the read method throw the promise when its not resolved yet, so the component is unmount, next time the component will be mounted (when the promise is resolved), the component will recreate the wrapper (the promise will be re-created and thrown by the read method), so you end up with an infinite loop – Olivier Boissé Sep 25 '21 at 20:41
  • Yes, that was my "suspicion", when i was playing with it. But then, i'm trying to figure out, how the useTranslation hook of i18n-react achieves that. Looking at their source code, was really hard for me to understand. – i.brod Sep 25 '21 at 20:43
  • you can look at [react fetch](https://github.com/facebook/react/tree/main/packages/react-fetch), it's still under development but it provides a `fetch` method that seems to use a cache so calling `fetch` twice with the same url will result in a single http request. You can see an example of how to use it in this [file](https://github.com/reactjs/server-components-demo/blob/main/src/Note.server.js) – Olivier Boissé Sep 25 '21 at 20:46
  • Well i guess ill just wait for this whole Suspense thingy to be "official". Problem is that i don't see any other way to "unify" the loading screen used by react lazy loading, with the loading process of specific views. If i remove the lazy loading, i'll be able to make this unification myself, because i wouldn't be relying on Suspnese. – i.brod Sep 25 '21 at 20:54