I have a project where use react-router v3 only for one reason. The reason is the need of server side rendering with data prefetching and the most convenient way to do this is to keep centralized route config in an object
or array
and loop over the matching elements to fetch the data from the API on the server side. The data later is going to be passed to the client with the response HTML and stored in variable of JSON format string.
Also application uses code splitting, however with the use of babel-plugin-transform-ensure-ignore
on sever side I can directly fetch the components instead of lazy loading and the native import
method will be used only on client side.
Nevertheless, above-mentioned structure isn't working with react-router v5, as it's little bit difficult, since I can't use @loadable/components
, as react-router official documentation suggests. Per my observation @loadable/components
just generates the HTML on the server side instead of giving me the components in which I implement the fetch
method responsible for server side logic.
Therefore, I would like to ask you the good structure for webpack + react-router v5 + ssr + data prefetch + redux + code splitting
I see it's quite complicated and no universal solution, however I may be wrong.
Any direction or suggestion is appreciated.
I have never tried @loadable/components
, but I do similar stuff (SSR + code splitting + data pre-fetching) with a custom implementation of code splitting, and I believe you should change your data pre-fetching approach.
If I got you right, your problem is that you are trying to intervene into the normal React rendering process, deducing in advance what components will be used in your render, and thus which data should be pre-fetched. Such intervention / deduction is just not a part of React API, and although I saw different people use some undocumented internal React stuff to achieve it, it all fragile in long term run, and prone to issues like you have.
I believe, a much better bullet-proof approach is to perform SSR as a few normal rendering passes, collecting in each pass the list list of data to be pre-fetch, fetching them, and then repeating the render from the very beginning with updated state. I am struggling to come up with a clear explanation, but let me try with such example.
Say, a component <A>
somewhere in your app tree depends on async-fetched data, which are supposed to be stored at some.path
of your Redux store. Consider this:
context
, or create a separate one with React's Context API).ReactDOMServer.renderToString(..)
.<A>
somewhere in your app's tree, no mater whether it is code-splitted, or not, if everything is set up correctly, that component will have access both to Redux store, and to the SSR context. So, if <A>
sees the current rendering happens at the server, and there is no data pre-fetched to some.path
of Redux store, <A>
will save into SSR context "a request to load those data", and renders some placeholder (or whatever makes sense to render without having those data pre-fetched). By the "request to load those data" I mean, the <A>
can actually fire an async function which will fetch the data, and push corresponding data promise to a dedicated array in context.ReactDOMServer.renderToString(..)
completes you'll have: a current version of rendered HTML markup, and an array of data fetching promises collected in SSR context object. Here you do one of the following:
You should see, if implemented correctly, it will work great with any number of different components relying on async data, no matter whether they are nested, and how exactly you implemented code-splitting, routing, etc. There is some overhead of repeated render passes, but I believe it is acceptable.
A small code example, based on pieces of code I use:
SSR loop (original code):
const ssrContext = {
// That's the initial content of "Global State". I use a custom library
// to manage it with Context API; but similar stuff can be done with Redux.
state: {},
};
let markup;
const ssrStart = Date.now();
for (let round = 0; round < options.maxSsrRounds; ++round) {
// These resets are not in my original code, as they are done in my global
// state management library.
ssrContext.dirty = false;
ssrContext.pending = [];
markup = ReactDOM.renderToString((
// With Redux, you'll have Redux store provider here.
<GlobalStateProvider
initialState={ssrContext.state}
ssrContext={ssrContext}
>
<StaticRouter
context={ssrContext}
location={req.url}
>
<App />
</StaticRouter>
</GlobalStateProvider>
));
if (!ssrContext.dirty) break;
const timeout = options.ssrTimeout + ssrStart - Date.now();
const ok = timeout > 0 && await Promise.race([
Promise.allSettled(ssrContext.pending),
time.timer(timeout).then(() => false),
]);
if (!ok) break;
// Here you should take data resolved by "ssrContext.pending" promises,
// and place it into the correct paths of "ssrContext.state", before going
// to the next SSR iteration. In my case, my global state management library
// takes care of it, so I don't have to do it explicitly here.
}
// Here "ssrContext.state" should contain the Redux store content to send to
// the client side, and "markup" is the corresponding rendered HTML.
And the logic inside a component, which relies on async data, will be somewhat like this:
function Component() {
// Try to get necessary async from Redux store.
const data = useSelector(..);
// react-router does not provide a hook for accessing the context,
// and in my case I am getting it via my <GlobalStateProvider>, but
// one way or another it should not be a problem to get it.
const ssrContext = useSsrContext();
// No necessary data in Redux store.
if (!data) {
// We are at server.
if (ssrContext) {
ssrContext.dirty = true;
ssrContext.pending.push(
// A promise which resolves to the data we need here.
);
// We are at client-side.
} else {
// Dispatch an action to load data into Redux store,
// as appropriate for your setup.
}
}
return data ? (
// Return the complete component render, which requires "data"
// for rendering.
) : (
// Return an appropriate placeholder (e.g. a "loading" indicator).
);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With