Dynamic Imports: Am I missing something? - javascript

I have a React project that uses Webpack as a bundler, and I'm splitting my bundle into two chunks -- the main codebase main.js, and the vendor bundle vendor.js.
After building these bundles, main.js ends up being 45kb and vendor.js is 651kb.
One specific vendor library is 225kb and seems to be the worst offendor in the vendor imports.
I am importing this library in a page component at the top of the file:
import React from 'react';
import { ModuleA, ModuleB } from 'heavyPackage'; // 225kb import
...
const Page = ({ setThing }) => {
...
};
To try and have this heavy import loaded in a separate bundle, I tried to instead import these modules using a dynamic import.
Inside the Page component, the modules weren't actually used until a particular function was called, so I tried to import the modules within that scope rather than at the top of the file:
import React from 'react';
...
const Page = ({ setThing }) => {
...
const handleSignIn = async () => {
const scopedPackage = await import('heavyPackage');
const { moduleA, moduleB } = scopedPackage;
// use moduleA & moduleB normally here
};
};
For some reason I figured Webpack would intelligently pick up on what I'm trying to do here and separate this heavy package into its own chunk that is downloaded only when needed, but the resulting bundles were the same -- a main.js that was 45kb and a vendor.js that was 651kb. Is my line of thinking here correct and possibly my Webpack configuration is off, or am I thinking of dynamic imports in the wrong way?
edit I have Webpack configured to split the bundle using splitChunks. Here is how I have this configured:
optimization: {
chunkIds: "named",
splitChunks: {
cacheGroups: {
commons: {
chunks: "initial",
maxInitialRequests: 5,
minChunks: 2,
minSize: 0,
},
vendor: {
chunks: "initial",
enforce: true,
name: "vendor",
priority: 10,
test: /node_modules/,
},
},
},
},

Update for React 18: The code below is no longer required to split chunks/dynamically load components. Instead, you can use React.lazy with Suspense, which achieves similar results (this only works for React components, therefore any node_module imports would need to be imported within this dynamically loaded component):
const ProfilePage = React.lazy(() => import('./ProfilePage')); // Lazy-loaded
<Suspense fallback={<Spinner />}>
<ProfilePage />
</Suspense>
#Ernesto's answer offers one way of code splitting by using react-loadable with the babel-dynamic-import plugin, however, if your Webpack version is v4+ (and has a custom Webpack config set to SplitChunks by all), then you'll only need to use magic comments and a custom React component.
From the docs:
By adding [magic] comments to the import, we can do things such as name our chunk or select different modes. For a full list of these magic comments see the code below followed by an explanation of what these comments do.
// Single target
import(
/* webpackChunkName: "my-chunk-name" */
/* webpackMode: "lazy" */
'module'
);
// Multiple possible targets
import(
/* webpackInclude: /\.json$/ */
/* webpackExclude: /\.noimport\.json$/ */
/* webpackChunkName: "my-chunk-name" */
/* webpackMode: "lazy" */
/* webpackPrefetch: true */
/* webpackPreload: true */
`./locale/${language}`
);
Therefore, you can create a reusable LazyLoad component like so:
import React, { Component } from "react";
import PropTypes from "prop-types";
class LazyLoad extends Component {
state = {
Component: null,
err: "",
};
componentDidMount = () => this.importFile();
componentWillUnmount = () => (this.cancelImport = true);
cancelImport = false;
importFile = async () => {
try {
const { default: file } = await import(
/* webpackChunkName: "[request]" */
/* webpackMode: "lazy" */
`pages/${this.props.file}/index.js`
);
if (!this.cancelImport) this.setState({ Component: file });
} catch (err) {
if (!this.cancelImport) this.setState({ err: err.toString() });
console.error(err.toString());
}
};
render = () => {
const { Component, err } = this.state;
return Component ? (
<Component {...this.props} />
) : err ? (
<p style={{ color: "red" }}>{err}</p>
) : null;
};
}
LazyLoad.propTypes = {
file: PropTypes.string.isRequired,
};
export default file => props => <LazyLoad {...props} file={file} />;
Then in your routes, use LazyLoad and pass it the name of a file in your pages directory (eg pages/"Home"/index.js):
import React from "react";
import { Route, Switch } from "react-router-dom";
import LazyLoad from "../components/LazyLoad";
const Routes = () => (
<Switch>
<Route exact path="/" component={LazyLoad("Home")} />
<Route component={LazyLoad("NotFound")} />
</Switch>
);
export default Routes;
On that note, React.Lazy and React-Loadable are alternatives to having a custom Webpack config or Webpack versions that don't support dynamic imports.
A working demo can be found here. Follow installation instructions, then you can run yarn build to see routes being split by their name.

