Skip to content

The problem of code splitting #4010

@Ephem

Description

@Ephem

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.
  • 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.

(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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions