Client Components

Rendering things on the server works well. We can even click on links and because we have state in the URL (whether in URL segment params or search query params), our application will update as the user navigates around. We could even make our server handle form submissions if we wanted to as well. Doing this, we'd be able to completely avoid client-side JavaScript.
But we're not in the business of avoiding client-side JavaScript. We're in the business of excellent user experiences. And modern user experience demands client-side JavaScript. But we want to keep the composibility of React components in the client as well as the server. Especially because our RSC setup enables mixing server-rendered components with client-rendered components.

The Divide

Server components should be used for things that come from the server and don't have client-side interactivity. For example, let's say you're building a todo list. The list of items can be rendered by the server. And the button to mark them as complete/uncomplete can even be rendered by the server as well.
However, if you want to implement optimistic UI for the checkbox to make the user experience feel faster, you're going to need to manage state on the client to simulate the update optimistically. This is a good example where you'd need a client component.
Another example would be a combobox. You need to manage state for the input value, the filter, the selected item, etc.
The great thing is that client and server components can be intermixed. But there's an important caveat to how they're mixed as a result of the way server components and client components interact.

Caveats

There are some interesting caveats to consider when mixing client and server components. The biggest one is the fact that server components can render client components, but client components can't render server components.
Here's a simple example to explain why:
'use client'
import { ServerComponent } from './server-component.js'

function ClientComponent() {
	// some client stuff
	return (
		<div>
			{/* some other stuff ...*/}
			<ServerComponent />
		</div>
	)
}
What happens when the ClientComponent renders? How would the client know what the server component would render? By definition, the server component code is not in the client, so it can't even have a reference to the server.
In fact, that import for the ./server-component.js module would not even work in the client. That module could have secret environment variables or a database connection etc.
So no, you cannot import server modules in client modules for safety reasons.
However, you can do the opposite. Client modules can be imported into server modules. So if you need the structure of the example above, you would simply change the structure:
'use client'

function ClientComponent({ serverUI }) {
	// some client stuff
	return (
		<div>
			{/* some other stuff ...*/}
			{serverUI}
		</div>
	)
}
And then a parent server component would render the client component as well as the server component like so:
import { ClientComponent } from './client-component.js'
import { ServerComponent } from './server-component.js'

function App() {
	return <ClientComponent serverUI={<ServerComponent />} />
}
This composition is safe because server modules can only be imported by other server modules, and client modules (which by definition are safe to send to the client) are imported by both server and client modules.

Client References

There's another caveat to this and that is: what do we do with the client components in the serialized JSX? Remember that there are two ways to get UI to React to render in the browser:
  1. Data + Components in the client
  2. RSC Payload
