A library to manage react component initialization in isomorphic applications using Redux.js.
This library is designed for usage in large-scale react applications with server-side rendering. It can also be used in smaller applications or application without server-side rendering. However, in these use cases a less complex solution might be more appropriate.
This library will only work for applications that have the following setup:
- A react+redux setup with server-side rendering. Redux state on the server should be injected into the client as initial state (as described in the Redux documentation)
- A Redux store configured with the redux-thunk middleware
- Support for Promises on both the server and client side
In a react application, we often want to perform a certain action when a component mounts. These actions are often asynchonous (like loading some data from an api). More specifically, in isomorphic applications (with server side rendering) we often want these actions to be completed before we start rendering the page. In order to achieve this, we have two alternatives:
- Component-based approach: We define the initialization actions on each component. This comes with a problem: the server does not know which components are mounted before a react render has completed. This would mean we have to do at least 2 render calls: one to figure out which components need to be initialized, and another after initialization actions have completed.
- Top-down approach: We define all initialization on the page level. This can get messy very quickly, because a page has to have knowledge about the data needs of all descendant components. It is easy to make a mistake and do too little or too much initialization. Moreover, it can lead to code duplication between pages.
This library aims to provide utilities to make the component based approach a feasible solution. It allows us to define initialization on each component without having to do more than one render.
Below is a general explanation of the implementation for this library. For quick setup instructions, see "Setup" below.
- Server side on the server, we don't start rendering until all components have been initialized.
- Set
initMode
theinitMode
is initially set toMODE_PREPARE
to indicate that we want to initialize components before we mount them. - Component prepare Before we start rendering, we need to call the initialization action of every component configured with
withInitAction()
. We refer to this as "preparing a component" and this can be done using theprepareComponent()
action. For more info see "The prepare tree" below. Note: components configured withallowLazy
may skip this step. For more info see thewithInitAction()
docs below - Wait for preparation to complete Before we render our page, we need to wait for the preparation to complete. This can be done using the promise returned by
prepareComponent()
- Render Our page can now be rendered. To make sure we never skip an initialization action, all components configured with
withInitAction()
will throw an error if mounted without preparing it first.
- Set
- Client side on the client, we don't want to redo initialization that has already been done on the server. When new components mount (for example, on client-side navigation), they should be initialized as well.
- First render this is essentially the same as step 4 on the server side. All component preparation has already been done on the server.
- Set
initMode
we dispatchsetInitMode(MODE_INIT_SELF)
to indicate that new components should initialize themselves as soon as they mount. - Next render(s) If a new component wrapped in
withInitAction()
mounts, it will automatically initialize. Additionally, a component can also be configured to re-initialize if itsprops
update. Note: By default, the component will start rendering even if theinitAction
has not completed yet. For more info see thewithInitAction()
docs below.
As described in "initialization lifecycle" above, we need to dispatch prepareComponent()
for each component on the page before page render. But how do we know in advance which components will be on our page? The trick is to configure our page component initialization to dispatch prepareComponent()
for each direct child component with an initAction
. We configure the child component initialization to dispatch prepareComponent()
for their children, and so on. This way, we only have to dispatch prepareComponent()
on the page component we want to render and it will recursively prepare its descendants.
Below is an example of a HomePage
component layout. We will need to load the notifications to display in the header, the list of posts, and some detail data for each post.
We use withInitAction()
to add the following initialization to our components:
Homepage
callsprepareComponent(Header)
andprepareComponent(HomeTimeline)
Header
callsprepareComponent(HeaderNotifications)
HeaderNotifications
loads the notifications for the current userHomeTimeline
loads a list of posts. It now has a couple of post ids and callsprepareComponent(Post, { id: postId })
for each postPost
loads some detail data to display the itself
NOTE: In this example, the list of posts are loaded separately from the post detail data. In another application this might be a single call
Make sure you have an existing setup with the prerequisites listed above.
Attach the react-redux-component-init
reducer to your Redux store under the init
key. The easiest way to do this is by using Redux combineReducers():
import { combineReducers, createStore } from 'redux';
import { initReducer as init } from 'react-redux-component-init';
const mainReducer = combineReducers({
init: initReducer,
// ... other reducers in the application
});
const store = createStore(mainReducer);
Please note: it is recommended to attach the reducer to the init
key, but it is also possible to include the reducer elsewhere in the state. See the getInitState
option of the withInitAction()
HoC.
In the function that renders your page on the server, call prepareComponent
with the page components you will render before you render your page. The example below is using express and react-router 3, but these are not required.
import { prepareComponents } from 'react-redux-component-init';
import { match, RouterContext } from 'react-router';
import { Provider } from 'react-redux';
import { renderToString } from 'react-dom/server';
...
function renderPage(req, res) {
...
match({ routes: Routes, location: req.url }, (error, redirectLocation, renderProps) => {
...
// note: prepareComponents is just a shorthand for multiple prepareComponent() wrapped in Promise.all()
store.dispatch(prepareComponents(
renderProps.routes.map(route => route.component),
renderProps
)).then(() => {
res.send(renderToString(
<Provider store={store}>
<RouterContext {...renderProps} />
</Provider>
);
});
});
}
On the client side of your application you should switch the initMode to MODE_INIT_SELF
after the first render.
import { setInitMode, MODE_INIT_SELF } from 'react-redux-component-init';
...
store.dispatch(setInitMode(MODE_INIT_SELF));
Higher-order component that adds initialization configuration to an existing component.
initProps
{Array<string>}
(optional) An array of names ofprops
that are relevant for initialization.- Only the values of these props are available in the
initAction
function - On component mount, a value is required for each of these props
- The values that these props will have on mount need to be provided to
prepareComponent()
- Component preparation using
withPrepare()
only executes once for each combination of these props. Duplicate calls (with the sameComponent
and the same values forinitProps
) will be ignored. - By default, if these props change value on the client, the component will "re-initialize". See
options
below - Dot notation can be used to define a subset of an object prop. For example, when using
['foo.bar', 'foo.foobar']
theinitAction
will only get the propertiesbar
andfoobar
on thefoo
prop.
- Only the values of these props are available in the
initAction
{(props, dispatch, getState) => Promise}
This is the actual initialization function. This function must return a Promise that resolves when initialization is complete. It receives the following arguments:props
{object}
An object containing values of the props defined ininitProps
. IfinitProps
is not defined, this is an empty object.dispatch
{function}
The Redux dispatch function. This can be used to dispatch initialization actions or dispatch thewithPrepare()
action for child componentsgetState
{function}
The Redux getState function.
options
{object}
(optional) An object containing additional options:allowLazy
Iftrue
, no error will be thrown when the component is mounted without being prepared usingprepareComponent()
first. Instead, theinitAction
will be performed oncomponentDidMount
on the client, as if it wasn't mounted on first render. This can be used to do non-critical initialization, like loading data for components that display below the fold. Defaults tofalse
reinitialize
Iftrue
, will callinitAction
again if any of the props defined ininitProps
change after mount. This change is checked with strict equality (===) Defaults totrue
initSelf
A string that indicates the behavior for initialization on the client (initMode == MODE_INIT_SELF
). Possible values:"ASYNC"
(default) the component will render immediately, even ifinitAction
is still pending. It is recommended to use this option and render a loading indicator or placeholder content untilinitAction
is resolved. This will give the user immediate feedback that something is being loaded. While theinitAction
is pending, anisInitializing
prop will be passed to the component."BLOCKING"
this will cause this higher-order component not tot mount the target component until the first initialization has completed. The component will remain mounted during further re-initialization."UNMOUNT"
same as"BLOCKING"
but it will also unmount the component during re-initialization."NEVER"
will only initialize on the server (initMode == MODE_PREPARE
). Initialization will be skipped on the client.
onError
Error handler for errors ininitAction
. If given, errors will be swallowed.getPrepareKey
A function that generates a "prepare key" that will be used to uniquely identify a component and its props. It has the following signature:({string} componentId, {Array} propsArray) => {string}
This defaults to a function that concatenates thecomponentId
and the stringifiedpropsArray
. In most cases, this will ensure that a component instance on the server is matched to the corresponding instance on the client. However, if the props are somehow always different between server and client, you may use this function to generate a key that omits that difference.getInitState
A function that takes the Redux state and returns the init state of the reducer from this module. By default, it is assumed the state is under theinit
property. If the reducer is included elsewhere, this function can be set to retrieve the state.
// PostComponent.js
class Post extends React.Component {
...
}
export default withInitAction(
['id'],
({ id }, dispatch) => dispatch(loadPostData(id)),
{ allowLazy: true }
)(Post);
// PostPage.js
import Post from './components/PostComponent';
...
class PostPage extends React.Component {
...
render() {
...
<Post id={this.props.location.query.postId} />
...
}
}
export default withInitAction(
['location.query'],
({ location: { query } }) => dispatch(prepareComponent(Post, { id: query.postId }))
)(PostPage);
Action creator to prepare a component for rendering on the server side (initMode == MODE_PREPARE
). Should be passed to the Redux dispatch function. Returns a Promise that resolves when preparation is complete
Component
{react.Component}
The component that should be prepared. This should be a component returned by thewithInitAction
higher-order component. If nowithInitAction
wrapper is around the Component, dispatching this action will have no effect.props
{object}
The props to prepare the component with. These should be the same props as you expect to pass when you eventually render component. It should at least include the props configured in theinitProps
array ofwithInitAction
.
A shorthand action creator for multiple prepareComponent
calls with the same props
. Returns a Promise that resolves when preparation for all components is complete
components
{Array<react.Component>}
An array of components to prepareprops
{object}
The props to prepare with
An action creator to switch the initMode
of the application. Should be called with MODE_INIT_SELF
after the initial render on the client.
initMode
{string}
Either of the modesMODE_PREPARE
orMODE_INIT_SELF
as defined in theinitMode
export of this module