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
Related
I have React application which has a structure similar to the following.
class App extends Component {
render() {
return (
<div className="App">
<NavBar />
<Router>
<Switch>
<Route path="/login" component={LoginPage} />
<Route path="/" exact component={DashboardPage} />
<Route path="/admin" exact component={AdminPage} />
// many other routes
<Route component={NotFound} />
</Switch>
</Router>
</div>
);
}
}
I do not want the login page to display the <NavBar /> element. I tried using the sessionStorage to get the userId and only display the navigation if the value is set. When I do this and go to the login page and the nav bar is not there. But when I log in, it's still not there. If I refresh however, it will appear.
I know one way to solve this is to make some sort of wrapper around the pages that do want the navigation, but I'd rather not have all of that code duplication, etc.
I feel this must be a common want, and I'm missing something dumb. Any help would be appreciated. I'm new to React so I don't follow everything that's going on here. Thanks.
I think your way of conditionally showing the NavBar is the right way. The question is how to trigger a state change so that the render method takes care of hiding and showing the NavBar, when you log in and out. I suggested maintaining a isLoggedIn state in your App component, and rendering the NavBar based on that, instead of directly accessing the SessionStorage. You could then use a custom event to update the state, when SessionStorage changes.
See this question for updating state based on Storage (in short, you fire and handle a custom event for storage changes): How to listen to localstorage in react.js
This might still be more code that you had hoped for, but it's more aligned with how React works, to derive the view (render) from component state.
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>
I'm using react-loadable library for lazy-loading in reactJS. It works very well for rendering component. But when i use the delay property, the render time is not effected. So, What i need to update here ?
const Home = Loadable({
loader: () => import('./Home'),
loading: Loading,
delay: 5000
});
const Test = Loadable({
loader: () => import('./Test'),
loading: Loading,
delay: 5000
});
return (
<Router>
<div className="App">
<Link to="/"> Home </Link>
<Link to="/test"> Test </Link>
<Route exact path="/" component={Home} />
<Route path='/test' component={Test} />
</div>
</Router>
);
Thanks for any helping.
Delay doesn't affect the rendering time of the actual component but it delays the rendering time of Loading component.
Here is excerpt from official Documentation:
Avoiding Flash Of Loading Component
Sometimes components load really quickly (<200ms) and the loading screen only quickly flashes on the screen.
A number of user studies have proven that this causes users to perceive things taking longer than they really have. If you don't show anything, users perceive it as being faster.
So your loading component will also get a pastDelay prop which will only be true once the component has taken longer to load than a set delay.
Source: https://github.com/jamiebuilds/react-loadable#avoiding-flash-of-loading-component
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'>
I have an App component which, using react-router, holds a few components in two routes. I also have a Firebase data store which I want to bind to the state of App (using rebase) so I can pass it down to any component I wish as a prop. This is my App class:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
items: {}
};
}
componentDidMount () {
rebase.bindToState('items', {
context: this,
state: 'items'
})
}
render() {
return (
<Router>
<div className='container'>
<div className="header">
<h3>Header</h3>
<Nav/>
</div>
<Switch>
<Route exact path='/' component={() => <Home items={this.state.items} rebase={rebase} />} />
<Route render={function () {
return <p>Not Found</p>
}} />
</Switch>
</div>
</Router>
)
}
}
Now, when I load my page I get two mounts of the Home component. This in itself is not great. However, I have several actions in the Home component that use rebase to modify/read from Firebase. As a callback of these actions they also change the Home component's state. The problem is, whenever I do a Firebase call, it remounts the Home component and any state I have is lost.
If I remove the Router wrappers from the Home component, and render it purely as render( <Home items={this.state.items} rebase={rebase} /> ), my app works perfectly as intended. I don't know why wrapping it in Router stuff makes it not work. I thought it was because I had additional URL parameters that also changed when I call firebase updates (e.g. /?p=sgergwc4), but I have a button that changes that parameter without a firebase update and it doesn't cause any problems (i.e. doesn't cause a remount). So what's up with the Router?
Turns out the answer is simple; instead of component={}, I should use render={}. Fixes everything. It was in the docs too.