So when we're generating the RSC payload, should we include the client component code in the payload? If we did, then we would be duplicating the client component code in the payload and in the client bundle. This would be a waste of space. More than that, if the client component has already rendered on the client and has state changes already, then what do we do if we get an updated RSC payload? Blow away those state changes?
No, we can't include client components in the RSC payload. So instead, the RSC payload will include a placeholder for the client component. This placeholder is a reference to the client component and the RSC payload will include information on how to retrieve the module for the client component.
Let's take another look at an example RSC payload:
1:D{"name":"App","env":"Server"}
0:{"returnValue":null,"root":"$L1"}
2:I["/ship-search.js","ShipSearch"]
5:I["/ship-details-pending.js","ShipDetailsPendingTransition"]
6:I["/error-boundary.js","ErrorBoundary"]
8:"$Sreact.suspense"
a:I["/img.js","ShipImg"]
3:D{"name":"SearchResults","env":"Server"}
4:D{"name":"SearchResultsFallback","env":"Server"}
4:[["$","li","0",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}],["$","li","1",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}],["$","li","2",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}],["$","li","3",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}],["$","li","4",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}],["$","li","5",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}],["$","li","6",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}],["$","li","7",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}],["$","li","8",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}],["$","li","9",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}],["$","li","10",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}],["$","li","11",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}]]
7:D{"name":"ShipError","env":"Server"}
7:["$","div",null,{"className":"ship-info","children":[["$","div",null,{"className":"ship-info__img-wrapper","children":["$","img",null,{"src":"/img/broken-ship.webp","alt":"broken ship"}]}],["$","section",null,{"children":["$","h2",null,{"children":"There was an error"}]}],["$","section",null,{"children":["There was an error loading \"","0268fc4817ad1","\""]}]]}]
9:D{"name":"ShipFallback","env":"Server"}
9:["$","div",null,{"className":"ship-info","children":[["$","div",null,{"className":"ship-info__img-wrapper","children":["$","$La",null,{"src":"/img/ships/0268fc4817ad1.webp?size=200","alt":"0268fc4817ad1"}]}],["$","section",null,{"children":["$","h2",null,{"children":"Loading..."}]}],["$","div",null,{"children":["Top Speed: XX"," ",["$","small",null,{"children":"lyh"}]]}],["$","section",null,{"children":["$","ul",null,{"children":[["$","li","0",{"children":[["$","label",null,{"children":"loading"}],":"," ",["$","span",null,{"children":["XX ",["$","small",null,{"children":"(loading)"}]]}]]}],["$","li","1",{"children":[["$","label",null,{"children":"loading"}],":"," ",["$","span",null,{"children":["XX ",["$","small",null,{"children":"(loading)"}]]}]]}],["$","li","2",{"children":[["$","label",null,{"children":"loading"}],":"," ",["$","span",null,{"children":["XX ",["$","small",null,{"children":"(loading)"}]]}]]}]]}]}]]}]
b:D{"name":"ShipDetails","env":"Server"}
1:["$","div",null,{"className":"app","children":[["$","div",null,{"className":"search","children":["$","$L2",null,{"search":"m","results":"$L3","fallback":"$4"}]}],["$","$L5",null,{"children":["$","$L6",null,{"fallback":"$7","children":["$","$8",null,{"fallback":"$9","children":"$Lb"}]}]}]]}]
c:I["/ship-search.js","SelectShipLink"]
3:[["$","li","Bomber",{"children":["$","$Lc",null,{"shipId":"5c13d8b28a14a","highlight":false,"children":[["$","$La",null,{"src":"/img/ships/5c13d8b28a14a.webp?size=20","alt":"Bomber"}],"Bomber"]}]}],["$","li","Diplomatic Vessel",{"children":["$","$Lc",null,{"shipId":"6f375578ead88","highlight":false,"children":[["$","$La",null,{"src":"/img/ships/6f375578ead88.webp?size=20","alt":"Diplomatic Vessel"}],"Diplomatic Vessel"]}]}],["$","li","Mining Ship",{"children":["$","$Lc",null,{"shipId":"627c497212456","highlight":false,"children":[["$","$La",null,{"src":"/img/ships/627c497212456.webp?size=20","alt":"Mining Ship"}],"Mining Ship"]}]}],["$","li","Medical Ship",{"children":["$","$Lc",null,{"shipId":"0268fc4817ad1","highlight":true,"children":[["$","$La",null,{"src":"/img/ships/0268fc4817ad1.webp?size=20","alt":"Medical Ship"}],"Medical Ship"]}]}]]
b:["$","div",null,{"className":"ship-info","children":[["$","div",null,{"className":"ship-info__img-wrapper","children":["$","$La",null,{"src":"/img/ships/0268fc4817ad1.webp?size=200","alt":"Medical Ship"}]}],["$","section",null,{"children":["$","h2",null,{"children":"Medical Ship"}]}],["$","div",null,{"children":["Top Speed: ",2," ",["$","small",null,{"children":"lyh"}]]}],["$","section",null,{"children":["$","p",null,{"children":"NOTE: This ship is not equipped with any weapons."}]}]]}]
We're not here to understand this format deeply (and it's possibly subject to change anyway). So let's cut out some of the bits in the payload to focus a bit and I'll format it to make it easier to read:
...
2:I["/ship-search.js","ShipSearch"]
// ...
1:[
  "$",
  "div",
  null,
  {
    "className": "app",
    "children": [
      [
        "$",
        "div",
        null,
        {
          "className": "search",
          "children": [
            "$",
            "$L2",
            null,
            { "search": "m", "results": "$L3", "fallback": "$4" }
          ]
        }
      ],
      [
        // ...
      ]
    ]
  }
]
...
Notice there's an entry for the I type with the id of 2 and that's referenced in the 1 entry by $L2.
So when React is processing the RSC payload, it sees that $L2 is a reference, it looks up that reference, sees that it's a client component and knows which module to find it in. It imports ShipSearch from /ship-search.js and renders it in that place.
This is how React avoids duplicating client component code in the RSC payload.

use client

So then the question is, how do we avoid duplicating client component code in the server when we're importing those modules and rendering them in our server components?
The answer is the bundler you're using. When you're creating the RSC bundler, you can tell it to replace certain modules with the reference to the client module. And you tell it using the 'use client'.
We can do this without a bundler by registering a custom Node.js loader. Luckily, for us, react-server-dom-esm exports a loader for us to use and as this isn't a workshop about Node.js loaders, you won't be required to configure it yourself. But it's important that you understand what it's doing. So we'll have you toss a few console.logs around until you get it.

Importing Client Components

One final thing you're going to need to understand about client components is how they're loaded. You're not responsible for loading them yourself.
We hand off the RSC payload to react-server-dom-esm/client which will take care of loading the client components for you. All we have to do is tell it where to get the modules.
You'll recall from the RSC format above that the client modules look like this:
2:I["/ship-search.js","ShipSearch"]
The first item in the array is the path to the module and the second item is the name of the export in module. The full path to that module will be something like file:///Users/kentcdodds/code/epicweb-dev/react-server-components/playground/ui/ship-search.js.
So when we call renderToPipeableStream with the RSC payload, we'll also provide react-server-dom-esm/server with a moduleBasePath option. This allows you to control where the client modules can be loaded from the file system and used to shorten the paths in the RSC payload:
const moduleBasePath = new URL('../ui', import.meta.url).href
// moduleBasePath is something like `file:///Users/kentcdodds/code/epicweb-dev/react-server-components/playground/ui`
const { pipe } = renderToPipeableStream(payload, moduleBasePath)
With the full file path removed, we're left with the path to the module relative to our ui directory. Our hono.js server is configured to serve any file in ui via /ui.
Because react-server-dom-esm/client is responsible for loading that module for us, we need to tell it the base URL to load modules from via an option called moduleBaseURL:
createFromFetch(fetchPromise, {
	moduleBaseURL: `${window.location.origin}/ui`,
})
Then when react-server-dom-esm/client needs to load a module, it will fetch the module from ${window.location.origin}/ui/ship-search.js and import the ShipSearch export from it.
๐Ÿ“œ Relevant Docs: