Integrate with Volto’s asyncConnect for SSR#
We already know that Volto provides full server-side rendering of the React components, making it an isomorphic application.
How does that work? In simplified pseudocode, it works like this:
in Volto's
server.jsx
we convert the React component tree to an HTML string withreact-dom.renderToString(<Router routes={routes} />)
the Router renders its declared components, which is the
App
and its direct child, theView
component
Now, here is where it gets tricky: the View component should have the content from the backend for the current URL, but it that content is fetched via an async backend endpoint call.
So we need a mechanism to "stop" the processing of the renderToString and make
it wait until the backend content has arrived. In Volto this is solved with
the asyncConnect()
HOC helper, which is a port of redux-connect
The internal implementation uses Redux and the "trick" is to prepopulate the Redux store with the information that would be needed in your components:
Here's an example where the asyncPropsExtenders
configuration is used to
prefetch a footer-links
page from the backend and include it with every SSR:
config.settings.asyncPropsExtenders = [
...(config.settings.asyncPropsExtenders || []),
{
path: '/',
extend: (dispatchActions) => {
const action = {
key: 'footer',
promise: ({ location, store }) => {
// const currentLang = state.intl.locale;
const bits = location.pathname.split('/');
const currentLang =
bits.length >= 2 ? bits[1] || DEFAULT_LANG : DEFAULT_LANG;
const state = store.getState();
if (state.content.subrequests?.[`footer-${currentLang}`]?.data) {
return;
}
const url = `/${currentLang}/footer-links`;
const action = getContent(url, null, `footer-${currentLang}`);
return store.dispatch(action).catch((e) => {
// eslint-disable-next-line
console.log(
`Footer links folder not found: ${url}. Please create as folder
named footer-links in the root of your current language`,
);
});
},
};
return [...dispatchActions, action];
},
},
];
Note: this example is a low-tech "frontend only" solution. In real life you will probably want to create a mechanism where that footer-links information is automatically included with every content request.
As you can see from the above example, the configuration registration is done by using a "modifier" of all the other registered asyncPropsExtender, so we can even change that list of extenders, with something like:
config.settings.asyncPropsExtenders = [
...config.settings.asyncPropsExtenders,
{
path: '/',
extend: (dispatchActions) => dispatchActions.filter(asyncAction=>
asyncAction.key !== 'breadcrumb')
}
]