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.
Related
I have a library that I import many components from and I would like them registered as async components at once without having to type it for each of them manually and I can't import them all as the library exports not just components.
My solution was to put the names of modules in an array
const imports = [
'CompA',
'CompB',
'CompC',
'CompD',
];
And then I tried to register individually using for each and dynamic imports.
imports.forEach((subComponent) => {
const component = defineAsyncComponent(() =>
import('#myLibrary/library').then((comp) => ({
default: comp[subComponent],
}))
);
app.component(subComponent, component);
});
This does not result in an error but with a warning in async wrapper
Component is missing template or render function.
at <Anonymous key=10 >
at <AsyncComponentWrapper key=10 >
I suspect an issue with the dynamic import but unsure how to proceed
When I do it individually like this it works:
const Component = defineAsyncComponent(() =>
import('#myLibrary/library').then(
(comp) => comp['component']
)
);
app.component('Component', Component);
But it throws a type error when doing it in the forEach
This is very bizarre. During my attempt at code-splitting, I encountered this:
const g = "bi";
const importStr = `react-icons/${g.toLowerCase()}`;
console.log(importStr);
console.log(importStr === `react-icons/bi`);
import(importStr).then(module => {
console.log(module);
});
import(`react-icons/bi`).then(module => {
console.log(module);
});
In the above code, if I import "importStr", then the import throws an error:
Uncaught (in promise) Error: Cannot find module 'react-icons/bi'
But if I directly import "react-icons/bi", then there is no issue. As you see,
importStr === `react-icons/bi`
Why and how do I fix this? I can't actually directly use "react-icons/bi" because g is dynamic and could take other values.
I quote from the following comment
Webpack performs a static analyse at build time. It doesn't try to infer variables which that import(test) could be anything, hence the failure. It is also the case for import(path+"a.js").
Because of the tree shaking feature webpack tries to remove all unused code from the bundle. That also means something similar could work
import("react-icons/" + g)
Edit: as per your suggestion I updating this to
import("react-icons/" + g.toLowerCase()+ '/index.js')
An easy way to implement code splitting in React is with lazy loading, as seen here (this might be easier than importing a dynamic string):
const OtherComponent = React.lazy(() => import('./OtherComponent'));
This implementation will load the bundle with OtherComponent only when it is first rendered.
Example from reactjs.org:
import React, { Suspense } from 'react';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
const AnotherComponent = React.lazy(() => import('./AnotherComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<section>
<OtherComponent />
<AnotherComponent />
</section>
</Suspense>
</div>
);
}
More info on React.lazy
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()
})
Here's my lazy component:
const LazyBones = React.lazy(() => import('#graveyard/Bones')
.then(module => ({default: module.BonesComponent}))
export default LazyBones
I'm importing it like this:
import Bones from './LazyBones'
export default () => (
<Suspense fallback={<p>Loading bones</p>}>
<Bones />
</Suspense>
)
And in my test I have this kind of thing:
import * as LazyBones from './LazyBones';
describe('<BoneYard />', function() {
let Bones;
let wrapper;
beforeEach(function() {
Bones = sinon.stub(LazyBones, 'default');
Bones.returns(() => (<div />));
wrapper = shallow(<BoneYard />);
});
afterEach(function() {
Bones.restore();
});
it('renders bones', function() {
console.log(wrapper)
expect(wrapper.exists(Bones)).to.equal(true);
})
})
What I expect is for the test to pass, and the console.log to print out:
<Suspense fallback={{...}}>
<Bones />
</Suspense>
But instead of <Bones /> I get <lazy /> and it fails the test.
How can I mock out the imported Lazy React component, so that my simplistic test passes?
I'm not sure this is the answer you're looking for, but it sounds like part of the problem is shallow. According to this thread, shallow won't work with React.lazy.
However, mount also doesn't work when trying to stub a lazy component - if you debug the DOM output (with console.log(wrapper.debug())) you can see that Bones is in the DOM, but it's the real (non-stubbed-out) version.
The good news: if you're only trying to check that Bones exists, you don't have to mock out the component at all! This test passes:
import { Bones } from "./Bones";
import BoneYard from "./app";
describe("<BoneYard />", function() {
it("renders bones", function() {
const wrapper = mount(<BoneYard />);
console.log(wrapper.debug());
expect(wrapper.exists(Bones)).to.equal(true);
wrapper.unmount();
});
});
If you do need to mock the component for a different reason, jest will let you do that, but it sounds like you're trying to avoid jest. This thread discusses some other options in the context of jest (e.g.
mocking Suspense and lazy) which may also work with sinon.
You don't need to resolve lazy() function by using .then(x => x.default) React already does that for you.
React.lazy takes a function that must call a dynamic import(). This must return a Promise which resolves to a module with a default export containing a React component. React code splitting
Syntax should look something like:
const LazyBones = React.lazy(() => import("./LazyBones"))
Example:
// LazyComponent.js
import React from 'react'
export default () => (
<div>
<h1>I'm Lazy</h1>
<p>This component is Lazy</p>
</div>
)
// App.js
import React, { lazy, Suspense } from 'react'
// This will import && resolve LazyComponent.js that located in same path
const LazyComponent = lazy(() => import('./LazyComponent'))
// The lazy component should be rendered inside a Suspense component
function App() {
return (
<div className="App">
<Suspense fallback={<p>Loading...</p>}>
<LazyComponent />
</Suspense>
</div>
)
}
As for Testing, you can follow the React testing example that shipped by default within create-react-app and change it a little bit.
Create a new file called LazyComponent.test.js and add:
// LazyComponent.test.js
import React, { lazy, Suspense } from 'react'
import { render, screen } from '#testing-library/react'
const LazyComponent = lazy(() => import('./LazyComponent'))
test('renders lazy component', async () => {
// Will render the lazy component
render(
<Suspense fallback={<p>Loading...</p>}>
<LazyComponent />
</Suspense>
)
// Match text inside it
const textToMatch = await screen.findByText(/I'm Lazy/i)
expect(textToMatch).toBeInTheDocument()
})
Live Example: Click on the Tests Tab just next to Browser tab. if it doesn't work, just reload the page.
You can find more react-testing-library complex examples at their Docs website.
I needed to test my lazy component using Enzyme. Following approach worked for me to test on component loading completion:
const myComponent = React.lazy(() =>
import('#material-ui/icons')
.then(module => ({
default: module.KeyboardArrowRight
})
)
);
Test Code ->
//mock actual component inside suspense
jest.mock("#material-ui/icons", () => {
return {
KeyboardArrowRight: () => "KeyboardArrowRight",
}
});
const lazyComponent = mount(<Suspense fallback={<div>Loading...</div>}>
{<myComponent>}
</Suspense>);
const componentToTestLoaded = await componentToTest.type._result; // to get actual component in suspense
expect(componentToTestLoaded.text())`.toEqual("KeyboardArrowRight");
This is hacky but working well for Enzyme library.
To mock you lazy component first think is to transform the test to asynchronous and wait till component exist like:
import CustomComponent, { Bones } from './Components';
it('renders bones', async () => {
const wrapper = mount(<Suspense fallback={<p>Loading...</p>}>
<CustomComponent />
</Suspense>
await Bones;
expect(wrapper.exists(Bones)).toBeTruthy();
}
I'm having trouble testing my React components that use react-loadable. Say, I have a Button component that, depending on whether it receives an icon prop, loads an Icon component like so:
Button.js
const LoadableIcon = Loadable({
loader: () => import('./Icon'),
loading: () => <div>Loading...</div>
})
function Button(props) {
return (
<button
onClick={props.onClick}>
{props.icon &&
<LoadableIcon name={props.icon} />}
{props.children}
</button>
)
}
When I test this component, however, the Icon had not loaded yet, and instead the test only finds the <div>Loading...</div> element...
Button.test.js
import React from 'react'
import {render} from 'react-testing-library'
import Button from '../Button'
describe('Button', () => {
it('renders icon correctly', () => {
const {getByText} = render(
<Button
icon='add'
/>
)
expect(getByText('add')).toBeInTheDocument()
})
})
Is there an elegant way to handle this situation without using actual setTimeouts?
So, the answer is to read the docs - note to self! The solution based on docs was the following:
describe('Button', () => {
it('renders icon correctly', async () => {
const {getByText} = render(
<Button
icon='add'
/>
)
const icon = await waitForElement(() => getByText('add'))
expect(icon).toBeInTheDocument()
})
})
Also, note that async needs to be used together with await.
I don't have personal experience using react-loadable, but I have implemented a similar component that handles code splitting via the dynamic import() syntax.
To get Jest to work with 'loadable' / 'async' components, I had to configure my .babel-rc config for Jest to include the dynamic-import-node babel plugin that way the modules can be properly resolved even when the import is async.