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 server.jsx we have code like react-dom.renderToString(<Router/>)

  • the Router renders its declared components, which is the App and its direct child, the View 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-er only" solution. In real life you will probably want to devise a mechanism where that footer-links information is automatically included with every content request.

Notice the extender mechanism, we register a "modifier" for the current list of "async connect dispatch actions".

config.settings.asyncPropsExtenders = [
  ...config.settings.asyncPropsExtenders,
  {
    path: '/',
        extend: (dispatchActions) => dispatchActions.filter(asyncAction=>
            asyncAction.key !== 'breadcrumb')
  }
]