Suspense for lazy-loading reducers #4114
Replies: 3 comments 17 replies
-
Appreciate the writeup! I'm going to try to give First, a caveat: I haven't done any actual lazy-loading of reducers myself. I'm familiar with the theory and basic mechanics, of course, but I've never tried to implement it in any app I've worked on. I think that the idea of leveraging Suspense in the lazy-loading process is actually a very viable approach, both in concept and in potential implementation. It just isn't something that had specifically occurred to me here. Suspense is sorta-kinda documented-ish, at this point. There's plenty of libs using it, and the "throw a promise" mechanism is well known. It's also correct to be throwing that promise while rendering, because React immediately suspends rendering for that component. From its point of view, there's never any inconsistency - by the time it does get rendered for real, it has data available and just keeps on chugging. The React team has documented a lot more of how Suspense will work in React 18 in the Working Group discussion threads in the last couple weeks: There's a lot of precedent for lazy-loading happening in the render phase. In fact, I've been told that it was critical to do this in the constructors of class components, so that the store was updated before
What "warnings" are you seeing atm? Is it the one where React says "you can't update another component while this one is rendering"? If so, I actually wouldn't expect that to be happening here, if you have well-written selectors, because none of the other parts of the state have changed and they should still be returning identical references as the last call. Given that you're looking at using Suspense, though, you ought to be able to do the So, to specifically answer your questions:
I'm going to ping @Ephem , who I know has done a lot of work around SSR and lazy-loading with Redux, and also see if anyone else has thoughts on this. |
Beta Was this translation helpful? Give feedback.
-
@NotTheUsual I think two things may make this task much easier.
So to implement:
Code examples: // @filename: store.ts
export function createStore(/* ... */) {
return configureStore({
// set up store so it has addReducer and hasReducer.
// have a minimal or empty reducer, initially.
});
}
export type BaseStore = ReturnType<typeof createStore>;
// @filename: cheeseSlice.ts
import { useDispatch, useStore, useSelector } from 'react-redux';
import { createSlice } from '@reduxjs/toolkit';
import type { BaseStore } from './store.ts';
const cheeseSlice = createSlice(/* ... */);
export const useCheeseDispatch = () => {
const store = useStore();
if (!store.hasReducer(cheeseSlice.reducer)) store.addReducer(cheeseSlice);
return useDispatch();
};
export const useCheeseState = () => {
if (!store.hasReducer(cheeseSlice.reducer)) store.addReducer(cheeseSlice); // DRY it how you want
return useSelector(store => store.cheese);
}
// @filename: CheeseDashboardPage.tsx
import { useCheeseState } from './cheeseSlice.ts'
export const CheeseDashboard = props => {
const cheeseState = useCheeseState();
return <span>You have {cheeseState.quantity} slices of cheese pizza</span>;
}
// @filename AppRouter.tsx
import { lazy } from 'react';
const CheeseDashboardPage = lazy(() => import('./CheeseDashboardPage'));
export const AppRouter = () => (
<Switch>
<Route path="/cheese" element={<CheeseDashboardPage />} />
</Switch>
); This will keep your reducers out of the main bundle, and since you're putting |
Beta Was this translation helpful? Give feedback.
-
This is a bit in the realm of “you’ve been asked not to do this for a reason” but, well, if you’re in the mood for something slightly inadvisable, read on.
We’ve set up our app to lazy load different pages with
React.lazy
, as you do. And, to match, we’re lazily adding Redux modules with theinjectReducer
function suggested on the Code Splitting page of the Redux docs.The reducer injection happens in a few of these entry point hooks dotted around the app, which also cover loading extra translations, etc. We check if we’ve run the initialisation before, and if not set everything up in a
useEffect
block (if it’s done in the body of the component, we get the warnings that we’re trying to update another (Redux-y) component from the body of this one). Looks a bit like thisYou then tend to end up with a component arrangement something like this
Now, here’s the part where people start to frown at me. Towards the end of last week, slightly tired, I proposed trying to Suspend the component while we initialised the code. Upsides being that (1) we wouldn’t have to manage the loading state ourselves, (2) we could slot the entry point hooks at the top of any component, knowing that any code below it would only be run post-initialisation, (3) these files were almost always used in lazy-loaded components, so almost always sat inside Suspense boundaries anyway. The downsides, of course, being that it’s an undocumented, unsupported API that you’re absolutely not meant to use in production code, for sensible reasons.
Code then starts to look more like this
// Parent code as-was
which does feel nicer! To my eyes, anyway. Shame it doesn't fully work outside of tiny demos.
The issue looks like this:
And the code above falls foul of the second condition when you try to add it to a real app.
Now, again, the obvious answer is “just don’t use the unreleased API” but while I’m down in this rabbit hole, the questions I have are:
Maybe worth discussion of how reducers “should” be lazily added in general? The docs talk about creating
injectReducer
(or alternatives), but less about calling them.Anyway, thanks for indulging me by reading this far (if you have) - hope there’s something interesting here for you, too.
Beta Was this translation helpful? Give feedback.
All reactions