Oki then, look! you have yow webpack config with the splitChunks property, also you need to add a chunkFilename property in side of the output object from webpack.
If we take for example the one generated by CRA
// The build folder.
path: isEnvProduction ? paths.appBuild : undefined,
// Add /* filename */ comments to generated require()s in the output.
pathinfo: isEnvDevelopment,
// There will be one main bundle, and one file per asynchronous chunk.
// In development, it does not produce real files.
filename: isEnvProduction
? 'static/js/[name].[contenthash:8].js'
: isEnvDevelopment && 'static/js/bundle.js',
// TODO: remove this when upgrading to webpack 5
futureEmitAssets: true,
// THIS IS THE ONE I TALK ABOUT
chunkFilename: isEnvProduction
? 'static/js/[name].[contenthash:8].chunk.js'
: isEnvDevelopment && 'static/js/[name].chunk.js',
// webpack uses `publicPath` to determine where the app is being served from.
// It requires a trailing slash, or the file assets will get an incorrect path.
// We inferred the "public path" (such as / or /my-project) from homepage.
publicPath: paths.publicUrlOrPath,
// Point sourcemap entries to original disk location (format as URL on Windows)
devtoolModuleFilenameTemplate: isEnvProduction
? info =>
path
.relative(paths.appSrc, info.absoluteResourcePath)
.replace(/\\/g, '/')
: isEnvDevelopment &&
(info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')),
// Prevents conflicts when multiple webpack runtimes (from different apps)
// are used on the same page.
jsonpFunction: `webpackJsonp${appPackageJson.name}`,
// this defaults to 'window', but by setting it to 'this' then
// module chunks which are built will work in web workers as well.
globalObject: 'this',
},
Once you have that on yow webpack. next thing is to install a npm i -D #babel/plugin-syntax-dynamic-import and add it to your babel.config.js
module.exports = api =>
...
return {
presets: [
.....
],
plugins: [
....
"#babel/plugin-syntax-dynamic-import",
....
]
}
then last thing npm install react-loadable
create a folder called: containers. in it place all the containers
inside index.js do some like:
The loadable object have two properties
export const List = Loadable({
loader: () => import(/* webpackChunkName: "lists" */ "./list-constainer"),
loading: Loading,
});
loader: component to dynamically import
loadinh: component to display until the dynamic component is loaded.
and for last on you Router set each loadable to a route.
...
import { Lists, List, User } from "../../containers";
...
export function App (): React.ReactElement {
return (
<Layout>
<BrowserRouter>
<SideNav>
<nav>SideNav</nav>
</SideNav>
<Main>
<Header>
<div>Header</div>
<div>son 2</div>
</Header>
<Switch>
<Route exact path={ROUTE_LISTS} component={Lists} />
<Route path={ROUTE_LISTS_ID_USERS} component={List} />
<Route path={ROUTE_LISTS_ID_USERS_ID} component={User} />
<Redirect from="*" to={ROUTE_LISTS} />
</Switch>
</Main>
</BrowserRouter>
</Layout>
);
}
so then when you bundle yow code we get some like:

Related

loading css in asynchronously imported javascript using webpack with MiniCssExtractPlugin

