Replies: 15 comments 7 replies
-
Thanks for writing this up! I get the general gist of what you're saying here. Unfortunately, I've never dealt with SSR usage myself, so it's a topic I don't have any real experience with or suggestions to offer. I am curious how other React ecosystem state management libs deal with this question, as well as how other similar tools like VueX and NgRx handle this. Would be interested in seeing comparisons even if the answer is "Vue and Angular just have very different constraints, so this isn't an issue there". |
Beta Was this translation helpful? Give feedback.
-
Not a SSR person here, but serious question: Does this type of SSR code splitting make any sense at all, since stuff like bundle size does not play a role there? I would imagine that on the server, you probably want to load all reducers at initialization and only do the code splitting/late injection on client side. True, this would lead to a phase where the reducer would not be injected after initially having been injected and it would increase the size of store contents to be communicated from server to client (since it would contain all |
Beta Was this translation helpful? Give feedback.
-
@phryneas That's a good question that I didn't cover in the original post! The bloated Edit to clarify: Hydration mismatches are when the markup created on the server and the client does not match. First render on the client needs to produce exact same result as on the server, so you still can't run |
Beta Was this translation helpful? Give feedback.
-
Maaaybe you could do something crazy like injecting all reducers on the server as per above and (pseudo-code): // clientEntry.js
const store = createStore(...);
if (isClient) window.store = store; // DynamicComponent
if (isClient && window.store) injectReducer(window.store, reducer); I haven't thought this through, but doesn't feel like a great solution even if it did work? |
Beta Was this translation helpful? Give feedback.
-
Oh, and just to comment on this, with serverless and lambdas code size does matter, but definitely less than on the client and for most apps I don't think including all reducers in the server code would be a problem. With ducks or RTK slices it's just not reducers though but actions and selectors too since they live in the same file, but still. |
Beta Was this translation helpful? Give feedback.
-
Maybe turn your "global" pattern around: Inject a slice reducer on module load and additionally, register it to a global variable. (Since you want a reducer to be present once you start importing the actions and/or selectors as well I guess?) That would do it for the client/first SSR. On the server, just inject all reducers that are already registered to that global reducer collection on every store creation. |
Beta Was this translation helpful? Give feedback.
-
That would be neat and I remember pursuing this avenue a year ago or so, I just remembered why it sadly doesn't work. 😞 The code for the dynamic components is not parsed and executed immediately, it happens inside of render of the parent component. I'm not sure if this applies to all of |
Beta Was this translation helpful? Give feedback.
-
Before the client component could actually execute anything imported from the slice module, the whole slice module would need to be parsed though? Can you give a specific example on how that would not work? |
Beta Was this translation helpful? Give feedback.
-
Yes. I think I misunderstood your comment as registering all reducers to a global registry ahead of creating the store by doing that in the module scope. That wouldn't work because of the above, but registering reducers to an already created global store would still work which on rereading seems like what you meant? But then I don't quite understand how your suggestion turns the pattern around? You would still need access to the store in the module scope, which would mean making it global on the client? Did you mean it as just a convenient way to inject all reducers on the server and avoiding a manual step for that? Still, registering them to a global variable would happen upon the first render on the server unless you preload the modules to execute that code explicitly ahead of render, which is possible in many frameworks through an extra step. |
Beta Was this translation helpful? Give feedback.
-
I need to think some more about this. My gut feeling is that it could be a possible solution in some or even many cases, but might not be general enough to solve all cases for all frameworks. Maybe not a solution to base an official API surface on, but could be generalizable enough to warrant adding a pattern for in the docs (though if you include scrubbing unused (On that note, since client side is a lot simpler, adding a pattern for where and how to call |
Beta Was this translation helpful? Give feedback.
-
Well, your point was that "module scope" is only read once for all invocations for SSR, so injecting on module load is not an option, right? My point is that "injecing on module load" will work just fine for the client. And for the server, you would do "register on module load in a central place". And on every subsequent store creation on the server, you can already inject all those "registered reducers" into that newly created store. That way, you would be in that situation of "server has all reducers, client only the required ones". |
Beta Was this translation helpful? Give feedback.
-
Yes, for the server this is not possible (technically it could be with isolates or other fancy solutions which isolates each request into it's own scope, but that's complex and can't be an official solution so lets leave that aside).
Unless modules are explicitly not dynamically loaded on the server, or explicitly preloaded ahead of time, that registration wont happen until that particular module has been rendered at least once, leaving the first render of any path broken. Now that I think about this, either of those solutions would pretty much require no code splitting on the server, which would definitely be bad in the serverless case.
This is indeed not a problem in this solution. Thanks for clarifying, I indeed misunderstood the first time and understood you correctly the second time. 😄 |
Beta Was this translation helpful? Give feedback.
-
Since this is more of a discussion than an issue, I'm moving it over here. |
Beta Was this translation helpful? Give feedback.
-
Maybe I should have saved this for the two year anniversary of this discussion which is tomorrow (:tada:? 😃), but: I think there is one thing I missed in this discussion, which is the possibility of making the injection idempotent. Idempotent side effects are okay in render which would remove this problem. This is probably hard to do though since any changes to the store (which reducer injection certainly is) triggers subscriptions, possibly triggering rerenders etc. If nothing is actually listening to that specific change, maybe you could argue the injection is side effect free, and this is the reason this approach has worked decently for so long, but this is not something you could rely on when building official APIs, we'd have to guarantee the injection to be idempotent. |
Beta Was this translation helpful? Give feedback.
-
This might be a dumb question, as I've used SSR only for smaller projects so far: Also, the injection being idempotent is not completely out of question. If I remember correctly, e.g. redux-dynamic-modules used module-counting to ensure that modules where only added if they were not added before while making sure that they are also only removed if there is no reference that uses them left. |
Beta Was this translation helpful? Give feedback.
-
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.
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:
import(...).then(cb)
only runs first time on server)import(...).then(cb)
only runs first time on server)getInitialProps
, but that is not a recommended API anymore and has other tradeoffs, e.g. breaking incremental static site generation)(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 auseInjectReducer
hook and you'll notice that either you do it inuseEffect
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.Beta Was this translation helpful? Give feedback.
All reactions