My server is a bit slow and when displaying even small size images, you can see them loading on screen, and I would like them to be displayed right away.
Thankfully I have a landing page which then after pressing a button, redirects to the main page which contains the images.
This routing is controlled in my app.js which is like the root file. This file routes to the landing page and to the main page. I thought that I could just load the images there and then pass them as props via the router, to the main page:
import projectImage1 from './assets/1.png';
import projectImage2 from './assets/2.png';
import projectImage3 from './assets/3.png';
class App extends Component {
render() {
return (
<Route render={({location}) => (
<TransitionGroup>
<CSSTransition
key={location.pathname === '/' ? location.key : null} // Use transition only if on landing page
timeout={800}
classNames="fade"
>
<Switch location={location}>
<Route path="/" exact component={Landingpage} />
<Route path="/app/:name" exact render={(props) => <MainApp
projectImage1={projectImage1}
projectImage2={projectImage2}
projectImage3={projectImage3}
/>} />
</Switch>
</CSSTransition>
</TransitionGroup>
)} />
);
}
}
As you can see my MainApp.js received the 3 images as props. This is technically working without errors but I'm still seeing the images loading on screen. Why is it so? How can I load them in advance so that they can be displayed smoothly?
Thanks
The browser doesn't load resources before it needs by default. For images this is when the image itself is visible. This is due to performance, but there is a way for you to load these images beforehand.
If you do this in your componentDidMount lifecycle method in your App component, the browser will load the images:
class App extends Component {
componentDidMount() {
const imagesToBePreloaded = [projectImage1, projectImage2, projectImage3]
imagesToBePreloaded.forEach(image => { new Image().src = image })
}
render() {
// [...] Removed for abbreviation.
}
}
I'm using the dynamic import() syntax to split each route component into a separate bundle and React.lazy to load the components only when they are required. I have a PageLoading component which serves as the fallback content for <React.Suspense />.
Is it possible to keep the current view component mounted until the "lazy loaded" component is ready to be rendered? The bundles are quite small individually so inevitably the page just ends up flashing for less than half a second while the "fallback" is rendered.
Thank you.
Edit: I am not looking to create an artificial delay. What am I asking for is exactly as I have described.
I wrote a component that accepts the lazy component you want to render and sets the fallback property of <Suspense /> to be the previously rendered component:
import * as React from 'react';
export type LazyProps = {
component: React.LazyExoticComponent<() => JSX.Element>;
initialFallback?: JSX.Element;
};
export const Lazy = ({ component, initialFallback = <></> }: LazyProps): JSX.Element => {
const fallback = React.useRef(() => initialFallback);
const Component = component;
const updateFallback = async (): Promise<void> => {
const result = await component._result;
fallback.current = typeof result === 'function' ? result : (result as any).default;
};
React.useEffect(() => {
updateFallback();
}, [component]);
return (
<React.Suspense fallback={<fallback.current />}>
<Component />
</React.Suspense>
);
};
Which can be used as such:
<Lazy component={MyLazyComponent} />
Do you mean something like this?
I was also searching for the right approach and tried many ways. Currently my best approach is to update the fallback whenever a page is rendered whether that page is lazy-loaded or not. So the fallback must be a state instead of something like
<div>Loading...</div>
It must be the same component instance as the currently rendered page.
Here is the source code for it.
Is it possible to keep the current view component mounted until the "lazy loaded" component is ready to be rendered?
Yes, my answer to a different question also applies here. Without seeing a code example, I don't know your exact situation - but you can achieve this behavior by simply wrapping your entire router Switch in Suspense.
For example:
<Suspense fallback={<PageLoading />}>
<Switch>
<Route exact path="/" component={Page1} />
<Route path="/page-2" component={Page2} />
<Route path="/page-3" component={Page3} />
</Switch>
</Suspense>
When I navigate to a new page, Reach Router scrolls down to the page content past the header (if the content is long enough). I'm assuming this is for accessibility but it's not necessary for my app and it's actually quite jarring. Can this behaviour be disabled?
Note that I'm talking about Reach Router not React Router.
Reach Router
Try using <Router primary={false}> which will not focus on the route component.
https://reach.tech/router/api/Router
primary: bool
Defaults to true. Primary Routers will manage focus on location transitions. If false, focus will not be managed. This is useful for Routers rendered as asides, headers, breadcrumbs etc. but not the main content.
WARNING: If you are concerned about breaking accessibility please see this answer: https://stackoverflow.com/a/56996986/428780
The top answer here, while solving the OP's problem, is probably not the solution most people want, since it turns off the most important accessibility feature of Reach router.
The fact Reach router focuses the content of the matched <Route> on a route change is for accessibility reasons - so screen readers etc can be directed to the newly updated, relevant content, when you navigate to a new page.
It uses HTMLElement.focus() to do this - see the MDN docs here.
The problem is that by default, this function scrolls to the element being focused. There is a preventScroll argument which can be used to turn this behaviour off, but the browser support for it is not good, and regardless, Reach Router does not use it.
Setting primary={false} turns this behaviour off for any nested <Router> you may have - it is not intended to set false on your main (primary) <Router> -- hence the name.
So, setting primary={false} on your primary <Router>, as the top answer suggests, 'works' in the sense that it stops the scrolling behaviour, but it achieves this by simply turning off the focusing behaviour completely, which breaks the accessibility feature. As I said, if you do this, you're breaking one of the main reasons to use Reach Router in the first place.
So, what's the solution?
Basically, it seems that this side effect of HTMLElement.focus() - scrolling to the focused element - is unavoidable. So if you want the accessibility feature, you have to take the scrolling behaviour with it.
But with that said, there might be a workaround. If you manually scroll to the top of the page using window.scrollTo(0, 0) on every route change, I believe that will not 'break' the focusing feature from an accessibility perspective, but will 'fix' the scrolling behaviour from a UX perspective.
Of course, it's a bit of a hacky and imperative workaround, but I think it's the best (maybe only) solution to this issue without breaking accessibility.
Here's how I implemented it
class OnRouteChangeWorker extends React.Component {
componentDidUpdate(prevProps) {
if (this.props.location.pathname !== prevProps.location.pathname) {
this.props.action()
}
}
render() {
return null
}
}
const OnRouteChange = ({ action }) => (
{/*
Location is an import from #reach/router,
provides current location from context
*/}
<Location>
{({ location }) => <OnRouteChangeWorker location={location} action={action} />}
</Location>
)
const Routes = () => (
<>
<Router>
<LayoutWithHeaderBar path="/">
<Home path="/" />
<Foo path="/foo" />
<Bar path="/bar" />
</LayoutWithHeaderBar>
</Router>
{/*
must come *after* <Router> else Reach router will call focus()
on the matched route after action is called, undoing the behaviour!
*/}
<OnRouteChange action={() => { window.scrollTo(0, 0) } />
</>
)
Building off of #Marcus answer, you can get rid of the jank with useLayoutEffect() instead of useEffect() - this way the scroll action happens after the DOM has been fully rendered, so you don't get the weird "bounce."
// ScrollToTop.js
import React from 'react'
export const ScrollToTop = ({ children, location }) => {
React.useLayoutEffect(() => window.scrollTo(0, 0), [location.pathname])
return children
}
I had to use a combination of things to make it work. Even with setting primary={false} there were still cases where the pages would not scroll to the top.
<Router primary={false}>
<ScrollToTop path="/">
<Home path="/" />
<Contact path="contact-us" />
<ThankYou path="thank-you" />
<WhoWeAre path="who-we-are" />
</ScrollToTop>
</Router>
This is based off of React Router's scroll restoration guide.
The scroll to top component will still work with you don't have primary={false}, but it causes jank from where it focuses the route and then calls window.scrollTo.
// ScrollToTop.js
import React from 'react'
export const ScrollToTop = ({ children, location }) => {
React.useEffect(() => window.scrollTo(0, 0), [location.pathname])
return children
}
There must be another thing to cause this. Like #Vinicius said. Because for my application primary={false} really works. I have a small application and my routes below.
<Router primary={false}>
<Home path="/" />
<Dashboard path="dashboard" />
<Users path="users" />
<Sales path="sales" />
<Settings path="settings" />
</Router>
Using primary={false} does not solve all the cases. A common solution is to wrap the page and use useEffect to achieve the result. There might be a flash issue as someone pointed out, although it never happened to me, so try your luck.
<PageWrapper Component={About} path="/about" />
const PageWrapper = ({ path, Component }) => {
useEffect(() => {
window.scrollTo(0, 0)
}, [path])
return <Component path={path} />
}
As a notice, in React Router, you could use history.listen as explained here. I did not check if there is a similar solution in Reach Router, but that solution would be more optimal.
I made an npm package out of it to make integration simple: https://www.npmjs.com/package/reach-router-scroll-top
This is a basic situation: I have a Nav with links and several routes for those links in a main content area.
const App = () => (
<div id="page-container">
<Nav />
<main id="main">
<Switch>
<Route exact path="/" component={IndexPage} />
<Route exact path="/blog" component={BlogPage} />
<Route exact path="/about" component={AboutPage} />
<Route component={NotFoundPage} />
</Switch>
</main>
</div>
);
Sometimes when I click the nav links the main content area updates instantaneously, and sometimes it takes maybe 2 or 3 seconds to load the next component. I believe that's an outrageous amount of time to wait (with no indication that anything is loading).
All I seemed to find was React Router Transitions, which I tried, but it seemed like it required a static timeout for the transition animation. I only want a loading animation to appear where necessary; I do not want the routes animated every single time. React Transition Group also seems to require each route to be wrapped in a Transition component, which requires a location argument, which I don't seem able to have in my current layout.
Here's what I hoped to do:
I hoped to add a boolean state variable to my Page component to keep track of loading:
class Page extends React.Component {
constructor (props) {
super(props);
this.state = {
loading: true
};
}
componentDidMount() {
this.setState({
loading: false
});
}
render() {
return (
<article className={this.state.loading ? 'loading' : ''}>
... stuff ...
</article>
);
}
}
But this doesn't seem to work, because componentDidMount fires after render, i.e. after all of the loading time. So, basically, (1) it loads for awhile, (2) renders article with the 'loading' class for probably a few milliseconds until (3) componentDidMount sets the state and renders again, replacing the class.
So I'm guessing I need to add the loading animation on the previous page component, instead of the next page component? Or is it better to ajax all my page content? I'm really surprised how little information I've found on this, and I'm hoping someone has some helpful advice. Thanks!
Try using React Lazy and Suspense fallback
I have a component that takes a while to load. Actually, it's a component which loads an <iframe> of another website, which takes a while to load.
I would like the component to mount and therefore run the componentDidMount code block that loads the iframe so that when the user clicks on the 'create' tab, the user instantly sees the iframe in the correct the <main> section of the page.
Is there a way to instruct react-router to pre-load the component while retaining the same conditional rendering logic on route change and retaining the position on the page of the rendered component?
This is currently my render() statement on the root level of the app to give you some context:
render() {
return (
<div className="App">
<Nav />
<Snackbar
open={this.props.snackbar.get().open}
message={this.props.snackbar.get().message}
autoHideDuration={4000}
onRequestClose={() => this.handleSnackbarRequestClose()}
/>
<TreeViewer />
<PayloadListener/>
<main>
<ThankYouModal open={this.props.showConfirmationModal.get()} handleClose={ () => this.props.showConfirmationModal.set(false) }/>
<Switch>
<Route path="/imageservices" component={ImageServicesController} />
<Route path="/create" component={Iframe} />
<Route exact path="/account" component={Account} />
<Route exact path="/analytics" component={AnalyticsController} />
<Route path="/support" component={SupportView} />
<Route path='/login' render={ (props) => <Login { ...props } /> } />
<Route path='/logout' render={ (props) => <Logout { ...props } /> } />
</Switch>
</main>
</div>
);
}
This is the component I would like React Router to pre-load:
<Route path="/create" component={Iframe} />
How can I achieve that?
Well if you take a look at a React component lifecycle you can see that render always runs before componentDidMount. So effectively you wouldn't be able to mount a component without rendering it.
These are the lifecycle methods called while mounting:
constructor()
static getDerivedStateFromProps()
render()
componentDidMount()
You can try a few things:
Render the iframe when you need it and fade it in when it is loaded.
You can easily add an eventListener for the load event of the iframe and make the appropriate changes for it to fadeIn when the state changes.
...
componentDidMount() {
this.iframe.addEventListener("load", this.handleLoad);
}
componentWillUnmout() {
this.iframe.removeEventListener("load", this.handleLoad);
}
...
<iframe ref={ref => this.iframe = ref } />
...
I've done this when I didn't always need the iframe. It was something sporadic and it was an always one page kinda thing.
You can play around with the preload attribute
This is ideal if you know the user will stumble upon the iframe content. You can start preloading content that the user will most likely encounter effectively preventing the user from waiting.
You can add this to the head of your document:
<link rel="preload" href="your-doc-url" as="document" />
And then use your iframe normally:
<iframe src="your-doc-url"></iframe>
If your iframe url is dynamic and dependent on some info from the authenticated user and you can't put it right away in your html file remember you can always use react-helmet to render it to the head tag whenever you have the info you want.
Not via react-router, but you can use link preload as document in your index.html to ensure the document is lazy-loaded by the browser. It's intended purpose is to preload documents which can then be displayed in iframe. You also won't need to change your router mechanism.
Read more about this here - https://developer.mozilla.org/en-US/docs/Web/HTML/Preloading_content.
Basically, just add this to the head of your index.html:
<link rel='preload' href='your_iframe_url' as='document'>