I'm using React lazy with Suspense to successfully code split my JS. Below mock up simply illustrates how I'm doing it.
const QuickView = React.lazy(() =>
import('quickview'),
);
<Suspense fallback={<></>}>
<QuickView/>
</Suspense>
However I have noticed that my styles have been chunked into a separate sheet containing all lazy components CSS. This sheet is then loaded immediately on page load instead of individually when the components JS is loaded.
I am using next 10.2.3 (working towards updating) with the below next.config.js
module.exports = withTM(
withCSS(
withSass({
cssModules: true,
cssLoaderOptions: {
importLoaders: 1,
localIdentName: '[local]___[hash:base64:5]',
},
assetPrefix,
transpileModules: [
'#web-core-sdk',
'#web-component-quickview',
'#web-component-library',
],
webpack(config) {
config.module.rules.forEach((rule) => {
if (String(rule.test) === String(/\.css$/)) {
rule.use.forEach((u) => {
if (u.options) {
u.options.modules = false;
}
});
}
});
return config;
},
}),
),
);
My ideal outcome is that the styles are individually chunked and only downloaded when the components js is loaded.
I have tried dynamically importing the stylesheet itself using below. However it seems as though if the import is reachable the chunk at page load will contain the styles instead of loading them when the import is called.
useEffect(() => {
if (!hasMountedQuickView) return;
(async () => {
await import('./QuickView.module.scss');
})();
}, []);
Thank you for any help
Related
I am using parcel as a bundler in React.js project.
How to load npm modules asynchronously in react.js?
There is only one page that uses one specific npm module so I didn't need to load it at first loading.
By avoiding this, I would like to reduce the bundle size.
Could you let me the proper way to do this?
========================
And also, if I understood anything wrongly about the bundle size optimization and lazy loading, please let me know.
By using Dynamic Import you may import the package when you really need the package.
You can use a dynamic import inside an useEffect hook like:
const Page = (props) => {
useEffect(
() => {
const [momentjsPromise, cancel] = makeCancelable(import("moment"));
momentjsPromise.then((momentjs) => {
// handle states here
});
return () => {
cancel?.();
};
},
[
/* don't forget the dependencies */
],
);
};
You can use dynamic imports.
Let's say you want to import my-module:
const Component = () => {
useEffect(() => {
import('my-module').then(mod => {
// my-module is ready
console.log(mod);
});
}, []);
return <div>my app</div>
}
Another way is to code-splitt Component itself:
// ./Component.js
import myModule from 'my-module';
export default () => <div>my app</div>
// ./App.js
const OtherComponent = React.lazy(() => import('./Component'));
const App = () => (
<Suspense>
<OtherComponent />
<Suspense>
);
my-module will be splitted along with Component.
These two patterns should work with any bundler, but it will work client side only.
We now use lazy-loading through loadable.lib for about 20 new files which used to load the npm module react-toastify synchronously. The changes are waiting in a draft PR but it seems that the unit tests are broken because they do not wait for the loadable.lib-passed module to be loaded.
Expected results
Be able to mock loadable.lib so that it works exactly like before but loads the given library synchronously and in the unit test this is seen as the children of loadable.lib resulted Component have access to that library and a first render does this successfully.
Actual results
The old snapshot (full of tags and nested things and props) and the new one (null) are not matching. These do not work:
// TODO: not working because loadable is used in many places
// and children are not always enough to render to avoid crashes,
// and even just with children there can be crashes
jest.mock('#loadable/component', (loadfn) => ({
lib: jest.fn(() => {
return { toast: {} };
}),
}));
If it is possible to mock the loadable.lib function to render its children instead of wait for some library to be loaded, I don't know how I can fill the undefined variables that the code uses because I have loadables that use loadables that use loadables and so on.
I've read that there are some WebPack hints such as webpackPrefetch and webpackPreload but I am not sure if it is a good road to go.
Relevant links on what I have tried
The code I am working on (and there are 19 other files like this one): https://github.com/silviubogan/volto/blob/1d015c145e562565ecfa058629ae3d7a9f3e39e4/src/components/manage/Actions/Actions.jsx (I am currently working on loading react-toastify through loadable.lib always.)
https://medium.com/pixel-and-ink/testing-loadable-components-with-jest-97bfeaa6da0b - I tried to do a similar thing like the code in that article but it is not working:
jest.mock('#loadable/component', async (loadfn) => {
const val = await loadfn();
return {
lib: () => val,
};
});
A little bit of code
Taken from the link above, this is how I currently use react-toastify (named LoadableToast):
/**
* Render method.
* #method render
* #returns {string} Markup for the component.
*/
render() {
return (
<LoadableToast>
{({ default: toast }) => {
this.toast = toast;
return (
<Dropdown
item
id="toolbar-actions"
Conclusion
To put it in other words, how can I mock a dynamic import? How can I make jest go over lazy loading and provide a value instead of making the test wait to receive a value?
Thank you!
Update 1
With the following new code, still not working:
jest.mock('#loadable/component', (load) => {
return {
lib: () => {
let Component;
const loadPromise = load().then((val) => (Component = val.default));
const Loadable = (props) => {
if (!Component) {
throw new Error(
'Bundle split module not loaded yet, ensure you beforeAll(() => MyLazyComponent.load()) in your test, import statement: ' +
load.toString(),
);
}
return <Component {...props} />;
};
Loadable.load = () => loadPromise;
return Loadable;
},
};
});
A better solution is to mock the Loadable components module. We can do this simply by using Jest mock capabilities (https://jestjs.io/docs/manual-mocks#mocking-node-modules) and Loadable itself in the following way:
Create a #loadable directory inside the mocks folder at the root level of your project (mocks should be at the same level of node_modules).
Inside the #loadable folder, create a component.js file. In this file we will mock the loadable function by writing the code below:
export default function (load) {
return load.requireSync();
}
That's it, now you should be able to run your unit tests as normal.
We did it by
jest.mock('#loadable/component', () => ({
lib: () => {
const MegadraftEditor = () => {} // Name of your import
return ({ children }) => children({ default: { MegadraftEditor } })
},
}))
In case there is only one default export it could also be
jest.mock('#loadable/component', () => ({
lib: () => {
const HotTable = () => {} // Name of your import
return ({ children }) => children({ default: HotTable })
},
}))
Then in the test you just need to dive() and use childAt() until you have the correct location of the component you want to lazy-load. Note that this will then not have any of your wrappers around the lazy-loaded component in the snapshot.
it('renders correct', () => {
const component = shallow(
<View
data={}
/>
)
.dive()
.childAt(0)
.dive()
expect(component).toMatchSnapshot()
})
I import CSS files from local files and node modules:
//> Global Styling
// Local
import "../styles/globals.scss";
// Icons
import "#fortawesome/fontawesome-free/css/all.min.css";
// Bootstrap
import "bootstrap-css-only/css/bootstrap.min.css";
// Material Design for Bootstrap
import "mdbreact/dist/css/mdb.css";
This works as intended on my local development version. All styles appear as they should be.
As you can see here, the styling is different on local and production.
(Take a look at font and buttons)
(Development left, Production right)
My next.config.js is:
//#region > Imports
const withSass = require("#zeit/next-sass");
const withCSS = require("#zeit/next-css");
const withFonts = require("next-fonts");
const withImages = require("next-images");
const withPlugins = require("next-compose-plugins");
//#endregion
//#region > Exports
module.exports = [
withSass({
webpack(config, options) {
config.module.rules.push({
test: /\.(png|jpg|gif|svg|eot|ttf|woff|woff2)$/,
use: {
loader: "url-loader",
options: {
limit: 100000,
},
},
});
return config;
},
}),
withPlugins([withCSS, withFonts, withImages]),
];
//#endregion
/**
* SPDX-License-Identifier: (EUPL-1.2)
* Copyright © 2020 InspireMedia GmbH
*/
It seems the MDB styling is being overwritten by bootstrap on building the app. I deploy my app by using next build && next export && firebase deploy and use the ./out folder for deployment source.
You can find the code here: https://github.com/aichner/nextjs-redux-template
If the issue is incorrect styling. (as you are using material-ui) :
Create _document.js under pages directory.
Fill the file with following code :
import React from "react";
import Document, { Html, Head, Main, NextScript } from "next/document";
import { ServerStyleSheets } from "#material-ui/styles"; // works with #material-ui/core/styles, if you prefer to use it.
import theme from "../Theme"; // change this theme path as per your project
export default class MyDocument extends Document {
render() {
return (
<Html lang="en">
<Head>
{/* Not exactly required, but this is the PWA primary color */}
<meta name="theme-color" content={theme.palette.primary.main} />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
// `getInitialProps` belongs to `_document` (instead of `_app`),
// it's compatible with server-side generation (SSG).
MyDocument.getInitialProps = async (ctx) => {
// Resolution order
//
// On the server:
// 1. app.getInitialProps
// 2. page.getInitialProps
// 3. document.getInitialProps
// 4. app.render
// 5. page.render
// 6. document.render
//
// On the server with error:
// 1. document.getInitialProps
// 2. app.render
// 3. page.render
// 4. document.render
//
// On the client
// 1. app.getInitialProps
// 2. page.getInitialProps
// 3. app.render
// 4. page.render
// Render app and page and get the context of the page with collected side effects.
const sheets = new ServerStyleSheets();
const originalRenderPage = ctx.renderPage;
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) => sheets.collect(<App {...props} />),
});
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
// Styles fragment is rendered after the app and page rendering finish.
styles: [
...React.Children.toArray(initialProps.styles),
sheets.getStyleElement(),
],
};
};
Reason : Material UI uses context behind the scenes to apply its styling. Due to NextJs server side rendering, this context will be lost. So, we need to tell Next to make use of that previous context. The above code does that.
Bit of an odd situation here - I have a website written in Vue and I want to demo a library I've written in react. I can avoid server side rendering (SSR) by wrapping ReactDOM.hydrate(ReactApp, document.getElementById('react'area')) but I don't want to do that. I want to render everything SSR, but I don't see how it's possible.
Here is my renderOnServer.js for vue:
process.env.VUE_ENV = 'server'
const fs = require('fs')
const path = require('path')
const filePath = './App/dist/server.js'
const code = fs.readFileSync(filePath, 'utf8')
const vue_renderer = require('vue-server-renderer').createBundleRenderer(code)
//prevent XSS attack when initialize state
var serialize = require('serialize-javascript')
var prerendering = require('aspnet-prerendering')
module.exports = prerendering.createServerRenderer(function (params) {
return new Promise((resolve, reject) => {
const context = {
url: params.url,
absoluteUrl: params.absoluteUrl,
baseUrl: params.baseUrl,
data: params.data,
domainTasks: params.domainTasks,
location: params.location,
origin: params.origin,
xss: serialize("</script><script>alert('Possible XSS vulnerability from user input!')</script>")
}
const serverVueAppHtml = vue_renderer.renderToString(context, (err, _html) => {
if (err) { reject(err.message) }
resolve({
globals: {
html: _html,
__INITIAL_STATE__: context.state
}
})
})
})
});
So basically I'm configuring SSR above to read server.js:
import { app, router, store } from './app'
export default context => {
return new Promise((resolve, reject) => {
router.push(context.url)
router.onReady(() => {
const matchedComponents = router.getMatchedComponents()
if (!matchedComponents.length) {
return reject(new Error({ code: 404 }))
}
Promise.all(matchedComponents.map(Component => {
if (Component.asyncData) {
return Component.asyncData({ store, context })
}
}))
.then(() => {
context.state = store.state
resolve(app)
})
.catch(reject)
}, reject)
})
}
and server.js above is just looking for the right vue component and rendering. I have a test react component:
import React from 'react'
export default class ReactApp extends React.Component {
render() {
return (
<div>Hihi</div>
)
}
}
and my vue component:
<template>
<div id="page-container">
<div id="page-content">
<h3 class="doc-header">Demo</h3>
<div id="react-page">
</div>
</div>
</div>
</template>
<script>
<script>
import ReactApp from './ReactApp.jsx'
import ReactDOM from 'react-dom'
export default {
data() {
return {
}
},
}
ReactDOM.hydrate(ReactApp, document.getElementById('#react-page'))
</script>
But obviously it won't work because I can't use document in SSR.
Basically, the purpose of hydrate is to match react DOM rendered in browser to the one that came from the server and avoid extra render/refresh at load.
As it was pointed in the comments hydrate should be used on the client-side and React should be rendered on the server with renderToString.
For example, on the server it would look something like this:
const domRoot = (
<Provider store={store}>
<StaticRouter context={context}>
<AppRoot />
</StaticRouter>
</Provider>
)
const domString = renderToString(domRoot)
res.status(200).send(domString)
On the client:
<script>
const domRoot = document.getElementById('react-root')
const renderApp = () => {
hydrate(
<Provider store={store}>
<Router history={history}>
<AppRoot />
</Router>
</Provider>,
domRoot
)
}
renderApp()
</script>
Technically, you could render React components server-side and even pass its state to global JS variable so it is picked up by client React and hydrated properly. However, you will have to make a fully-featured react rendering SSR support(webpack, babel at minimum) and then dealing with any npm modules that are using window inside (this will break server unless workarounded).
SO... unless it is something that you can't live without, it is easier, cheaper and faster to just render React demo in the browser on top of returned Vue DOM. If not, roll up your sleeves :) I made a repo some time ago with react SSR support, it might give some light on how much extra it will be necessary to handle.
To sum everything up, IMO the best in this case would be to go with simple ReactDOM.render in Vue component and avoid React SSR rendering:
<script crossorigin src="https://unpkg.com/react#16/umd/react.production.min.js"></script>
<script src="my-compiled-react-bundle.js"></script>
<script>
function init() {
ReactDOM.render(ReactApp, document.getElementById('#react-page'))
}
init();
</script>
I have a really interesting problem at hand.
I am currently working on a Admin-Interface for a headless CMS and I want to dynamically load the components for viewing the content of different parts.
I know that it is rather easy to accomplish wit React Router since v4, but I don't want to rely on Routes to handle this to keep the URL as clean as possible.
Here is what I am trying to do:
I want to render the basic layout of the UI (in which I load the
different sub-components when clicking the navigation-links)
Inside the async componentWillMount() function of this basic layout I want to import() said components using await import()
Ideally those should be stored to the state inside a property called this.state.mainComponents
I will then pass down the names of the components to the navigation component along with a function which will change the currently displayed component in the parents state
I tried several different approaches, but did not manage to get it to work until now.
This is what I have now:
async componentDidMount() {
const mainComponents = {
Dashboard: await import('../_mainComponents/Dashboard').then(
({ Dashboard }) => {
return Dashboard;
},
),
};
const componentNames = [];
for (let p in mainComponents) {
componentNames.push(p);
}
this.setState({
mainComponents: {
views: mainComponents,
names: componentNames,
},
currentComponent: mainComponents.Dashboard,
currentTitle: 'Dashboard',
});
}
I would really appreciate any help you could give me on this.
EDIT:
So according to the answer of #Mosè Raguzzini I implemented react-loadable again and tried to store the const inside my state like this:
const LoadableDashboard = Loadable({
loader: () => import('../_mainComponents/Dashboard'),
loading: Loading
});
const mainComponents = {
Dashboard: LoadableDashboard
};
And inside my constructor function I am trying to pass this Object into the state like this:
const componentNames = [];
for (let p in mainComponents) {
componentNames.push(p);
}
this.state = {
mainComponents: {
views: mainComponents,
names: componentNames,
},
currentComponent: null,
currentTitle: null,
loading: false,
};
It is not working when trying to output the Components stored inside the this.state.mainComponents.views object inside to the render function.
When using <Dashboard /> it is working as expected, but this is not what I am trying to accomplish here since I don't want to add every component to the render function in a massive switch ... case.
Does anybody of you have an idea how I can accomplish this?
I found a solution to my problem and wanted to share it with all of those who will come after me searching for an appropiate answer.
Here is what I did:
import Loadable from 'react-loadable';
import Loading from '../_helperComponents/Loading';
// Define the async-loadable components here and store them inside of the mainComponent object
const LoadableDashboard = Loadable({
loader: () => import('../_mainComponents/Dashboard'),
loading: Loading,
});
const LoadableUserOverview = Loadable({
loader: () => import('../_mainComponents/UserOverview'),
loading: Loading,
});
const mainComponents = {
Dashboard: <LoadableDashboard />,
User: <LoadableUserOverview />,
};
I do not store the components inside my state (it would have been possible but goes against the guidelines as I've found out by researching state even further) :D
With this object I can simply call the corresponding component by outputting it in the render() function like this:
render() {
return (
<div>
{mainComponents[this.state.currentComponent]}
</div>
);
}
I hope I could help someone with this.
Happy coding!
I suggest to take a look to react-loadable:
import Loadable from 'react-loadable';
import Loading from './my-loading-component';
const LoadableComponent = Loadable({
loader: () => import('./my-component'),
loading: Loading,
});
export default class App extends React.Component {
render() {
return <LoadableComponent/>;
}
}
It provides the feature you need in a clean style.