In my current case I want to lazy-load components through React Router to decrease my bundle size on initial page load.
React Suspense requires a fallback prop, but I don't want it.
I render html from the server for better UX (matching the client component), and providing a loader placeholder when lazy-loading components on react router causes a flicker.
Currently I implement my own loader from within a React component, and this is the way I want it (I have a custom feed loader that is isolated in a widget, and I don't want a loader for a full page component). Not showing a loader provides much better user experience.
I don't know how to fix this with the current api, so I'm left to abandon lazy-loading components because the current UX is really a no-no.
Please advise on the motivation behind making the fallback prop mandatory, and whether you could consider abandoning that imposing idea, so I could once again use the React Suspense api to improve my initial page load speed.
You can see its effect here: https://stock-dd.herokuapp.com/home/topics/1
(Takes some time to load, free service that needs to be woken up after inactivity).
If there is a workable alternative, please do share.
I'm not sure I understand. <Suspense> throws in stable releases with server rendering. So how do you "render html from the server" while using it?
I created a HOC for the generic router and use a separate router instance for server and client side rendering, only applying Suspense / lazy loading to the client router. I should probably have mentioned that I'm using isomorphic rendering using renderToString and Redux hydration.
React Suspense requires a fallback prop, but I don't want it.
You have to tell React explicitly that you don't want a fallback. React wants you to make a conscious decision rather than an one by omission (which is more likely to be a mistake than saying it explicitly.
<React.Suspense fallback={null}>
<div />
</React.Suspense>
Not sure if this is a bug or not.
I tend to agree that you not always want a visible loading indicator for lazy loaded components. For example we don't display a visual indicator for the search bar at the top since it would be too distracting to flash a placeholder. youtube.com is also not displaying a visible skeleton for their search input.
@eps1lon Yes passing null works technically, Suspense will still render some empty div (or whatever it is that causes the server rendered html to be replaced with the placeholder).
I would assume it could be possible to wait until the component is available (without rendering any placeholder) and just render the component when it is available (yesno?).
Suspense will still render some empty div (or whatever it is that causes the server rendered html to be replaced with the placeholder).
It doesn't render anything with a null fallback. The inconsistencies you are seeing are probably due to the way you handle server rendering for Suspense since that isn't supported by React yet.
Or do you mean other components in that Suspense boundary that are hidden via display: none? This is a quirk in legacy ReactDOM.render and will be fixed with the new root-APIs (createBlockingRoot and createRoot). https://github.com/facebook/react/issues/18141#issuecomment-591648972 has more context about that.
@eps1lon Server rendering in my app doesn't use lazy loading, although it uses the same react components, the router is decoupled. What I see (when passing fallback = { null }) is that:
My client router looks like this:
import React, { lazy, Suspense } from 'react';
import AppRouter from "./AppRouter";
const LoginPageHoc = lazy(() => import('../../pages/login/LoginPageHoc'));
const LoggingInPage = lazy(() => import('../../pages/login/LoggingIn'));
const AboutPage = lazy(() => import('../../pages/about/AboutPage'));
const SectorPageHoc = lazy(() => import('../../pages/sector/SectorPageHoc'));
const HelpPage = lazy(() => import('../../pages/help/HelpPage'));
const UserProfileHoc = lazy(() => import('../../pages/user/UserProfileHoc'));
const CreateTopicPageHoc = lazy(() => import('../../pages/topic-create/CreateTopicPageHoc'));
const ProtectedRoute = lazy(() => import('../../utils/ProtectedRoute'));
const HomePageHoc = lazy(() => import('../../pages/home/HomePageHoc'));
const HomeEventsPageHoc = lazy(() => import('../../pages/home-events/HomeEventsPageHoc'));
const TopicByIdPageHoc = lazy(() => import('../../pages/topic/TopicPageHoc'));
const TickerPageHoc = lazy(() => import('../../pages/ticker/TickerPageHoc'));
const TickerCalendarPageHoc = lazy(() => import('../../pages/ticker-calendar/TickerCalendarPageHoc'));
const ScrollToTop = lazy(() => import('../../widgets/scroll-to-top/ScrollToTop'));
const NotFoundPage = lazy(() => import('../../pages/error/NotFoundPage'));
const lazyLoadComponent = Component =>
props => (
<Suspense fallback={ null }>
<Component {...props} />
</Suspense>
);
const ClientAppRouter = () =>
<AppRouter
LoginPageHoc = { lazyLoadComponent(LoginPageHoc) }
LoggingPageHoc = { lazyLoadComponent(LoggingInPage) }
AboutPage = { lazyLoadComponent(AboutPage) }
SectorPageHoc = { lazyLoadComponent(SectorPageHoc) }
HelpPage = { lazyLoadComponent(HelpPage) }
UserProfileHoc = { lazyLoadComponent(UserProfileHoc) }
CreateTopicPageHoc = { lazyLoadComponent(CreateTopicPageHoc) }
HomePageHoc = { lazyLoadComponent(HomePageHoc) }
HomeEventsPageHoc = { lazyLoadComponent(HomeEventsPageHoc) }
TopicByIdPageHoc = { lazyLoadComponent(TopicByIdPageHoc) }
TickerPageHoc = { lazyLoadComponent(TickerPageHoc) }
TickerCalendarPageHoc = { lazyLoadComponent(TickerCalendarPageHoc) }
ScrollToTop = { lazyLoadComponent(ScrollToTop) }
NotFoundPage = { lazyLoadComponent(NotFoundPage) }
/>;
export default ClientAppRouter;
Where the AppRouter:
import React from 'react';
import { withRouter } from 'react-router-dom';
import { Redirect, Route, Switch } from 'react-router-dom';
import AppLayout from '../../layout/page-layout/AppLayout';
import ProtectedRoute from "../../utils/ProtectedRoute";
const AppRouter = ({
ScrollToTop,
LoginPageHoc, LoggingInPage, HomePageHoc, HomeEventsPageHoc,
UserProfileHoc, CreateTopicPageHoc, TickerPageHoc, TickerCalendarPageHoc,
TopicByIdPageHoc, AboutPage, HelpPage, SectorPageHoc,
NotFoundPage
}) => {
return (
<AppLayout>
<ScrollToTop>
<Switch>
<Redirect exact from='/' to='/home/topics/1' />
<Redirect exact from='/home' to='/home/topics/1' />
<Route exact path = "/login"
component = { LoginPageHoc }
/>
<Route exact path = "/logging-in"
component = { LoggingInPage }
/>
<Redirect exact from='/home/topics' to='/home/topics/1' />
<Route exact path = "/home/topics/:pageId"
component = { HomePageHoc }
/>
<Redirect exact from='/home/events' to='/home/events/1' />
<Route exact path = "/home/events/:pageId"
component = { HomeEventsPageHoc }
/>
<Redirect exact from='/users/:nickName' to='/users/:nickName/1' />
<Route exact path = "/users/:nickName/:pageId"
component = { UserProfileHoc }
/>
<ProtectedRoute exact path = "/tickers/:tickerSymbol/create-topic"
component = { CreateTopicPageHoc }
/>
<Redirect exact from='/tickers/:tickerSymbol' to='/tickers/:tickerSymbol/topics' />
<Redirect exact from='/tickers/:tickerSymbol/topics' to='/tickers/:tickerSymbol/topics/1' />
<Route path = "/tickers/:tickerSymbol/topics/:pageId"
component = { TickerPageHoc }
/>
<Redirect exact from='/tickers/:tickerSymbol/calendar' to='/tickers/:tickerSymbol/calendar/1' />
<Route exact path = "/tickers/:tickerSymbol/calendar/:pageId"
component = { TickerCalendarPageHoc }
/>
<Route path = "/topics/:topicId"
component = { TopicByIdPageHoc }
/>
<Route exact path = "/about"
component = { AboutPage }
/>
<Route exact path = "/help"
component = { HelpPage }
/>
<Route exact path = "/sectors"
component = { SectorPageHoc }
/>
<Redirect exact
from='/sectors/:sectorId'
to='/sectors/:sectorId/industries' />
<Route exact path = "/sectors/:sectorId/industries"
component = { SectorPageHoc }
/>
<Redirect exact
from='/sectors/:sectorId/industries/:industryId'
to='/sectors/:sectorId/industries/:industryId/tickers' />
<Route exact path = "/sectors/:sectorId/industries/:industryId/tickers"
component = { SectorPageHoc }
/>
<Route exact path = "/not-found"
component = { NotFoundPage }
/>
<Redirect from='*' to='/' />
</Switch>
</ScrollToTop>
</AppLayout>
);
};
export default withRouter(AppRouter);
When I bluntly replace the client router (using Suspense) with the router I use on the server (the one not using Suspense, but just import all react components directly and passes them to the common AppRouter), this empty page 'flicker' does not occur. So I can only assume that it is related to Suspense.
If this is not expected behaviour, it would be a bug (or I'm missing something).
only applying Suspense / lazy loading to the client router.
That's the source of the problem. It is not supported to conditionally render something different on the server and on the client. Suspense is throwing on the server precisely because if you try to use it this way, you'll see a flash. By rendering something different, you are ignoring what React is trying to tell you (that lazy doesn't work in SSR apps as of today).
As the docs say, using lazy and Suspense with server rendering is not currently supported and you should consider other solutions. It will be supported in the future versions of React (as part of Concurrent Mode) but not today.
Regarding your concrete feature request, I think I can reframe it slightly differently. It's not that you want "optional fallback" since that wouldn't make sense for new screens (we've got to show _something_). What I believe you're looking for is a way to _skip showing the fallback if the content is already in HTML_. This is precisely how React behaves in Concurrent Mode so the feature request is already implemented (and will eventually become the default behavior in a stable release).
@gaearon The last message that you posted is exactly what I was aiming at. Sorry if my explanation may have been a bit contrived. Ok, I will play with concurrent mode and wait for stable release.
Can't promise that it works with React Router today (even in experimental releases) but here's a demo of progressive hydration: https://codesandbox.io/s/floral-worker-xwbwv. If you add Suspense there but only actually suspend on the client, React should not blow away the existing DOM.