history<\/code><\/a> packages from npm, and looks somewhat like this (some details have been removed for simplicity, like authentication).<\/p>\n\n\n\nimport createHistory from \"history\/createBrowserHistory\";\nimport queryString from \"query-string\";\nexport const history = createHistory();\nexport function getCurrentUrlState() {\n\u00a0 let location = history.location;\n\u00a0 let parsed = queryString.parse(location.search);\n\u00a0 return {\n\u00a0 \u00a0 pathname: location.pathname,\n\u00a0 \u00a0 searchState: parsed\n\u00a0 };\n}\nexport function getCurrentModuleFromUrl() {\n\u00a0 let location = history.location;\n\u00a0 return location.pathname.replace(\/\\\/\/g, \"\").toLowerCase();\n}<\/code><\/pre>\n\n\n\nI have an appSettings<\/code> reducer that holds the current module and searchState<\/code> values for the app, and uses these methods to sync with the URL when needed.<\/p>\n\n\nThe pieces of a Suspense-based navigation<\/h3>\n\n\n Let’s get started with some Suspense work. First, let’s create the lazy-loaded components for our modules.<\/p>\n\n\n\n
const ActivateComponent = lazy(() => import(\".\/modules\/activate\/activate\"));\nconst AuthenticateComponent = lazy(() =>\n\u00a0 import(\".\/modules\/authenticate\/authenticate\")\n);\nconst BooksComponent = lazy(() => import(\".\/modules\/books\/books\"));\nconst HomeComponent = lazy(() => import(\".\/modules\/home\/home\"));\nconst ScanComponent = lazy(() => import(\".\/modules\/scan\/scan\"));\nconst SubjectsComponent = lazy(() => import(\".\/modules\/subjects\/subjects\"));\nconst SettingsComponent = lazy(() => import(\".\/modules\/settings\/settings\"));\nconst AdminComponent = lazy(() => import(\".\/modules\/admin\/admin\"));<\/code><\/pre>\n\n\n\nNow we need a method that chooses the right component based on the current module. If we were using React Router, we’d have some nice <Route \/><\/code> components. Since we’re rolling this manually, a switch<\/code> will do.<\/p>\n\n\n\nexport const getModuleComponent = moduleToLoad => {\n\u00a0 if (moduleToLoad == null) {\n\u00a0 \u00a0 return null;\n\u00a0 }\n\u00a0 switch (moduleToLoad.toLowerCase()) {\n\u00a0 \u00a0 case \"activate\":\n\u00a0 \u00a0 \u00a0 return ActivateComponent;\n\u00a0 \u00a0 case \"authenticate\":\n\u00a0 \u00a0 \u00a0 return AuthenticateComponent;\n\u00a0 \u00a0 case \"books\":\n\u00a0 \u00a0 \u00a0 return BooksComponent;\n\u00a0 \u00a0 case \"home\":\n\u00a0 \u00a0 \u00a0 return HomeComponent;\n\u00a0 \u00a0 case \"scan\":\n\u00a0 \u00a0 \u00a0 return ScanComponent;\n\u00a0 \u00a0 case \"subjects\":\n\u00a0 \u00a0 \u00a0 return SubjectsComponent;\n\u00a0 \u00a0 case \"settings\":\n\u00a0 \u00a0 \u00a0 return SettingsComponent;\n\u00a0 \u00a0 case \"admin\":\n\u00a0 \u00a0 \u00a0 return AdminComponent;\n\u00a0 }\n\u00a0\u00a0\n\u00a0 return HomeComponent;\n};<\/code><\/pre>\n\n\nThe whole thing put together<\/h3>\n\n\n With all the boring setup out of the way, let’s see what the entire app root looks like. There’s a lot of code here, but I promise, relatively few of these lines pertain to Suspense, and I’ll cover all of it.<\/p>\n\n\n\n
const App = () => {\n\u00a0 const [startTransitionNewModule, isNewModulePending] = useTransition({\n\u00a0 \u00a0 timeoutMs: 3000\n\u00a0 });\n\u00a0 const [startTransitionModuleUpdate, moduleUpdatePending] = useTransition({\n\u00a0 \u00a0 timeoutMs: 3000\n\u00a0 });\n\u00a0 let appStatePacket = useAppState();\n\u00a0 let [appState, _, dispatch] = appStatePacket;\n\u00a0 let Component = getModuleComponent(appState.module);\n\u00a0 useEffect(() => {\n\u00a0 \u00a0 startTransitionNewModule(() => {\n\u00a0 \u00a0 \u00a0 dispatch({ type: URL_SYNC });\n\u00a0 \u00a0 });\n\u00a0 }, []);\n\u00a0 useEffect(() => {\n\u00a0 \u00a0 return history.listen(location => {\n\u00a0 \u00a0 \u00a0 if (appState.module != getCurrentModuleFromUrl()) {\n\u00a0 \u00a0 \u00a0 \u00a0 startTransitionNewModule(() => {\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 dispatch({ type: URL_SYNC });\n\u00a0 \u00a0 \u00a0 \u00a0 });\n\u00a0 \u00a0 \u00a0 } else {\n\u00a0 \u00a0 \u00a0 \u00a0 startTransitionModuleUpdate(() => {\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 dispatch({ type: URL_SYNC });\n\u00a0 \u00a0 \u00a0 \u00a0 });\n\u00a0 \u00a0 \u00a0 }\n\u00a0 \u00a0 });\n\u00a0 }, [appState.module]);\n\u00a0 return (\n\u00a0 \u00a0 <AppContext.Provider value={appStatePacket}>\n\u00a0 \u00a0 \u00a0 <ModuleUpdateContext.Provider value={moduleUpdatePending}>\n\u00a0 \u00a0 \u00a0 \u00a0 <div>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 <MainNavigationBar \/>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 {isNewModulePending ? <Loading \/> : null}\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 <Suspense fallback={<LongLoading \/>}>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 <div id=\"main-content\" style={{ flex: 1, overflowY: \"auto\" }}>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 {Component ? <Component updating={moduleUpdatePending} \/> : null}\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 <\/div>\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 <\/Suspense>\n\u00a0 \u00a0 \u00a0 \u00a0 <\/div>\n\u00a0 \u00a0 \u00a0 <\/ModuleUpdateContext.Provider>\n\u00a0 \u00a0 <\/AppContext.Provider>\n\u00a0 );\n};<\/code><\/pre>\n\n\n\nFirst, we have two different calls to useTransition<\/code>. We’ll use one for routing to a new module, and the other for updating search state for the current module. Why the difference? Well, when a module’s search state is updating, that module will likely want to display an inline loading indicator. That updating state is held by the moduleUpdatePending<\/code> variable, which you’ll see I put on context for the active module to grab, and use as needed:<\/p>\n\n\n\n<div>\n\u00a0 <MainNavigationBar \/>\n\u00a0 {isNewModulePending ? <Loading \/> : null}\n\u00a0 <Suspense fallback={<LongLoading \/>}>\n\u00a0 \u00a0 <div id=\"main-content\" style={{ flex: 1, overflowY: \"auto\" }}>\n\u00a0 \u00a0 \u00a0 {Component ? <Component updating={moduleUpdatePending} \/> : null} \/\/ highlight\n\u00a0 \u00a0 <\/div>\n\u00a0 <\/Suspense>\n<\/div><\/code><\/pre>\n\n\n\nThe appStatePacket<\/code> is the result of the app state reducer I discussed above (but did not show). It contains various pieces of application state which rarely change (color theme, offline status, current module, etc).<\/p>\n\n\n\nlet appStatePacket = useAppState();<\/code><\/pre>\n\n\n\nA little later, I grab whichever component happens to be active, based on the current module name. Initially this will be null.<\/p>\n\n\n\n
let Component = getModuleComponent(appState.module);<\/code><\/pre>\n\n\n\nThe first call to useEffect<\/code> will tell our appSettings<\/code> reducer to sync with the URL at startup.<\/p>\n\n\n\nuseEffect(() => {\n\u00a0 startTransitionNewModule(() => {\n\u00a0 \u00a0 dispatch({ type: URL_SYNC });\n\u00a0 });\n}, []);<\/code><\/pre>\n\n\n\nSince this is the initial module the web app navigates to, I wrap it in startTransitionNewModule<\/code> to indicate that a fresh module is loading. While it might be tempting to have the appSettings<\/code> reducer have the initial module name as its initial state, doing this prevents us from calling our startTransitionNewModule<\/code> callback, which means our Suspense boundary would render the fallback immediately, instead of after the timeout.<\/p>\n\n\n\nThe next call to useEffect<\/code> sets up a history subscription. No matter what, when the url changes we tell our app settings to sync against the URL. The only difference is which startTransition<\/code> that same call is wrapped in.<\/p>\n\n\n\nuseEffect(() => {\n\u00a0 return history.listen(location => {\n\u00a0 \u00a0 if (appState.module != getCurrentModuleFromUrl()) {\n\u00a0 \u00a0 \u00a0 startTransitionNewModule(() => {\n\u00a0 \u00a0 \u00a0 \u00a0 dispatch({ type: URL_SYNC });\n\u00a0 \u00a0 \u00a0 });\n\u00a0 \u00a0 } else {\n\u00a0 \u00a0 \u00a0 startTransitionModuleUpdate(() => {\n\u00a0 \u00a0 \u00a0 \u00a0 dispatch({ type: URL_SYNC });\n\u00a0 \u00a0 \u00a0 });\n\u00a0 \u00a0 }\n\u00a0 });\n}, [appState.module]);<\/code><\/pre>\n\n\n\nIf we’re browsing to a new module, we call startTransitionNewModule<\/code>. If we’re loading a component that hasn’t been loaded already, React.lazy<\/code> will suspend, and the pending indicator visible only to the app’s root will set, which will show a loading spinner at the top of the app while the lazy component is fetched and loaded. Because of how useTransition<\/code> works, the current screen will continue to show for three seconds. If that time expires and the component is still not ready, our UI will suspend, and the fallback will render, which will show the <LongLoading \/><\/code> component:<\/p>\n\n\n\n{isNewModulePending ? <Loading \/> : null}\n<Suspense fallback={<LongLoading \/>}>\n\u00a0 <div id=\"main-content\" style={{ flex: 1, overflowY: \"auto\" }}>\n\u00a0 \u00a0 {Component ? <Component updating={moduleUpdatePending} \/> : null}\n\u00a0 <\/div>\n<\/Suspense><\/code><\/pre>\n\n\n\nIf we’re not changing modules, we call startTransitionModuleUpdate<\/code>:<\/p>\n\n\n\nstartTransitionModuleUpdate(() => {\n\u00a0 dispatch({ type: URL_SYNC });\n});<\/code><\/pre>\n\n\n\nIf the update causes a suspension, the pending indicator we’re putting on context will be triggered. The active component can detect that and show whatever inline loading indicator it wants. As before, if the suspension takes longer than three seconds, the same Suspense boundary from before will be triggered… unless, as we’ll see later, there’s a Suspense boundary lower in the tree.<\/p>\n\n\n\n
One important thing to note is that these three-second timeouts apply not only to the component loading, but also being ready to display. If the component loads in two seconds, and, when rendering in memory (since we’re inside of a startTransition<\/code> call) suspends, the useTransition<\/code> will continue<\/em> to wait for up to one more second before Suspending.<\/p>\n\n\n\nIn writing this blog post, I used Chrome’s slow network modes to help force loading to be slow, to test my Suspense boundaries. The settings are in the Network tab of Chrome’s dev tools. <\/p>\n\n\n\n <\/figure>\n\n\n\nLet’s open our app to the settings module. This will be called:<\/p>\n\n\n\n
dispatch({ type: URL_SYNC });<\/code><\/pre>\n\n\n\nOur appSettings<\/code> reducer will sync with the URL, then set module to “settings.” This will happen inside of startTransitionNewModule<\/code> so that, when the lazy-loaded component attempts to render, it’ll suspend. Since we’re inside startTransitionNewModule<\/code>, the isNewModulePending<\/code> will switch over to true<\/code>, and the <Loading \/><\/code> component will render.<\/p>\n\n\n\n <\/figure>\n\n\n\nIf the component is still not ready to render within three seconds, the in-memory version of our component tree will switch over, suspend, and our Suspense boundary will render the <LongLoading \/><\/code> component.<\/figcaption><\/figure>\n\n\n\nWhen it\u2019s done, the settings module will show.<\/figcaption><\/figure>\n\n\n\nSo what happens when we browse somewhere new? Basically the same thing as before, except this call:<\/p>\n\n\n\n
dispatch({ type: URL_SYNC });<\/code><\/pre>\n\n\n\n\u2026will come from the second instance of useEffect<\/code>. Let’s browse to the books module and see what happens. First, the inline spinner shows as expected:<\/p>\n\n\n\n <\/figure>\n\n\n\nIf the three-second timeout elapses, our Suspense boundary will render its fallback:<\/figcaption><\/figure>\n\n\n\nAnd, eventually, our books module loads:<\/figcaption><\/figure>\n\n\nSearching and updating<\/h3>\n\n\n Let’s stay within the books module, and update the URL search string to kick off a new search. Recall from before that we were detecting the same module in that second useEffect<\/code> call and using a dedicated useTransition<\/code> call for it. From there, we were putting the pending indicator on context for whichever module was active for us to grab and use.<\/p>\n\n\n\nLet’s see some code to actually use that. There’s not really much Suspense-related code here. I\u2019m grabbing the value from context, and if true, rendering an inline spinner on top of my existing results. Recall that this happens when a useTransition<\/code> call has begun, and the app is suspended in memory<\/strong>. While that\u2019s happening, we continue to show the existing UI, but with this loading indicator.<\/p>\n\n\n\nconst BookResults: SFC<{ books: any; uiView: any }> = ({ books, uiView }) => {\n const isUpdating = useContext(ModuleUpdateContext);\n return (\n <>\n {!books.length ? (\n <div\n className=\"alert alert-warning\"\n style={{ marginTop: \"20px\", marginRight: \"5px\" }}\n >\n No books found\n <\/div>\n ) : null}\n {isUpdating ? <Loading \/> : null}\n {uiView.isGridView ? (\n <GridView books={books} \/>\n ) : uiView.isBasicList ? (\n <BasicListView books={books} \/>\n ) : uiView.isCoversList ? (\n <CoversView books={books} \/>\n ) : null}\n <\/>\n );\n};<\/code><\/pre>\n\n\n\nLet’s set a search term and see what happens. First, the inline spinner displays.<\/p>\n\n\n\n <\/figure>\n\n\n\nThen, if the useTransition<\/code> timeout expires, we’ll get the Suspense boundary’s fallback. The books module defines its own Suspense boundary in order to provide a more fine-tuned loading indicator, which looks like this:<\/p>\n\n\n\n <\/figure>\n\n\n\nThis is a key point. When making Suspense boundary fallbacks, try not to throw up any sort of spinner and “loading” message. That made sense for our top-level navigation because there’s not much else to do. But when you’re in a specific part of your application, try to make your fallback re-use many of the same components with some sort of loading indicator where the data would be \u2014 but with everything else disabled.<\/p>\n\n\n\n
This is what the relevant components look like for my books module:<\/p>\n\n\n\n
const RenderModule: SFC<{}> = ({}) => {\n const uiView = useBookSearchUiView();\n const [lastBookResults, setLastBookResults] = useState({\n totalPages: 0,\n resultsCount: 0\n });\n return (\n <div className=\"standard-module-container margin-bottom-lg\">\n <Suspense fallback={<Fallback uiView={uiView} {...lastBookResults} \/>}>\n <MainContent uiView={uiView} setLastBookResults={setLastBookResults} \/>\n <\/Suspense>\n <\/div>\n );\n};\nconst Fallback: SFC<{\n uiView: BookSearchUiView;\n totalPages: number;\n resultsCount: number;\n}> = ({ uiView, totalPages, resultsCount }) => {\n return (\n <>\n <BooksMenuBarDisabled\n totalPages={totalPages}\n resultsCount={resultsCount}\n \/>\n {uiView.isGridView ? (\n <GridViewShell \/>\n ) : (\n <h1>\n Books are loading <i className=\"fas fa-cog fa-spin\"><\/i>\n <\/h1>\n )}\n <\/>\n );\n};<\/code><\/pre>\n\n\nA quick note on consistency<\/h3>\n\n\n Before we move on, I’d like to point out one thing from the earlier screenshots. Look at the inline spinner that displays while the search is pending, then look at the screen when that search suspended, and next, the finished results:<\/p>\n\n\n\n <\/figure>\n\n\n\nNotice how there’s a “C++” label to the right of the search pane, with an option to remove it from the search query? Or rather, notice how that label is only on the second two screenshots? The moment the URL updates, the application state governing that label is<\/em> updated; however, that state does not initially display. Initially, the state update suspends in memory (since we used useTransition), and the prior<\/em> UI continues to show.<\/p>\n\n\n\nThen the fallback renders. The fallback renders a disabled version of that same search bar, which does show the current search state (by choice). We’ve now removed our prior UI (since by now it\u2019s quite old, and stale) and are waiting on the search shown in the disabled menu bar.<\/p>\n\n\n\n
This is the sort of consistency Suspense gives you, for free.<\/p>\n\n\n\n
You can spend your time crafting nice application states, and React does the leg work of surmising whether things are ready, without you needing to juggle promises.<\/p>\n\n\n
Nested Suspense boundaries<\/h3>\n\n\n Let’s suppose our top-level navigation takes a while to load our books component to the extent that our \u201cStill loading, sorry\u201d spinner from the Suspense boundary renders. From there, the books component loads and the new Suspense boundary inside the books component renders. But, then, as rendering continues, our book search query fires, and suspends. What will happen? Will the top-level Suspense boundary continue to show, until everything is ready, or will the lower-down Suspense boundary in books take over?<\/p>\n\n\n\n
The answer is the latter. As new Suspense boundaries render lower in the tree, their fallback will replace<\/em> the fallback of whatever antecedent Suspense fallback was already showing. There’s currently an unstable API to override this, but if you’re doing a good job of crafting your fallbacks, this is probably the behavior you want. You don’t want \u201cStill loading, sorry\u201d to just keep showing. Rather, as soon as the books component is ready, you absolutely want to display that shell with the more targeted waiting message.<\/p>\n\n\n\nNow, what if our books module loads and starts to render while the startTransition<\/code> spinner is still showing and then suspends? In other words, imagine that our startTransition<\/code> has a timeout of three seconds, the books component renders, the nested Suspense boundary is in the component tree after one second, and the search query suspends. Will the remaining two seconds elapse before that new nested Suspense boundary renders the fallback, or will the fallback show immediately? The answer, perhaps surprisingly, is that the new Suspense fallback will show immediately by default. That\u2019s because it’s best to show a new, valid UI as quickly as possible, so the user can see that things are happening, and progressing. <\/p>\n\n\nHow data fits in<\/h3>\n\n\n Navigation is fine, but how does data loading fit into all of this?<\/p>\n\n\n\n
It fits in completely and transparently. Data loading triggers suspensions just like navigation with React.lazy<\/code>, and it hooks into all the same useTransition<\/code> and Suspense boundaries. This is what’s so amazing about Suspense: all your async dependencies seamlessly work in this same system. <\/strong>Managing these various async requests manually to ensure consistency was a nightmare before Suspense, which is precisely why nobody did it. Web apps were notorious for cascading spinners that stopped at unpredictable times, producing inconsistent UIs that were only partially finished.<\/p>\n\n\n\nOK, but how do we actually tie data loading into this? Data loading in Suspense is paradoxically both more complex, and also simple.<\/p>\n\n\n\n
I’ll explain.<\/p>\n\n\n\n
If you’re waiting on data, you’ll throw a promise in the component that reads (or attempts to read) the data. The promise should be consistent based on the data request. So, four repeated requests for that same “C++” search query should throw the same, identical promise. This implies some sort of caching layer to manage all this. You’ll likely not write this yourself. Instead, you’ll just hope, and wait for the data library you use to update itself to support Suspense.<\/p>\n\n\n\n
This is already done in my micro-graphql-react<\/a> library. Instead of using the useQuery<\/code> hook, you\u2019ll use the useSuspenseQuery<\/code> hook, which has an identical API, but throws a consistent promise when you’re waiting on data.<\/p>\n\n\nWait, what about preloading?!<\/h3>\n\n\n Has your brain turned to mush reading other things on Suspense that talked about waterfalls, fetch-on-render, preloading, etc? Don’t worry about it. Here’s what it all means.<\/p>\n\n\n\n
Let’s say you lazy load the books component, which renders and then<\/em> requests some data, which causes a new Suspense. The network request for the component and the network request for the data will happen one after the other\u2014in a waterfall fashion.<\/p>\n\n\n\nBut here’s the key part: the application state that led to whatever initial query that ran when the component loaded was already available when you started loading the component (which, in this case, is the URL). So why not “start” the query as soon as you know you’ll need it? As soon as you browse to \/books<\/code>, why not fire off the current search query right then and there, so it’s already in flight when the component loads.<\/p>\n\n\n\nThe micro-graphql-react module does indeed have a preload<\/code> method, and I urge you to use it. Preloading data is a nice performance optimization, but it has nothing to do with Suspense. Classic React apps could (and should) preload data as soon as they know they’ll need it. Vue apps should preload data as soon as they know they’ll need it. Svelte apps should… you get the point.<\/p>\n\n\n\nPreloading data is orthogonal to Suspense, which is something you can do with literally any framework. It\u2019s also something we all should have been doing already, even though nobody else was.<\/p>\n\n\n
But seriously, how do you preload?<\/h3>\n\n\n That’s up to you. At the very least, the logic to run the current search absolutely needs to be completely separated into its own, standalone module. You should literally make sure this preload function is in a file by itself. Don’t rely on webpack to treeshake; you’ll likely face abject sadness the next time you audit your bundles.<\/p>\n\n\n\n
You have a preload()<\/code> method in its own bundle, so call it. Call it when you know you’re about to navigate to that module. I assume React Router has some sort of API to run code on a navigation change. For the vanilla routing code above, I call the method in that routing switch from before. I had omitted it for brevity, but the books entry actually looks like this:<\/p>\n\n\n\nswitch (moduleToLoad.toLowerCase()) {\n case \"activate\":\n return ActivateComponent;\n case \"authenticate\":\n return AuthenticateComponent;\n case \"books\":\n \/\/ preload!!!\n booksPreload();\n return BooksComponent;<\/code><\/pre>\n\n\n\nThat’s it. Here’s a live demo to play around with:<\/p>\n\n\n\n