Skip to content

Instantly share code, notes, and snippets.

@msutkowski
Last active September 26, 2021 10:49
Show Gist options
  • Save msutkowski/90c90d04474ce51d0e56e96bb21e980d to your computer and use it in GitHub Desktop.
Save msutkowski/90c90d04474ce51d0e56e96bb21e980d to your computer and use it in GitHub Desktop.
Notes on the React Server Components Demo

Introducing Zero-Bundle-Size React Server Components

Notes

The whole experience is based around the client sending GET requests to the server at /react?location= (props are serialized as params)

  •   app.get('/react', function(req, res) {
        sendResponse(req, res, null);
      });

Note: the location is a misleading name IMO - so for the sake of learning purposes and mental models, think about this as state and NOT location in the sense of react/reach-router.

  • The .client file naming is important as the webpack plugin specifically handles those and will include them in the output to react-client-manifest.json, which is used by the server (api.server.js) when rendering the tree.

    • A special point here is that what we're used to with index.js rendering the client is gone, and is replaced by index.client.js, which is treated specially by the webpack plugin.

See: react-server-dom-webpack/plugin for more info.

How the client renders stuff

If you dig into the source, the client side app setup isn't exactly straightforward, but makes sense after a few reads. The 'application shell' is very lightweight and once rendered, it immediately fetches data from the server.

Being that there are no props specified by the client at this time, it just renders App.server with it's defaults.

In the App.server component, there are 2 client components that are referenced:

  • EditButton.client
  • SearchField.client

What's the difference between a server and client component?

  • Server components in this demo do nothing with state, and are simply used to fetch data or do other things based on the props (provided via query string) and return a tree.

  • Client components are what you'd expect them to be. You can mess with local component state, throw a party, etc etc. Nothing happens with the server interaction until you setLocation(props).

  • Server components can contain client components, but client components may not contain server components.

It's worth inspecting what is returned by the initial page load for a better example:

M1:{"id":"./src/SearchField.client.js","chunks":["client5"],"name":""}
M2:{"id":"./src/EditButton.client.js","chunks":["client1"],"name":""}
S3:"react.suspense"
J0:["$","div",null,{"className":"main","children":[["$","section",null,{"className":"col sidebar","children":[["$","section",null,{"className":"sidebar-header","children":[["$","img",null,{"className":"logo","src":"logo.svg","width":"22px","height":"20px","alt":"","role":"presentation"}],["$","strong",null,{"children":"React Notes"}]]}],["$","section",null,{"className":"sidebar-menu","role":"menubar","children":[["$","@1",null,{}],["$","@2",null,{"noteId":null,"children":"New"}]]}],["$","nav",null,{"children":["$","$3",null,{"fallback":["$","div",null,{"children":["$","ul",null,{"className":"notes-list skeleton-container","children":[["$","li",null,{"className":"v-stack","children":["$","div",null,{"className":"sidebar-note-list-item skeleton","style":{"height":"5em"}}]}],["$","li",null,{"className":"v-stack","children":["$","div",null,{"className":"sidebar-note-list-item skeleton","style":{"height":"5em"}}]}],["$","li",null,{"className":"v-stack","children":["$","div",null,{"className":"sidebar-note-list-item skeleton","style":{"height":"5em"}}]}]]}]}],"children":["@4","Does this do a thing?"]}]}]]}],["$","section","null",{"className":"col note-viewer","children":["$","$3",null,{"fallback":["$","div",null,{"className":"note skeleton-container","role":"progressbar","aria-busy":"true","children":[["$","div",null,{"className":"note-header","children":[["$","div",null,{"className":"note-title skeleton","style":{"height":"3rem","width":"65%","marginInline":"12px 1em"}}],["$","div",null,{"className":"skeleton skeleton--button","style":{"width":"8em","height":"2.5em"}}]]}],["$","div",null,{"className":"note-preview","children":[["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}],["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}],["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}],["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}],["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}]]}]]}],"children":["$","div",null,{"className":"note--empty-state","children":["$","span",null,{"className":"note-text--empty-state","children":"Click a note on the left to view something! �"}]}]}]}]]}]
M5:{"id":"./src/SidebarNote.client.js","chunks":["client6"],"name":""}
J4:["$","ul",null,{"className":"notes-list","children":[["$","li","7",{"children":["$","@5",null,{"id":7,"title":"Untitled","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"asdfsadf"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"Untitled"}],["$","small",null,{"children":"7:40 PM"}]]}]}]}],["$","li","6",{"children":["$","@5",null,{"id":6,"title":"Real title!","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"Hello!"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"Real title!"}],["$","small",null,{"children":"7:30 PM"}]]}]}]}],["$","li","5",{"children":["$","@5",null,{"id":5,"title":"I am new","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"asdfasdf"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"I am new"}],["$","small",null,{"children":"7:28 PM"}]]}]}]}],["$","li","4",{"children":["$","@5",null,{"id":4,"title":"A note with a very long title because sometimes you need more words","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"You can write all kinds of amazing notes in this app! These note live on the server in the notes..."}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"A note with a very long title because sometimes you need more words"}],["$","small",null,{"children":"1/4/21"}]]}]}]}],["$","li","3",{"children":["$","@5",null,{"id":3,"title":"I wrote this note today","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"It was an excellent note."}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"I wrote this note today"}],["$","small",null,{"children":"7:21 PM"}]]}]}]}],["$","li","2",{"children":["$","@5",null,{"id":2,"title":"Make a thing","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"It's very easy to make some words bold and other words italic with Markdown. You can even link to React's..."}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"Make a thing"}],["$","small",null,{"children":"1/10/21"}]]}]}]}],["$","li","1",{"children":["$","@5",null,{"id":1,"title":"Meeting Notes","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"This is an example note. It contains Markdown!"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"Meeting Notes"}],["$","small",null,{"children":"1/13/21"}]]}]}]}]]}]

Notice that the two .client components are specified with their filenames and chunks? But wait, there's a component we didn't spot originally: SidebarNote.client, which comes from NoteList.server (child of App.server). This thing is smart enough to know which components need to be handled by the client throughout the whole App.server tree. Pretty cool!

So those client chunks are code split and injected after the response from the server (this is why the react-client-manifest.json) is important.

Tada, that's it! j/k

What happens when I click on a note or edit it?

To give some clarity, it's important to understand that in this demo, there is no real concept of navigation or 'location'. The client portion of the application is wrapped with a LocationContext provider, but would be better named ApplicationContext. Whenever you do anything 'interactive' like create or edit a note, all that happens is the values of the context for the provider change, which causes a network request to be sent to the server. Depending on those values, different things will be render (conditional rendering based on state).

As an overview, let's break down creating a note:

  • Click on New
  • In simple terms, does the equivalent of: setLocation({selectedId: null, isEditing: true})
  • Being that context values have changed, Content() in Root.client creates a request with these query params:

    location: {"selectedId":null,"isEditing":true,"searchText":""}

    • Being that selectedId: null && isEditing: true, Note.server renders:

    <NoteEditor noteId={null} initialTitle="Untitled" initialBody="" />

Cool things

Try searching and watching your network log at the same time. Clear the input. Type the same thing. Spot the difference? It's caching the generated query strings to eliminate duplicate searches.

See: useServerResponse in Cache.client

@chantastic
Copy link

Nice write-up on your first impressions @msutkowski!
Thanks for sharing 🙏

@ajcwebdev
Copy link

Super valuable, thanks so much!

@msutkowski
Copy link
Author

Adding another take that goes much more in-depth about Server Components overall: https://www.swyx.io/react-server-components-demo/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment