React Suspense lazy loading without fallback - javascript

I want to lazy load my components to decrease my initial bundle size and get components on the fly using code splitting using react router.
However, when using React Suspense, they force you to use a fallback for loading.
This wouldn't work:
const lazyLoadComponent = Component =>
props => (
<Suspense> // Missing fallback property
<Component {...props} />
</Suspense>
);
In my case I am rendering html from the server so I don't want to use a spinner.
This would create a useless flicker on my screen!
I.e.:
Html loads
Place holder appears
PageComponent for the route gets loaded
I have my own spinner that loads a feed from within the page component
In my case the html corresponds to the react component that gets loaded.
Is there any known hack to easily work around this problem (except for creating a loader for any route that copies the html (!!), which by the way, would make lazy loading useless).
I am a bit displeased with "forcing" us to add a loader and I don't understand the logic behind the decision to make it mandatory.

Try to use code splitting in the docs
fallback props is just a React element, you can set it for null.
const MyLazyComponent= React.lazy(() => import('./MyComponent'));
<Suspense fallback={null}>
<MyLazyComponent />
</Suspense>

I created an issue for this on Github: https://github.com/facebook/react/issues/19715
There isn't a current clean solution using React-Router / React.
This is however foreseen in a future release using concurrent mode. As mentioned by Dan Abramov:
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).
For me it is not a problem to wait, so currently I will omit lazy-loading the routes as this concerns a hobby-project and I have time to wait for a future release.

In my experience (with React 17), there's no flickering happens if you pass null to fallback param.
I have a Modal component that renders lazy components.
Here's my Typescript solution:
type LazyLoadHOC = {
component: React.LazyExoticComponent<any>,
fallback?: React.ComponentType | null,
[x:string]: any
};
export const LazyLoad: React.FC<LazyLoadHOC> = ({
component: Component, fallback = null, ...props
}) => {
return (
<React.Suspense fallback={fallback}>
<Component {...props} />
</React.Suspense>
);
};
Here's my Modal:
const AddressFormModel = React.lazy(() => import('#components/address/address-form-modal'));
<Modal show={isOpen} backdrop={'static'} dialogClassName='custom-modal'>
<ModalBody>
{view === 'ADDRESS-FORM' && <LazyLoad component={AddressFormModel} />}
</ModalBody>
</Modal>
This will ensure to not trigger your global React.Suspense.

So basically all we have to do is, wrap the loader in a component and load the script using that component (Loadable here).
This way we can use React.lazy wihtout any animation.
Since our fallback is empty you wont see any animation and your content will be visible always. There will not be any flicker of content hiding.
loadable.js
import React, { Suspense } from 'react';
const Loadable = (Component) => (props) => (
<Suspense fallback={<></>}>
<Component {...props} />
</Suspense>
);
export default Loadable;
We can wrap the import in Loadable so that it will load lazy.
route.js
import React, { lazy } from 'react';
import Loadable from './components/Loadable';
// page import
const Home = Loadable(lazy(() => import('./pages/home')));
// define routes
const routes = [
{
path: "dashboard",
element: <Layout />,
children: [
{
path: "",
element: <Home />,
},
]
},
]

Related

on NextJS How to prerender shared components of my site?

I have a site built with nextjs with a top navigation bar and a footer. Since those are the same on every page, I didn't add them to their individual js files under pages/, instead, I have a 'wrapper' that adds those to each page. The data of both come from a JSON (2 different languages).
I was trying to use getServerSideProps but that won't work because the navigation bar itself is not inside the page.
What is the 'correct' way of doing this? I cannot hardcode the JSON in the code because it might change and I would prefer not to have to add those sections on the code of every page I have.
I see that the nextjs is preparing a release with the possibility to get static props in _app.js but what is the current solution for this problem?
_app.js
export default function MyApp({ Component, pageProps }) {
return (
<AppWrapper>
<Component {...pageProps} />
</AppWrapper>
);
}
in AppWrapper I create the app context with react hook (createContext) and then wrap it
const WebWrapper = ({ children }) => {
return (
<>
<NavBar />
<main>{children}</main>
<Footer />
</>
);
};

How to have the same component on different routes in next js? [duplicate]

Trying next with layout pattern:
https://github.com/zeit/next.js/tree/canary/examples/layout-component
And the problem is that Layout component get remounted on every page change. I need to use layout component as a Container so it'll fetch data from server on every mount. How can I prevent layout to get re-mounted? Or am I missing something there?
This helped me for persistent layouts. The author puts together a function that wraps your page components in your Layout component and then passes that fetch function to your _app.js. This way the _app.js is actually the components that renders the Layout but you get to specify which pages use which layout (in case you have multiple layouts).
So you have the flexibility of having multiple layouts throughout your site but those pages that share the same layout will actually share the same layout component and it will not have to be remounted on navigation.
Here is the link to the full article
Persistent Layout Patterns in Next.js
Here are the important code snippets. A page and then _app.js
// /pages/account-settings/basic-information.js
import SiteLayout from '../../components/SiteLayout'
import AccountSettingsLayout from '../../components/AccountSettingsLayout'
const AccountSettingsBasicInformation = () => (
<div>{/* ... */}</div>
)
AccountSettingsBasicInformation.getLayout = page => (
<SiteLayout>
<AccountSettingsLayout>{page}</AccountSettingsLayout>
</SiteLayout>
)
export default AccountSettingsBasicInformation
// /pages/_app.js
import React from 'react'
import App from 'next/app'
class MyApp extends App {
render() {
const { Component, pageProps, router } = this.props
const getLayout = Component.getLayout || (page => page)
return getLayout(<Component {...pageProps}></Component>)
}
}
export default MyApp
If you put your Layout component inside page component it will be re-remounted on page navigation (page switch).
You can wrap your page component with your Layout component inside _app.js, it should prevent it from re-mounting.
Something like this:
// _app.js
import Layout from '../components/Layout';
class MyApp extends App {
static async getInitialProps(appContext) {
const appProps = await App.getInitialProps(appContext);
return {
...appProps,
};
}
render() {
const { Component, pageProps } = this.props;
return (
<Layout>
<Component {...pageProps} />
<Layout />
);
}
}
export default MyApp;
Also, make sure you replace all the to <Link href=""></Link>, notice that only have change the Html tag to link.
I struggled because with this for many days, although I was doing everything else correctly, these <a> tags were the culprit that was causing the _app.js remount on page change
Even though this is the topic Layout being mounted again and again, the root cause of this problem is that you have some data loaded in some child component which is getting fetched again and again.
After some fooling around, I found none of these problem is actually what Next.Js or SWR solves. The question, back to square one, is how to streamline a single copy of data to some child component.
Context
Use context as a example.
Config.js
import { createContext } from 'react'
export default createContext({})
_App.js
import Config from '../Config'
export default function App({ Component, pageProps }) {
return (
<Config.Provider value={{ user: { name: 'John' }}}>
<Component {...pageProps} />
</Config.Provider>
)
}
Avatar.js
import { useContext } from 'react'
import Config from '../Config'
function Avatar() {
const { user } = useContext(Config)
return (
<span>
{user.name}
</span>
)
}
export default Avatar
No matter how you mount and dismount, you won't end up with re-render, as long as the _app doesn't.
Writable
The above example is only dealing with readable. If it's writable, you can try to pass a state into context. setUser will take care the set in consumer.
<Provider value={useState({})} />
const [user, setUser] = useContext(Config)
setUser is "cached" and won't be updated. So we can use this function to reset the user anytime in child consumer.
There're other ways, ex. React Recoil. But more or less you are dealing with a state management system to send a copy (either value or function) to somewhere else without touching other nodes. I'll leave this as an answer, since even we solved Layout issue, this problem won't disappear. And if we solve this problem, we don't need to deal with Layout at all.

Typescript is absurdly slow when developing with dynamic imports, React.Suspense

Edit
According to the docs,
The import() must contain at least some information about where the module is located. Bundling can be limited to a specific directory or set of files so that when you are using a dynamic expression - every module that could potentially be requested on an import() call is included. For example, import(./locale/${language}.json) will cause every .json file in the ./locale directory to be bundled into the new chunk. At run time, when the variable language has been computed, any file like english.json or german.json will be available for consumption.
So what's basically happening here, is that when I do,
const Component = React.lazy(() => import(`#material-ui/icons/${iconName}`));
instead of,
const Component = React.lazy(() => import(`#material-ui/icons/DoneAll`));
It literally bundles the whole node_modules directory each time you make changes in the code and hit save.
Pity it doesn't work as I'd like it to, but good to know I guess. Still unsure why the Sandbox works just fine.
I'm running into a very frustrating issue with Typescript when I'm developing with dynamic import() in React, (Github repo and Sandbox links below).
Consider the below code:
import React from 'react';
const MySuspense = React.memo(() => {
const Component = React.lazy(() => import(`#material-ui/icons/DoneAll.js`));
return (
<React.Suspense fallback={<div />}>
<Component />
</React.Suspense>
);
});
export default function App() {
return (
<MySuspense />
);
}
This works just fine and every time I make change to the code and save them, the TS compiler recompiles everything in about a second. Cool.
But say I want to make dynamic imports... well, dynamic. Consider this:
import React from 'react';
type TSuspense = {
iconName: string;
};
const MySuspense = React.memo(({ iconName }: TSuspense) => {
const Component = React.lazy(() => import(`#material-ui/icons/${iconName}.js`));
return (
<React.Suspense fallback={<div />}>
<Component />
</React.Suspense>
);
});
export default function App() {
return (
<MySuspense iconName={'Done'} />
);
}
With this code, whenever I make changes to the code, I have to wait upwards of ~15s for the compiler to recompile everything and show me those changes.
What is also strange, is that in the below Sandbox, everything works as expected. The above dynamic code works just fine and the changes are reflected instantly.
This is beyond frustrating, what am I doing wrong and how can I get around this? I've tried the code on 3 different machines and all have the same behavior. Why would the Sandbox be any different?
Github
Sandbox