i'm working on a webapp that loads it's different pages asynchronously via dynamic imports e.g.:
import {Component, h} from "preact";
import AsyncRoute from "preact-async-route";
import {Layout} from "components/layout";
import {PageLoading} from "components/page-loading";
export class Page extends Component {
render() {
return (
<Layout>
<AsyncRoute
key="doorstation"
path={"/doorstation"}
getComponent={this.getSamplePage}
loading={() => <PageLoading />}
/>
</Layout>
);
}
async getSamplePage(){
const { init, Page } = await import("modules/sample");
init();
return Page;
}
}
and in the actual imported file i do
// modules/sample/page/index.tsx
import {Component, h} from "preact";
import styles from "./styles.css";
export class Page extends Component {... components logic}
// modules/sample/index.tsx
export const init = () => // some initialization logic
export { Page } from "./Page"
Each of those pages has their own css that gets imported on their respective file. What i'm stuck now with is that it puts the resulting css in it's own chunk but when the browser build tries to import said css it just fails with an error like:
GET http://<my-ip>/341.()=%3E%225028a51a47a787d4cc85%22.css net::ERR_ABORTED 404 (Not Found)
Error: Loading CSS chunk 341 failed.
(/341.()=>"5028a51a47a787d4cc85".css)
at o.<computed>.o.<computed>.<computed>.e.push.o.<computed>.d.onerror
on inspection of the import on the console the href of resulting stylesheet tag is actually set to:
link.href = "http://<my-ip>/341.()=%3E%225028a51a47a787d4cc85%22.css"
what i expected was that webpack would resolve those paths on my dynamically imported modules but apparently this isn't the case.
for now my config looks like this:
module: {
rules: [
test: /\.css$/,
use: [
{
loader: args.mode === "production" ? MiniCssExtractPlugin.loader : "style-loader",
},
{
loader: "css-loader",
options: {
sourceMap: args.mode !== "production",
importLoaders: 1,
modules: {
localIdentName:
args.mode === "production"
? "c-[hash:base64:8]"
: "[name]-[local]-[hash:base64:4]",
},
},
},
{
loader: "postcss-loader",
},
]
},
plugins: [
new MiniCssExtractPlugin({
filename: "[name].[hash:8].css",
chunkFilename: "[name].[hash:8].css",
}),
]
by just using the style-loader everything works just fine but this won't give me actual css chunks. I'm not entirely sure where or what to change so webpack will require the actual css chunk files instead of resolving the path with that... what looks like an function expression. I was already looking up the issue but so far it didn't really solve my issue since it usually revolved around undefined values or something.
Did i miss something in this config? is someone able to help?
Thank you in advance.

SCSS styles imported to components in app.js are being applied to all components in app.js

I am using react-router and am importing styles and components into my app.js. The issue is that any styles that applied in say Login then are somehow also applied to Home, Item, and Bag. Looking at source in chrome it shows all of the css files being loaded whenever I go to any of the pages. I am using webpack and I think this might be the issue but I have no idea what in it is causing this. Any help would be greatly appreciated!
import '../../styles/styles.scss';
import '../../styles/generalStyle.scss';
import 'bootstrap/dist/js/bootstrap';
import {
Route,
Switch,
} from 'react-router-dom';
import NotFound from './NotFound';
import Login from '../Login/Login';
import Home from '../Home/Home';
import Item from '../Item/Item';
import Bag from '../Bag/Bag';
export default function App() {
return (
<>
<main>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/login" component={Login} />
<Route path="/item/:name" component={Item} />
<Route path="/bag" component={Bag} />
<Route component={NotFound} />
</Switch>
</main>
</>
);
}```
In ReactJS the css is not component scope. The css is global scope. so if you add a css file in any component then it will apply to whole app.
If you want css to be component scope then you could use 'CSS Modules'
This project supports CSS Modules alongside regular stylesheets using the [name].module.css file naming convention. CSS Modules allows the scoping of CSS by automatically creating a unique classname of the format [filename]_[classname]__[hash].
you could read more about this here
You need css modules to scope the styles for a particular component. Because css styles are global in nature.
Quick question:
Are you using mini-css-extract-plugin in your webpack config? If so when you build the application via webpack, all the styles in js (imported scss files) are pulled in to one single file and injected into index.html page.
First you need to convert your scss files to css modules and do the below you get what you need.
This link should help you in converting the scss files to css modules.
Example:
{
test: /\.css$/i,
loader: 'css-loader',
options: {
modules: true, // this one make existing scss to css modules
},
},
Lets assume you have convert scss to css modules. Lets see an example.
Eg: page1.scss, page2.scss to styles.css
I would suggest to split styles according to your need. Lets see an example for your above code.
Eg: vendor.css (contains bootstrap and general styles) and main.css (contains component styles)
Example: webpack.config.js
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
function recursiveIssuer(m) {
if (m.issuer) {
return recursiveIssuer(m.issuer);
} else if (m.name) {
return m.name;
} else {
return false;
}
}
module.exports = {
entry: {
vendor: path.resolve(__dirname, 'src/vendor'),
main: path.resolve(__dirname, 'src/main'),
},
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
name: 'foo',
test: (m, c, entry = 'vendor') =>
m.constructor.name === 'CssModule' && recursiveIssuer(m) === entry,
chunks: 'all',
enforce: true,
},
main: {
name: 'bar',
test: (m, c, entry = 'main') =>
m.constructor.name === 'CssModule' && recursiveIssuer(m) === entry,
chunks: 'all',
enforce: true,
},
},
},
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css',
}),
],
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
],
},
};
Above file is an example taken directly from mini-css-extract-plugin plugin page.
Explanation:
Above config will create two files, one for vendor.css and one for main.css. Like this you can create n number of styles for your routes and use it accordingly.
Hope this help!

Unexpected undefined while import with Webpack

I have a problem that has never happened to me before: I'm compiling a little basic starter browser web app (with React) using Webpack + Babel 7.
I've got three different file:
withAuth.js The Auth High Order Component
NavBar.js The NavBar Component
Login.js The Login Form
If I import the withAuth HOC in the NavBar is everything alright, but if I import the withAuth component in the Login.js file it return undefined
/** withAuth.js */
console.log('withAuth Loaded');
const withAuth = Child => ChildProps => (
<AuthContext.Consumer>
{ authClient => <Child {...ChildProps} authClient={authClient} }
</AuthContext.Consumer>
)
export { withAuth };
/** NavBar.js */
import { withAuth } from 'HOC/Auth';
console.log('NavBar Loaded', withAuth); // <- My HOC
const NavBarComponent = (authClient) => { /* ... My Code ... */ }
const NavBar = withAuth(NavBarComponent);
export default NavBar;
/** Login.js */
import { withAuth } from 'HOC/Auth';
console.log('Login Loaded', withAuth); // <- undefined ??
const LoginFormComponent = (authClient) => { /* ... My Code ... */ }
const LoginForm = withAuth(LoginFormComponent);
// /|\
// |
// Will produce an Error, withAuth is Undefined
This is my Webpack Configuration:
/** webpack.config.js */
module.exports = {
entry: { core: 'index.js' },
resolve: {
alias: {
HOC: './path/to/hoc/folder'
}
},
optimization: {
runtimeChunk: 'single',
splitChunks: {
chunks: 'all'
}
},
plugins: [ /* Various Plugin */ ],
module: {
rules: [ /* My Rules */ ]
}
}
Any one know why my HOC is undefined?
Edit:
I've placed Console Log in the tree file. The result are:
'Login Loaded' - undefined
'withAuth Loaded'
'NavBar Loaded' - function() { }
Edit 2:
This is the files structure:
app/
|-high-order-component/
| |-auth/
| |-withAuth.js
|
|-layout-component/
| |-navbar/
| |-index.js
|
|-pages/
|-auth/
|-login.js
Resolved
After much testing and research throughout the afternoon I came to the solution of the problem. As I said in the question, mine is a larger project and I only partially wrote its structure because I thought the problem was located in those three files.
In reality, the problem was a Circular Dependency problem and not a Webpack configuration problem.
In my project I have a module called 'Route' that store all Path and all Component for Path, so I can build the React Router using Array Map function. That module has a function that allow me to Route through path and that can return me a path string to a Component.
My problem was due to the fact that this module is often called in the project and this has created a Circular Dependency.
Webpack doesn't show the Circular Dependency during compiling, but I found useful adding a plugin, called CircualDependencyPlugin. This plugin will break Webpack compiling when a Circual Dependency will be found.
Splitting the Route module into two files solved my problem.

How to use Webpack to load a static file with a relative path with React?

I'm trying to create a map component in React using the Tangram Library.
I got it working with Webpack alone but it started bugging out when I used react in the mix.
I've tried using various loaders such as a raw loader, a yaml loader and so forth, but none of them have worked thus far.
The map component looks as follows:
// -- Import dependencies --
import React from 'react';
import { Map } from 'react-leaflet';
// -- Import Tangram --
import Tangram from 'tangram';
import yaml from 'js-yaml';
import data from '!!raw-loader!./scene.yaml';
export default class LeafletMap extends React.Component {
componentDidMount() {
const layer = Tangram.leafletLayer({
scene: data
});
layer.addTo(this.map.leafletElement);
}
render() {
return (
<Map center={[40.70532, -74.00976]} zoom={15} ref={(ref) => { this.map = ref }} />
);
}
}
How can I actually load the scene.yaml so that the Tangram library makes use of it ?
In the end it responds with a 404 as the file isn't found.
The solution was, that the static files weren't being copied to the bundle built by webpack.
I solved it by using the CopyPlugin in the webpack config and copying the files to a folder respecting the relative path name, like so:
const CopyPlugin = require('copy-webpack-plugin');
module.exports = {
entry: './main.js',
output: {
filename: './bundle.js'
},
plugins: [
new CopyPlugin([
{ from: 'src', to: 'src' },
]),
],
};

How to remove imported css in reactjs

I have used the following code to import css
componentWillMount() {
import('./patient-summary.css');
}
How to remove imported css from react when component is not in use. When i go back to previous screen this css gets applied there. Any idea ?
UPDATE:: Webpack config
const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'public/dist')
},
module: {
rules: [
{
test: /\.js?$/,
loader: 'babel-loader',
exclude: /node_modules/
},
{
test: /\.css$/,
use: [ 'style-loader', 'css-loader' ]
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
loader: "file-loader"
}
,
{
test: /\.(png|jpeg|jpg|gif|svg)$/,
loader: "file-loader"
}
]
},
devServer: {
contentBase: path.resolve(__dirname, "public"),
historyApiFallback: true,
port: 3000,
watchOptions: {
// Delay the rebuild after the first change
aggregateTimeout: 300,
// Poll using interval (in ms, accepts boolean too)
poll: 1000,
},
},
plugins: [
// Ignore node_modules so CPU usage with poll
// watching drops significantly.
new webpack.WatchIgnorePlugin([
path.join(__dirname, "node_modules")
])
],
};
I found a (sort of) reasonable way to do this in React. In short, you can lazy-load React components that contain the import './style.css', and when it loads, you can capture the imported StyleSheet to toggle its StyleSheet.disabled property later.
Here's the main code, with more explanation below. Here's my Gist.
useDisableImportedStyles.tsx
import { useEffect } from 'react'
// global list of all the StyleSheets that are touched in useDisableImportedStyles
const switchableGlobalStyleSheets: StyleSheet[] = []
// just to clarify what createUseDisableImportedStyles() returns
type useDisableImportedStyles = () => void
export const createUseDisableImportedStyles = (
immediatelyUnloadStyle: boolean = true
// if true: immediately unloads the StyleSheet when the component is unmounted
// if false: waits to unloads the StyleSheet until another instance of useDisableImportedStyles is called.This avoids a flash of unstyled content
): useDisableImportedStyles => {
let localStyleSheet: StyleSheet
return () => {
useEffect(() => {
// if there are no stylesheets, you did something wrong...
if (document.styleSheets.length < 1) return
// set the localStyleSheet if this is the first time this instance of this useEffect is called
if (localStyleSheet == null) {
localStyleSheet = document.styleSheets[document.styleSheets.length - 1]
switchableGlobalStyleSheets.push(localStyleSheet)
}
// if we are switching StyleSheets, disable all switchableGlobalStyleSheets
if (!immediatelyUnloadStyle) {
switchableGlobalStyleSheets.forEach(styleSheet => styleSheet.disabled = true)
}
// enable our StyleSheet!
localStyleSheet.disabled = false
// if we are NOT switching StyleSheets, disable this StyleSheet when the component is unmounted
if (immediatelyUnloadStyle) return () => {
if (localStyleSheet != null) localStyleSheet.disabled = true
}
})
}
}
WARNING: This is pretty finicky. You must set this up exactly or there may be unintended consequences
Conditions:
createUseDisableImportedStyles must called in global scope in the same tsx file as the imported css being targeted and the component to be lazy loaded
import React from 'react'
import { createUseDisableImportedStyles } from './useDisableImportedStyles'
import './global-styles.css'
const useDisableImportedStyles = createUseDisableImportedStyles()
export const CssComponent: React.FC<{}> = () => {
useDisableImportedStyles()
return null
}
export default CssComponent
A component using this hook should be lazy loaded:
LazyCssComponent = React.lazy(() => import('./cssComponent'))
...
<React.Suspense fallback={<></>}>
{condition && <LazyCssComponent/>}
</React.Suspense>
An exception to lazy loading might be using this in a single, normal, non-lazy component so styles are loaded on first render
NOTE: the InitialCssComponent never needs to actually render, it just needs to be imported
BUT: this will only work if there is one single .css file imported globally, otherwise, I don't know what would happen
import InitialCssComponent from './initialCssComponent'
LazyCssComponent = React.lazy(() => import('./cssComponent'))
//...
{false && <InitialCssComponent/>}
<React.Suspense fallback={<></>}>
{condition && <LazyCssComponent/>}
</React.Suspense>
GOOD LUCK!
First of all, AFAIK, you should not call any imports in componentWillMount. This means that every time a new component about to mount, this css will be loaded over and over. Instead, it must be placed at the beginning of your module.
The way that you avoid unnecessary css imports is to avoid unnecessary component imports. Hence, if your component is not called anywhere, then this css will not be loaded.
For routing, I think you will need to do some code splitting, but I am not sure if it is straightforward or the right way to do.
Link 1
Link 2

Categories

Resources