-
-
Save acdlite/a68433004f9d6b4cbc83b5cc3990c194 to your computer and use it in GitHub Desktop.
// getComponent is a function that returns a promise for a component | |
// It will not be called until the first mount | |
function asyncComponent(getComponent) { | |
return class AsyncComponent extends React.Component { | |
static Component = null; | |
state = { Component: AsyncComponent.Component }; | |
componentWillMount() { | |
if (!this.state.Component) { | |
getComponent().then(Component => { | |
AsyncComponent.Component = Component | |
this.setState({ Component }) | |
}) | |
} | |
} | |
render() { | |
const { Component } = this.state | |
if (Component) { | |
return <Component {...this.props} /> | |
} | |
return null | |
} | |
} | |
} | |
const Foo = asyncComponent(() => | |
System.import('./Foo').then(module => module.default) | |
) | |
const Bar = asyncComponent(() => | |
System.import('./Bar').then(module => module.default) | |
) | |
const App = () => | |
<BrowserRouter> | |
<Link to="/foo">Foo</Link> | |
<Link to="/bar">Bar</Link> | |
<Match pattern="/foo" component={Foo} /> | |
<Match pattern="/bar" component={Bar} /> | |
</BrowserRouter> | |
export default App |
Sorry if this is a dumb question but where does come from? Is this an old react-routerism that no longer exists?
Hey Andrew @acdlite can u please give clue to do server side rendering along with the code splitting with RRv4
Thank you for this brilliant code. I've been using this code very usefully.
Since System.import
is deprecated, you have to use import
.
Additionally, rather than doing module => module.default
for every routes, you can remove some unnecessary codes by doing ({default: Component}) => ...
in componentWillMount
Full Code:
function asyncComponent(getComponent) {
return class AsyncComponent extends React.Component {
static Component = null;
state = { Component: AsyncComponent.Component };
componentWillMount() {
if (!this.state.Component) {
getComponent().then(({default: Component}) => {
AsyncComponent.Component = Component
this.setState({ Component })
})
}
}
render() {
const { Component } = this.state
if (Component) {
return <Component {...this.props} />
}
return null
}
}
}
const Foo = asyncComponent(() => import('./Foo'))
const Bar = asyncComponent(() => import('./Bar'))
@ashuorg, It is simple. Do not implement code splitting for server side rendering. Why would you need code splitting for SSR?
You can implement code splitting only for the production build by using webpack.NormalModulesReplacementPlugin as:
new webpack.NormalModuleReplacementPlugin(
/^pages$/,
'pages/index.async.js'
),
For more details, check this out: https://medium.com/@apostolos/server-side-rendering-code-splitting-and-hot-reloading-with-react-router-v4-87239cfc172c
@acdlite, is there any idea on loading the asyncComponent related reducers together, and inject the reducers into the store?
I have implemented the injectHelper.
function createReducer(asyncReducers) {
return combineReducers({
...asyncReducers,
system,
router,
})
}
function injectReducer(store, { key, reducer }) {
if (Reflect.has(store.asyncReducers, key)) return
store.asyncReducers[key] = reducer // eslint-disable-line no-param-reassign
store.replaceReducer(createReducer(store.asyncReducers))
}
@acdlite, thanks for you solution it's working perfectly.
Right now, I'm trying to prefetch the async bundles (I have a "setup" on my app, and I know my users will later navigate to an other view).
The prefetched bundles are fine, however when I navigate on an other view, the bundle is reloaded like it haven't been before.
Any idea how to solve the problem please ? I've been trying several things for a few days now and can't get a hint of what's going on on the async bundles...
Thanks :)
Will the component js file be loaded more than once if the component unmounted and mounted again in the case such as React router. In the case of the code is not splitted, one bundle.js file is loaded just once.
Hello everyone!
I'm using this code:
import React from "react";
import {
BrowserRouter as Router,
Route,
Link,
Switch,
Redirect,
Match
} from "react-router-dom";
// import Bundle from "Bundle/Bundle";
// import PedidoApp from "bundle-loader?lazy!./PedidoApp";
// import PedidoForm from "bundle-loader?lazy!./pedidoform";
// getComponent is a function that returns a promise for a component
// It will not be called until the first mount
const asyncComponent = getComponent => {
return class AsyncComponent extends React.Component {
static Component = null;
state = { Component: AsyncComponent.Component };
componentWillMount() {
if (!this.state.Component) {
getComponent().then(({ default: Component }) => {
AsyncComponent.Component = Component;
this.setState({ Component });
});
}
}
render() {
const { Component } = this.state;
if (Component) {
return <Component {...this.props} />;
}
return null;
}
};
};
const PedidoApp = asyncComponent(() => import("./PedidoApp"));
const PedidoForm = asyncComponent(() => import("./pedidoform"));
class Root extends React.Component {
render() {
return (
<Router>
<div>
<Link to="/pedido-1">Pedido</Link>
<Link to="/pedido-1/add">Agregar</Link>
<Route pattern="/pedido-1" component={PedidoApp} />
<Route pattern="/pedido-1/add" component={PedidoForm} />
<Redirect path="*" to="/pedido-1" />
</div>
</Router>
);
}
}
export default Root;
But it shows both components at the same time. How do I do in order to show <PedidoApp/>
by default (Home) and when I click the Add button (or edit) show <PedidoForm/>
???
Thank you
Hello,
I've implemented with recompose:
import React from 'react'
import { lifecycle } from 'recompose'
export default getComponent => {
const setLifecycle = lifecycle({
state: {
Component: undefined,
},
componentWillMount() {
if (!this.props.Component) {
getComponent().then(Component => {
this.setState({ Component })
})
}
},
})
const AsyncComponent = props => {
const { Component, ...otherProps } = props
if (Component) {
return <Component {...otherProps} />
}
return undefined
}
return setLifecycle(AsyncComponent)
}
export class LazyLoadComponent extends Component {
constructor(props) {
super(props);
this.state = {
fetched_module: null
};
}
componentWillMount() {
if (!this.state.fetched_module) {
System.import(this.props.file_location)
.then(fetched_component => {
this.setState({fetched_module: fetched_component.default});
});
}
}
render() {
const CurrentComponent = this.state.fetched_module;
const {loading_page, ...component_props} = this.props;
if (CurrentComponent) {
return (<CurrentComponent {...component_props} />);
}
return (<LoadingPage page={loading_page}/>);
}
}
LazyLoadComponent.propTypes = {
file_location: PropTypes.string.isRequired,
loading_page: PropTypes.string
};
Something like this won't work?
After rendering route component on a server I have FOT because when React initializes app - asyncComponent renders null at first time!
The second problem is about Stores keys - reducers of the route component will be injected into main reducer when it will be loaded.
But at that time server state is lost the keys which are belonged to the loaded component - so SSR is useless ! ((
Is there a solution of these problems?
@aerosunilkumar, transpile using https://babeljs.io/docs/plugins/preset-stage-0/
Thanks for this awesome post, made my life lot easier :)
@acdlite
What is the main purpose of having static Component field?
@budarin @AndreGajdos It's memoization. It's caching it for the next time the component is used. First time it will do the fetch. Every time thereafter it will already be there in the static Component field.
I have the same question as @budarin.
How we can add progress until component not import.
I want to use this type of progress.
@nateq314 This should only be necessary if you're using something other than webpack. Using webpack the module only gets fetched one time, even if you're not caching the component in the static field.
How do I pass props to the lazy loaded component? I see that {...this.props}
has been used, but you can't pass properties to the AsyncComponent. Am I missing something?
I'm currently using something like this:
export default function asyncComponent(getComponent, props = {}) {
return class AsyncComponent extends Component {
state = {};
componentWillMount() {
getComponent().then(({ default: component }) => {
this.setState({ Component: component });
});
}
render() {
const { Component } = this.state
return Component ? <Component {...props} /> : null;
}
};
}
The route entry looks like this:
<Route path="/about" component={asyncRoute(() => import('./About'), { title: "About" })} />
Nice!!!
I would use FetchedComponent
or something internally instead of Component
(since you're probably already importing that symbol (Component
)).
Other than that awesome!
Hello everyone!
I'm using this code:
import React from "react"; import { BrowserRouter as Router, Route, Link, Switch, Redirect, Match } from "react-router-dom"; // import Bundle from "Bundle/Bundle"; // import PedidoApp from "bundle-loader?lazy!./PedidoApp"; // import PedidoForm from "bundle-loader?lazy!./pedidoform"; // getComponent is a function that returns a promise for a component // It will not be called until the first mount const asyncComponent = getComponent => { return class AsyncComponent extends React.Component { static Component = null; state = { Component: AsyncComponent.Component }; componentWillMount() { if (!this.state.Component) { getComponent().then(({ default: Component }) => { AsyncComponent.Component = Component; this.setState({ Component }); }); } } render() { const { Component } = this.state; if (Component) { return <Component {...this.props} />; } return null; } }; }; const PedidoApp = asyncComponent(() => import("./PedidoApp")); const PedidoForm = asyncComponent(() => import("./pedidoform")); class Root extends React.Component { render() { return ( <Router> <div> <Link to="/pedido-1">Pedido</Link> <Link to="/pedido-1/add">Agregar</Link> <Route pattern="/pedido-1" component={PedidoApp} /> <Route pattern="/pedido-1/add" component={PedidoForm} /> <Redirect path="*" to="/pedido-1" /> </div> </Router> ); } } export default Root;But it shows both components at the same time. How do I do in order to show
<PedidoApp/>
by default (Home) and when I click the Add button (or edit) show<PedidoForm/>
???Thank you
You have to use Either <switch>
component to wrap routes (or) Exact
Attribute on <Route>
Hello!
Writing an async component like this in TypeScript:
export function routeAsyncComponent(getComponent: () => Promise<React.Component>) {
interface IRouteAsyncComponentState {
Component: React.Component;
}
return class RouteAsyncComponent extends React.Component<any, IRouteAsyncComponentState> {
static Component: React.Component = null;
state = { Component: RouteAsyncComponent.Component };
componentWillMount() {
if (!this.state.Component) {
getComponent().then(Component => {
RouteAsyncComponent.Component = Component;
this.setState({ Component });
});
}
}
render() {
const { Component } = this.state;
if (Component) {
return <Component {...this.props} />;
}
return null;
}
}
}
TypeScript complains on return <Component {...this.props} />;
:
Type assertion using the '<>' syntax is forbidden. Use the 'as' syntax instead. (no-angle-bracket-type-assertion)tslint(1)
Type assertion on object literals is forbidden, use a type annotation instead. (no-object-literal-type-assertion)tslint(1)
Cannot find name 'Component'.ts(2304)
so dirty and so good, bro
Thank you for this brilliant code. I've been using this code very usefully.
Since
System.import
is deprecated, you have to useimport
.Additionally, rather than doing
module => module.default
for every routes, you can remove some unnecessary codes by doing({default: Component}) => ...
in componentWillMountFull Code:
function asyncComponent(getComponent) { return class AsyncComponent extends React.Component { static Component = null; state = { Component: AsyncComponent.Component }; componentWillMount() { if (!this.state.Component) { getComponent().then(({default: Component}) => { AsyncComponent.Component = Component this.setState({ Component }) }) } } render() { const { Component } = this.state if (Component) { return <Component {...this.props} /> } return null } } } const Foo = asyncComponent(() => import('./Foo')) const Bar = asyncComponent(() => import('./Bar'))
Can't figure out why .default
is needed
@Slavenin use https://github.com/thejameskyle/react-loadable/
It is not so dirty, has
isMounted
checks and other good things.This code is just proposal.