How to use page query while using root-wrapper for providing Theme with Gatsby

I am using wrapRootElement function in gatsby for providing ThemeProvider of styled-component on the root element. Here is the code :
export const wrapRootElement = () => {
if (typeof window === 'undefined') return <span>Something went wrong!</span>
return(
<ThemeProvider theme={theme}>
<FadeTransitionRouter>
<Page path="/" page={<IndexPage />} />
<Page path="/plan" page={<PlanPage />} />
<Page path="/promos/:slug" page={<Template />} />
</FadeTransitionRouter>
</ThemeProvider>
)};
Because of this I am compelled to use staticQuery in pages as page queries are not working there. But I need to use page queries to introduce variables in graphql queries. How can I achieve this ? If I do not use wrapRootElement, my page queries works fine. But it is important for me to use it as I need to provide my external Theme to all the pages.
Thanks in advance.
You aren't wrapping the root element here, you're replacing the entirety of the Gatsby app tree. You're probably seeing warnings about page queries being ignored because they're in files that aren't being treated like pages, which is likely where the source of confusion is.
Here's the example the Gatsby docs provide:
exports.wrapRootElement = ({ element }) => {
return (
<Provider store={store}>
{element}
</Provider>
)
}
Note how this example accepts an element prop that it passes down as the children to the Provider component? That's what you're missing. You probably want something like this:
export const wrapRootElement = ({ element }) =>
<ThemeProvider theme={theme}>
{element}
</ThemeProvider>
You don't need to check for the existence of window here. If you're in gatsby-browser.js it will only ever be executed in the context of the browser. If you're in gatsby-ssr.js it'll only ever be executed in the context of node. Either way, they need to produce the same DOM output in order to have React hydrate cleanly. Bailing in the way you've shown will result in your entire DOM being replaced when React hydrates, which is less efficient than a client-side rendered app.
Finally, I removed the FadeTransitionRouter bit because you haven't shared what that is and it is unlikely to be compatible with Gatsby routing. If you'd like to have page transitions, this page in the documentation has details on how to set this up.

Dynamically inject data in React Router Routes

I've been working on trying to modularize my React.js app (that will be delivered as a Desktop app with Electron) in a way that if I make a new module in the future, I can just add a new folder and modify a couple of files and it should integrate fine.
I got originally inspired by this article: https://www.nylas.com/blog/react-plugins/
After that point, I started doing as much research as I could and ended up creating a JSON file that would live in the server with a manifest of the plugins that are registered for that specific client.
Something like this:
{
"plugins": [
{
"name": "Test Plugin",
"version": "0.0.1",
"path": "testplugin",
"file": "test",
"component":"TestPlugin"
},
{
"name": "Another Plugin",
"version": "0.0.1",
"path": "anothertest",
"file": "othertest",
"component":"TestPluginDeux"
}
]
}
After that, I made a couple folders that match the path value and that contain a component that matches the name in the manifest (e.g. testplugin/test.jsx that exports the TestPlugin component as a default). I also made a pluginStore file that reads the manifest and mounts the plugins in the this.state.
Then, did a ton of research on Google and here and found this answer: React - Dynamically Import Components
With that function, I was able to iterate through the manifest, find the folders in the directory, and mount the plugins in the this.state by running the mountPlugins() function I had created in the pluginStore, inside a componentDidMount() method in my homepage.
So far so good. I'm using React-Router and I was able to mount the plugins dynamically in the State and able to load them in my Home Route by just calling them like this: <TestPlugin />.
The issue that I have now, is that I wanted to dynamically create Routes that would load these components from the state, either by using the component or the render method, but I had no luck. I would always get the same result... Apparently I was passing an object instead of a String.
This was my last iteration at this attempt:
{this.state.modules.registered.map((item) =>
<Route exact path={`/${item.path}`} render={function() {
return <item.component />
}}></Route>
)}
After that, I made a Route that calls a PluginShell component that is called by a Navlink that sends the name of the plugin to inject and load it dynamically.
<Route exact path='/ex/:component' component={PluginShell}></Route>
But I ended having the same exact issue. I'm passing an object and the createElement function expected a string.
I searched all over StackOverflow and found many similar questions with answers. I tried applying all the possible solutions with no luck.
EDIT:
I have put together a GitHub repo that has the minimal set of files to reproduce the issue.
Here's the link:
https://codesandbox.io/embed/aged-moon-nrrjc
Okey pokey. There are a lot of moving parts here that can be vastly simplified.
I'd recommend moving toward a more developer-friendly, opinionated state store (like Redux). I've personally never used Flux, so I can only recommend what I have experience with. As such, you can avoid using plain classes for state management.
You should only import the modules ONCE during the initial application load, then you can dispatch an action to store them to (Redux) state, then share the state as needed with the components (only required if the state is to be shared with many components that are spread across your DOM tree, otherwise, not needed at all).
Module imports are asynchronous, so they can't be loaded immediately. You'll have to set up a condition to wait for the modules to be loaded before mapping them to a Route (in your case, you were trying to map the module's registered string name to the route, instead of the imported module function).
Module imports ideally should be contained to the registered modules within state. In other words, when you import the module, it should just overwrite the module Component string with a Component function. That way, all of the relevant information is placed within one object.
No need to mix and match template literals with string concatenation. Use one or the other.
Use the setState callback to spread any previousState before overwriting it. Much simpler and cleaner looking.
Wrap your import statement within a try/catch block, otherwise, if the module doesn't exist, it may break your application.
Working example (I'm just using React state for this simple example, I also didn't touch any of the other files, which can be simplified as well):
App.js
import React from "react";
import Navigation from "./components/MainNavigation";
import Routes from "./routes";
import { plugins } from "./modules/manifest.json";
import "./assets/css/App.css";
class App extends React.Component {
state = {
importedModules: []
};
componentDidMount = () => {
this.importPlugins();
};
importPlugins = () => {
if (plugins) {
try {
const importedModules = [];
const importPromises = plugins.map(plugin =>
import(`./modules/${plugin.path}/${plugin.file}`).then(module => {
importedModules.push({ ...plugin, Component: module.default });
})
);
Promise.all(importPromises).then(() =>
this.setState(prevState => ({
...prevState,
importedModules
}))
);
} catch (err) {
console.error(err.toString());
}
}
};
render = () => (
<div className="App">
<Navigation />
<Routes {...this.state} />
</div>
);
}
export default App;
routes/index.js
import React from "react";
import React from "react";
import isEmpty from "lodash/isEmpty";
import { Switch, Route } from "react-router-dom";
import ProjectForm from "../modules/core/forms/new-project-form";
import NewPostForm from "../modules/core/forms/new-post-form";
import ProjectLoop from "../modules/core/loops/project-loop";
import Home from "../home";
const Routes = ({ importedModules }) => (
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/projectlist/:filter" component={ProjectLoop} />
<Route exact path="/newproject/:type/:id" component={ProjectForm} />
<Route exact path="/newpost/:type" component={NewPostForm} />
{!isEmpty(importedModules) &&
importedModules.map(({ path, Component }) => (
<Route key={path} exact path={`/${path}`} component={Component} />
))}
</Switch>
);
export default Routes;
You've got the right idea, if anything I guess your syntax is slightly off. I didn't have to tweak much from your example to get dynamic routing work.
Here's a working example of what I think you want to do:
const modules = [{
path: '/',
name: 'Home',
component: Hello
},{
path: '/yo',
name: 'Yo',
component: Yo
}];
function DynamicRoutes() {
return (
<BrowserRouter>
{ modules.map(item => <Route exact path={item.path} component={item.component}/>) }
</BrowserRouter>
);
}
https://stackblitz.com/edit/react-zrdmcq
I think the problem is the way you are trying to render <item.component /> but not sure, did you get the same error whit this?
Try:
<Route exact path={`/${item.path}`} render={function() {
return React.createElement(item.component, props)
}}></Route>
)}

Categories