-
-
Notifications
You must be signed in to change notification settings - Fork 15.2k
Description
This is neither a bug nor a feature request, but rather a problem description, but I was asked to post it anyway. I only know how this problem affects the React implementation, but I was asked to post it here in the main repo because there is a big chance that other frameworks have similar problems.
If you are familiar with Redux code splitting in frameworks other than React, please add context to this issue!
The summary of this issue is that code splitting in Redux (at least in React) is currently cumbersome and hard to get right, often leading to bloated main bundles that includes the logic for the entire application, even if the rest of that application is code split. It is my belief that this is not solvable in "a correct way" (without side effects in render) within the boundaries of Redux, React Redux or Redux Toolkit and I'm writing this issue to explain why.
Code splitting in Redux is implemented via the low level replaceReducer. The docs also include a section on code splitting which demonstrates different approaches to create a code splitting abstraction, like injectReducer
. The libraries in use for code splitting are based on some version of that but can offer additional APIs.
So far so good, but things get tricky when you start thinking about where to call these abstractions. To make it easy, the optimal place to call something like injectReducer
would be close to the component using that reducer. Something like a hook or wrapping the component in a HoC or a <ReducerProvider>
or something like that. That way the code splitting would be granular and easy to use. This is also what I proposed in this RFC and this follow up PR but these and the libraries that implement similar APIs have a fundamental flaw when they try to support SSR.
Since SSR does not support useEffect
and you can't inject reducers on a module level there, because modules are shared between each request, the only place you can do it is inside render. This means breaking the render purity rule which is not great. (Some libraries/solutions do this in a class constructor instead, but those shouldn't have side effects either.) This may or may not result in problems now, when Concurrent Mode is launched or further down the road, but breaking render purity might not be a good thing to rely on for a more official code splitting solution (though this is debatable of course).
I only know of two ways to implement this without side effects in render when using SSR:
- Use a code splitting library that supports running a callback on every use of that module.
- loadable-components does not support this (
import(...).then(cb)
only runs first time on server) - next/dynamic does not support this (
import(...).then(cb)
only runs first time on server) - react-universal-components does support this via onLoad (though I'm not sure if this is implemented in a safe way)
- loadable-components does not support this (
- Inject the reducers ahead of time, before rendering starts
- This can't be done in Next.js (possibly via
getInitialProps
, but that is not a recommended API anymore and has other tradeoffs, e.g. breaking incremental static site generation) - Very cumbersome even in custom SSR frameworks, but can be done for example by only declaring reducers to inject at the route level, matching route ahead of time and injecting the reducers then.
- This can't be done in Next.js (possibly via
(Note that because neither of the above solutions works with Next.js, I'm fairly certain it's actually impossible to do reducer code splitting in Next.js without side effects in render currently.)
These solutions lie outside of the scope of the Redux ecosystem, so in my view, I can't come up with a way to fundamentally improve code splitting inside of core.
This is a pretty complex issue and I didn't want to write too much of a monster of a post, so do ask questions if something is unclear. Also, I'd love to be wrong, so do feel free to chip in with ideas! 😄
Update: A good way to approach understanding this problem is to focus only on the custom hook case. Imagine and work through what it would take to call injectReducer
inside a useInjectReducer
hook and you'll notice that either you do it in useEffect
and don't support SSR, or you do it in the body of the hook which is essentially inside of render and thus making render not pure.