I'm using react-loadable v4.0.4 and webpack v3.5.1.
Here is my code,
import Dashboard from '../../scenes/dashboard/dashboard';
import ReactLoadable from 'react-loadable';
...
const yoPath = 'src/components/scenes/dashboard/dashboard';
const DashboardWrap = ReactLoadable({
loading: Dashboard,
loader: () => {
return new Promise((resolve, reject) =>
require.ensure(
[],
(require) => resolve(require(yoPath)),
(error) => reject(error),
'dashboardChunk'
)
)
}
});
And using react-router-dom v4.1.2, I've set Route as follows,
<Switch>
...
<Route exact path='/dashboard' component={DashboardWrap} />
...
</Switch>
I'm able to build the chunks for the respective component with the name dashboardChunk.
But while loading that component I'm getting the issues as follows.
In the console,
And the chunkfile,
Please let me know if I'm doing anything wrong.
I basically wanted to do code splitting, for that I've just done the following and it works fine.
I've created a common component(wrapper component) as follows,
import React, { Component } from 'react';
class Async extends Component {
componentWillMount = () => {
this.props.load.then((Component) => {
this.Component = Component
this.forceUpdate();
});
}
render = () => (
this.Component ? <this.Component.default /> : null
)
}
export default Async;
Then I've used above component as follows,
export const AniDemo = () => <Async load={import(/* webpackChunkName: "aniDemoChunk" */ "../../scenes/ani-demo/AniDemo.js")} />
export const Dashboard = () => <Async load={import(/* webpackChunkName: "dashboardChunk" */ "../../scenes/dashboard/Dashboard.js")} />
And using the above, I've made the changes in route as follows,
<Route exact path="/ani-demo" component={AniDemo} />
<Route exact path="/dashboard" component={Dashboard} />
With the help of the above changes that I made, I'm able to create chunks properly with the names that I've mentioned in the comments inside import statements ie aniDemoChunk.js and dashboardChunk.js respectively.
And these chunks load only when the respective component is called ie aniDemoChunk.js is loaded on browser only when AniDemo component is called or requested. Similarly for the Dashboard component respectively.
Note: If anyone is getting error re:Unexpected token import. So to support import() syntax just replace import to System.import() or else use babel-plugin-syntax-dynamic-import.
Webpack must be able to determine the imported path during static analysis. If you pass an argument into require, this is not possible.
It is best to put the actual path into require.ensure, i.e.
require.ensure(
['src/components/scenes/dashboard/dashboard']
require =>
resolve(require('src/components/scenes/dashboard/dashboard').default)
error => reject(error),
'dashboardChunk'
)
or use the newer dynamic import syntax. With the newer syntax you could simplify the above into:
Loadable({
loader: () => import(/* webpackChunkName: "dashboardChunk" */ 'src/components/scenes/dashboard/dashboard')
loading: MyLoader
})
Also, the loading argument should be a component to display while your asynchronous load is taking place, e.g. some kind of loading animation.
Related
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
I'm using Gatsby and I would like to create a one site using multilanguage, so far I've defined pages/index.js which contains this:
import React from "react"
import Layout from "../components/layout/layout"
import BGTState from "../context/bgt/bgtState"
import { Router } from "#reach/router"
import Home from "../components/pages/home"
import Collection from "../components/pages/collection"
import NotFound from "../components/pages/404"
const IndexPage = () => {
return (
<BGTState>
<Layout>
<Router>
<Home path="/" />
<Collection path="collection/:id" />
<NotFound default />
</Router>
</Layout>
</BGTState>
)
}
export default IndexPage
and I have modified gatsby-node.js as:
// Implement the Gatsby API onCreatePage. This is
// called after every page is created.
exports.onCreatePage = async ({ page, actions }) => {
const { createPage } = actions
if (page.path === "/") {
page.matchPath = "/*"
createPage(page)
}
}
each request is forwarded on index.js, but there is a problem. I'm using the plugin gatsby-plugin-intl that add to the url a dynamic prefix like: http://localhost:3001/en/
If I visit http://localhost:3001/en/, then I get the NotFound component displayed because no Route match the url. Is there a way to prefix the url and reroute everything to the correct component?
Why you are using client-only routes/wrapping everything inside the <Router>?
I don't know what's the goal in your scenario to change the gatsby-node.js with:
// Implement the Gatsby API onCreatePage. This is
// called after every page is created.
exports.onCreatePage = async ({ page, actions }) => {
const { createPage } = actions
if (page.path === "/") {
page.matchPath = "/*"
createPage(page)
}
}
If you are not using client-only routes, you can remove them.
It's a broad question but, just define your languages and translation files. In your gatsby-config.js:
plugins: [
{
resolve: `gatsby-plugin-intl`,
options: {
// language JSON resource path
path: `${__dirname}/src/intl`,
// supported language
languages: [`en`,`es`],
// language file path
defaultLanguage: `en`,
// option to redirect to `/en` when connecting `/`
redirect: true,
},
},
]
The useIntl hook will capture the internal requests so, you just need to worry about the views, forgetting the routing:
import React from "react"
import { useIntl, Link, FormattedMessage } from "gatsby-plugin-intl"
const IndexPage = () => {
const intl = useIntl()
return (
<Layout>
<SEO title={intl.formatMessage({ id: "title" })}/>
<Link to="/page-2/">
<FormattedMessage id="go_page2" />
</Link>
</Layout>
)
}
export default IndexPage
Your Collection component should be a page, wrapped inside /page folder, or a custom collection with a specific id. If that page is dynamically created, you should manage the customizations in your gatsby-node.js, and, in that case, it should be a template of collections in that scenario.
To link between pages, I would recommend using page-queries to get the needed data to create your components. Your page should look like:
const IndexPage = () => {
return (
<BGTState>
<Layout>
<Link to="/"> // home path
<Link to="collection/1">
</Layout>
</BGTState>
)
}
export default IndexPage
The 404-page will automatically be handled by Gatsby, redirecting all wrong requests (in development will show a list of pages). Your other routing should be managed using the built-in <Link> component (extended from #reach/router from React).
To make dynamic the <Link to="collection/1"> link, you should make a page query, as I said, to get the proper link to build a custom dynamic <Link> from your data.
Once you installed the gatsby-plugin-intl plugin, all your pages will be prefixed automatically, however, to point to them using <Link> or navigate you need to get the current language and prefix it:
export const YourComponent = props => {
const { locale } = useIntl(); // here you are getting the current language
return <Link to={`${locale}/your/path`}>Your Link</Link>;
};
Because useIntl() is a custom hook provided by the plugin, the value of locale will be automatically set as you change the language.
How would I be able to test the router in the code below? When using React you are able to use MemoryRouter to pass initialEntries to mock a route change but I cannot find an alternative for preact-router. I looked at the Preact docs and the preact-router docs but I am unable to find a clear solution.
import 'preact/debug';
import { h, render } from 'preact';
import HomePage from './pages/homepage';
import Router from 'preact-router';
import AsyncRoute from 'preact-async-route';
import './styles/index.scss';
const App = () => (
<Router>
<HomePage path="/" />
<AsyncRoute
path="/admin"
getComponent={ () => import('./pages/admin').then(module => module.default) }
/>
</Router>
);
export default App;
This is a little old, but I figured I would share what I found.
The first and quickest thing to do is to just use the route function in preact-router.
import { render, route } from 'preact-router';
import App from './App';
describe('<App/>', () => {
it('renders admin', async () => {
const { container, findByText } = render(<App/>);
// Go to admin page
route('/admin');
// Wait for page to load since it's loaded async
await findByText(/Admin Page/);
// perform expectations.
});
});
While this works, I don't like that it relies on the brower's real history. Luckily, the <Router> component accepts a history prop of type CustomHistory. So you can use an in-memory implementation of a History API to make this happen. I think I've seen docs that suggest using the history package - however I had to make an adjustment
import { createMemoryHistory } from 'history';
class MemoryCustomHistory {
constructor(initialEntries = undefined) {
this.wrapped = createMemoryHistory({initialEntries});
}
get location() {
return this.wrapped.location;
}
// Listen APIs not quite compatible out of the box.
listen(callback) {
return this.wrapped.listen((locState) => callback(locState.location));
}
push(path) {
this.wrapped.push(path);
}
replace(path) {
this.wrapped.replace(path);
}
}
Next, update your app to accept a history property to pass to the <Router>
const App = ({history = undefined} = {}) => (
<Router history={history}>
<HomePage path="/" />
<AsyncRoute
path="/admin"
getComponent={ () => import('./pages/admin').then(module => module.default) }
/>
</Router>
);
Finally, just update the tests to wire your custom history to the app.
it('renders admin', async () => {
const history = new MemoryCustomHistory(['/admin]);
const { container, findByText } = render(<App history={history}/>);
// Wait for page to load since it's loaded async
await findByText(/Admin Page/);
// perform expectations.
});
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 am using react-loadable to split my code. I have opted to do this at route level to begin with. My routes.js looks like this:
import React from 'react';
import { Route, Switch } from 'react-router-dom';
import Loadable from 'react-loadable';
const Loading = props => {
if (props.error) {
return <div>Error loading page!</div>;
} else {
return null;
}
};
const AsyncHomepage = Loadable({ loader: () => import('../Scenes/Homepage/Homepage' /* webpackChunkName: "homepage" */), loading: Loading });
const AsyncSearchPage = Loadable({ loader: () => import('../Scenes/Search/Search' /* webpackChunkName: "search" */), loading: Loading });
const AsyncAboutUsPage = Loadable({ loader: () => import('../Scenes/AboutUs/AboutUs' /* webpackChunkName: "aboutus" */), loading: Loading });
// omitted for brevity
export const Routes = () => {
return (
<Switch>
<Route exact path="/" component={AsyncHomepage} />
<Route exact path="/search" component={AsyncSearchPage} />
<Route exact path="/about-us" component={AsyncAboutUsPage} />
// omitted for brevity
</Switch>
);
};
This is the only place I'm using react-loadable.
When I build the application, I get my named chunks as expected but I also get a bunch of random numbered chunks as well:
These other chunks seem to contain things like moment.js, react-select, there's another that contains react-dom which surely would just be included in every page?
What I'd like to know is why is this happening and how can I name these chunks or merge them with the page chunks?