Replies: 3 comments 16 replies
-
@Ephem is the main expert on Redux-related SSR that I know of. There's actually an open PR for React-Redux related to React 18 SSR support, and the last few comments of that may be relevant: |
Beta Was this translation helpful? Give feedback.
-
Wow, that's an ambitious project, and a well researched post. I can't comment on everything, but I'll try to pick a few things. 😃 First of all, know that what you are getting into is a pretty big and complex problem space and something not only the React team, but other frameworks as well have spent years thinking about (Marko being early in a lot of regards). What might save you is that you are implementing this for a single specific project with known constraints as opposed to trying to solve for the general case. Also, I'd be wary of locking myself too much into a proprietary solution if that makes it significantly harder to merge with the ecosystem later, but I hear ya, having UX-problems here and now carries a lot of weight. Redux is one big store, and a low level tool, a lot of complexity is going to be up to you to manage here. A likely problem you might run into here is if separate roots rely on the same state in the store. One root hydrates first and changes the state, now the next root and the server will have rendered with different state, causing a hydration mismatch. This might be solvable in the next major Redux version if you are on React >16.8, but even so, I think you'll probably run into other edge cases around this. What I think you need to do is isolate the state for the different roots (at least any state that ever changes). This could still be done with a single store, but it might be easier to actually use separate stores for each root to guarantee they don't modify each other (but they could reuse parts of the initial state). I realize this probably isn't easy to do because of all that legacy code, but yeah, that's the tricky part. If you use a single store for all roots, the trick is to make each root only select parts of the store the other roots doesn't change, at least for as long as hydration of any root is still ongoing, but this is tricky to enforce, especially over time. As for the technical details, how to actually solve this? If you go for the separate stores route, you pass separate A totally separate issue here is of course how to handle script loading. How do you know exactly which scripts needs to be loaded before hydrating a specific root? If you solve that, code splitting reducers and injecting them to the store before dispatching the custom hydrate action should be easy (in comparison). A lot of this builds on the assumption that you want to start hydrating the roots as early as possible on the client, if it's the streaming html you are after but you're fine with hydrating all roots on the client after everything is done and loaded, some problems might get simpler to tackle. In that case, in Web Vitals speak, you would get a good Largest Contentful Paint, but would still have a bad First Input Delay. Just a few random thoughts, not sure if they help. I haven't done this myself, but feel free to ask if you have any specific questions. 😄 (Note that I'm not a RTK Query user, so I'm not sure how that would handle this case, I'm speaking more generally here.) |
Beta Was this translation helpful? Give feedback.
-
That is another kind of streaming - making a HTTP request to an api and getting updates via websocket. You might want to look into what https://github.com/kirill-konshin/next-redux-wrapper is doing - they allow to hydrate SSR content on a per-page basis. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
I asked @markerikson about this in his Twitter DMs, and he recommended I post it here.
I’m looking for advice, concerns, caveats, needed changes, etc. from y’all’s deep Redux context, not necessarily proof-of-concept code. I found some of that in #1184, but a lot of it is specific to other technologies.
For an example of the kind of raw immediate pageload speed I’m targeting (and how we’re sending down chunks of HTTP streams), I wrote a post: Skeleton screens, but fast
Building off that, the gist of the output HTML stream generation would look like…
It’s simplified, but hopefully expresses my point. (This might look similar to
<Suspense>
, but Marko’s<await>
expresses the same functionality differently. The promises for<@then>
are injected from the top-level to avoid the “fetch-as-you-render” waterfall problem the React Suspense docs warn against.)The problem: each of those newly-isolated React roots assume a monolithic Redux store is ready before they render — the SSR response was only as fast as the slowest API call. That’s what I want to avoid; once everything in
headerApiCall
resolves, then we should write an HTTP chunk containing the markup and enough Redux initial state to hydrate it with. The<SiteHead>
should be interactive even iffooterApiCall
takes 10 seconds to finish.Prior art on incremental Redux I found
Twitter Lite’s approach seems closest to what I want, but I’m not quite plugged-in enough to Redux to tell if what they’re talking about is slightly different than what I’m looking for.
Route-splitting reducers seems to be fairly-understood and widely-practiced by now, but are there any problems or gotchas from doing so?
Possibly-important details
The eventual total transmitted JS bundle goal is 20kB, but any improvement over our current ≥52.2kB setup is welcome. For example:
Suspense/Concurrent Mode/
useMutableSource
/etc. are probably “future best practice”, but this is a problem we’re solving for current customers. (We’re not even on React 17 yet 🤷)This is going in an A/B test, so the code doesn’t need to avoid hacks… yet.
Our Redux code doubles as a fascinating archaeological dig. We’ve got
.ts
reducers next toconst SIGN_IN = 'SIGN_IN'
, slices coexisting with/reducers/
, etc.Our reducers/thunks/other sundry functions implement a lot of business logic, analytics, etc. that would be a huge pain to extract.
It also probably contains everything you could think of. For example, a reducer that writes to
localStorage
to work around a SPA architecture problem.We SSR, so the store is JSON-serializable.
The streaming is called “chunked transfer-encoding” in HTTP/1.1; HTTP/2 does it differently, but the results are the same.
The stream must avoid another HTTP request (like how React 18 is doing the weird-not-quite JSON), instead interpolating data into the root
text/html
response.AJAX requests might modify the still-on-server state in the middle of the HTTP request stream (unless that would be too hard to work with, in which case I’ll play ball).
Replicating the entire store on the client is unnecessary, and arguably undesirable: it would be great to “quarantine” pieces on the server that clients don’t need.
Beta Was this translation helpful? Give feedback.
All reactions