DEV Community

Discussion on: Learning to Appreciate React Server Components

Collapse
 
peerreynders profile image
peerreynders

homepage.js and homepage.data.js

The thing is that the example of retrieving data for an entire page and injecting that data into a "component" to render the appropriate user representation is easily justified - meanwhile the example from Data Fetching with React Server Components where each component fetches its own data evokes all the controversy that goes along with using active record as an architectural pattern - mostly because component boundaries are often too finely grained to align with optimal data access and query boundaries. Fetching an artist details, top 10 and discography separately only makes sense if that information is managed by separate services - and even then those services will usually be queried for data relating to multiple artists. Ideally a visual component should receive just enough data to do its job while having PI (persistence ignorance), i.e. it should have no dependency on the fundamental organization of the data.

From that perspective having fine-grained components that fetch their own data should raise some eyebrows.

In Complexity: Divide and Conquer! Michel Weststrate states:

If the view is purely derived from the state, then routing should effect the state, not the derived component tree.

By the same reasoning visual components should appear as a result of state change - the appearance of visual components should not result in a change of state.

And in terms of coupling, data fetching components have the whole banana, gorilla, jungle thing going on.

From server vs client perspective the library providing a constant isServer variable would allow any bundler to dead code eliminate the server only code from the client.

Marko's optional split components seem more straight forward:

  • the x.marko template describes the user facing HTML fragment.
  • the x.component.js module describes the server side functionality to instantiate the template.
  • the x.component-browser.js module describes the code that takes control of the fragment on the client-side.

x.component.js can assume a Node.js environment while x.component-browser.js is tailored to the browser environment - and common code is handled via shared modules.

It really doesn't take much more to put together what would happen if the data was encoded JSX(or HTML) instead of JSON data.

... but it still has to be decoded. A straight up HTML partial spliced into the DOM is intuitively fast.

Decoding some non-standard stream of elements for the VDOM - perhaps including code for some nested client-side components - may not be that much faster than decoding JSON data with a custom client-side component.

Collapse
 
ryansolid profile image
Ryan Carniato • Edited

Yeah I thought that example in the video to be illustrative rather than best practices. I don't expect there to be many server components fetching data in practice if you want good performance. I think the problem is apps already fetch below the navigation fold and they want an easy way to address it.

I do find I make these sort of concessions in Solid compared to React. React solutions tend to be general, and I tend to focus on a category. My application of concurrent rendering is limited mainly to linear progressions like navigation where React's concurrent mode could render 10 different possible futures.

Same here as I think co-location still requires logical roots. I like the idea of giving components the say in what they fetch the way GraphQL fragments do but it's more of a secondary tree that starts from the .data.js component rather than a bunch of isolated nodes. This lets us build-up to the single query again. I suppose Server Components on their own don't help. It moves the inefficiencies to the server.

The thing I like about isServer is generally I think this gets buried in a service. Like Solid has a special primitive for data loading called a resource that takes an input signal (that is tracked), and a fetcher that receives the value of that signal and returns the promise.

const [user] = createResource(() => props.userId, fetchUser)
Enter fullscreen mode Exit fullscreen mode

I'm more likely to bury isServer in the fetchJSON call. I will just swap out the database call for the API call. Now this doesn't help with partial hydration but it means that most of the component code looks isomorphic. Until I toyed with a .server.js idea I really wanted authoring the pages and wiring up the data to really be thinking client vs server. Marko's split works because it uses Single File Components. Not something we will see JSX libraries pick up.

... but it still has to be decoded. A straight up HTML partial spliced into the DOM is intuitively fast.

Yeah but React is talking about diffing it. It is arguable the performance savings of running through a diff and patch but they are touting preserving browser state like element focus. Now not sure which stateless elements will be carrying focus there, but presumably they could re-render on the server and only update the content of one textNode. I'd argue if they just sent the data my fine-grained reactivity could have done that with less over the wire, and no diffing. But they saved sending the component JS.

But the thing I wonder is, in fine-grained reactive approach component size scales with amount of interactivity. The largest static component is still:

const tmpl = makeTemplate("Giant HTML String")

function MyComp() {
  return !isHydrating && tmpl.cloneNode(true);
}
Enter fullscreen mode Exit fullscreen mode

Most of the component is the HTML string. If I send that in a JS file I fetch in parallel or send it over the wire each time what am I actually saving. The problem is doing both on initial render if you never render it again on the client. Not too hard if you can detect it being stateless to just have the component no-op and treeshake the template.

Collapse
 
peerreynders profile image
peerreynders

The thing I like about isServer is generally I think this gets buried in a service.

My personal perspective is that isServer prompts the whole Replace Conditional with Polymorphism scenario - or in this case don't complect things that are separate.

I think a case can be made that isServer is symptomatic of a design error. CSR frameworks deliberately design around components being client centric: components are created on the client, live on the client and die on the client.

Introducing SSR and Server-Side Components violates that fundamental assumption of CSR design. Typically addressing such a "design error" without fundamentally changing the underlying design leads to accidental complexity. And I think that's what's happening with React - its core is taking on more and more complexity to accommodate the illusion of its (beloved) abstraction while having to deal with the realities of its operational environment.

Contrast that with a framework that from the get-go frames the problem entirely differently:

  1. Server side: Component needs to be rendered and perhaps some initial UI state. No behaviour required.
  2. Client side: Component is already rendered and perhaps has some initial UI state. UI state dependent behaviour still needs to be bound to it.
  3. Client side: Component is created entirely on the client.

CSR frameworks only worry about the last case - they are "incorrectly framed" to address the the other two cases down the road (with predictable consequences).

Marko's split works because it uses Single File Components. Not something we will see JSX libraries pick up.

My view is that it works for Marko because the template can be factored out as a commonality between the server (as an HTML fragment) and the client (as a DOM fragment). The issue is that most JSX code conflates code to control templating with code to enable interaction behaviour. Code to control templating is needed on both the server and client side. Interaction behaviour is purely a client-side aspect - therefore should be optionally injected (perhaps at compile-time) into rendering.