Closed jwbay closed 1 year ago
This won't work because it's fundamentally unsupported by react. you can't render a server component within a client component like this. Waku might have a method of doing something like this, but it's gonna be more involved than just rendering a component, because it requires sending a request to the server -- that's where the rendering needs to happen, and react has no way of doing that "automagically".
So, about those Next docs. The "nesting" they're referring to is about something like this:
function OuterServerComponent() {
return (
<ClientComponent someProp={<InnerServerComponent1>}>
<InnerServerComponent2 />
</ClientComponent>
)
}
Note that in this case the client component receives server components as children/props. that works fine. but it can't render them directly, i.e. you can't just do <InnerServerComponent2 />
from within a client component. this is a subtle but important distinction.
To understand why it's like this, we need to look at the RSC rendering process. Simplifying a lot, it goes something like this:
Server
<div className="foo" />
, nothing more to do there, we can just serialize that<React.ClientComponentPlaceholder id="src/FilePicker.tsx#FilePicker" props={YOUR_PROPS_HERE} />
, but there's no API like this, that's just an illustration)Client
<div>
), we can basically just pass that to React.createElement
, simple stuffimport()
the corresponding client-side JS and put that in the react tree instead. meaning that that imaginary <React.ClientComponentPlaceholder id="src/FilePicker.tsx#FilePicker" props={YOUR_PROPS_HERE} />
turns back into <FilePicker {...YOUR_PROPS_HERE} />
<FilePicker>
is actually rendered at all.You'll notice that your FilePicker gets rendered very late in this process -- really, at the very end of it, after the request is already completed. So then we'd need to immediately send a second request so that the server can RSC-render <FileRenderer path="file1.txt">
for us. This is basically a pretty heavy network waterfall, and AFAIU that's why the react team chose to not allow doing this directly. (and, of course, if the state changes, and we pick a new file, we need to do another request to render <FileRenderer path="file2.png" />
)
Now, like i said at the start, a framework can technically expose methods of doing something like this. In fact i think Waku already does it internally. but it'll be more involved than just rendering a component -- for one, displaying <FileRenderer path="...">
from a client component has to be asynchronous, because we have to call back to the server to render it. Also, the actual component code lives on the server, so we have to refer to the component by some id, same as we did with React.ClientComponentPlaceholder
. so essentially you'd need to do some variation of
<Suspense fallback="Waiting for rendered content...">
<Waku.GetFromServer
// we need to use some kind of component id that the server will understand
component="src/FileRenderer.tsx#FileRenderer"
props={YOUR_PROPS_HERE}
/>
</Suspense>
where an (imaginary) Waku.GetFromServer
wrapper has some internals that handle calling an endpoint like /_waku/render/FileRenderer?props=...
on your server + suspending while we wait for the rendered response. I don't know much about Waku's internals, so there might already be a method of doing something like this, but you need @dai-shi for that
Anyway, sorry that this is a bit long, but i don't think there's currently any official docs explaining this, and idk how to explain it w/o going into these technical details. I'll try and look for some @gaearon twitter threads on this, he's probably got something better
That's a great write-up, thank you! The constraints outlined make sense. I was under the impression RSC in general had more magic behind it, I guess 😆 Not that it's not already without some.
So a central tenant seems to be that server-sourced stuff can be "injected" into client components, but not imported and rendered.
I adopted this with my sample app by adding a server action/function that just reads the file, then passing that to and calling it from from a client component. This works well enough 👍
Wow, nice to see discussions go on here.
One note to add is we should interpret "use client" directive as "mark this file as client boundary". It's not about a client component. So is "use server".
I created a test app with
npm create waku@latest
to play around with RSC without the baggage of next.js. I ran into an issue trying to render server components inside client components, but it's quite possible I'm missing something, too. Here's the code:App.tsx
FilePicker.tsx
FileRenderer.tsx
That code renders like this:
However, if I uncomment the
fs
reference in FileRenderer to actually use "server" code inside FileRenderer, the UI fails with this error:Uncaught TypeError: fs.readFileSync is not a function
``` FileRenderer@http://localhost:3000/src/FileRenderer.tsx?t=1685134414661:21:19 renderWithHooks@http://localhost:3000/node_modules/.vite/deps/chunk-LJQUZ74A.js?v=1f8b7987:8179:35 mountIndeterminateComponent@http://localhost:3000/node_modules/.vite/deps/chunk-LJQUZ74A.js?v=1f8b7987:11861:21 beginWork$1@http://localhost:3000/node_modules/.vite/deps/chunk-LJQUZ74A.js?v=1f8b7987:12863:22 callCallback2@http://localhost:3000/node_modules/.vite/deps/chunk-LJQUZ74A.js?v=1f8b7987:14207:22 invokeGuardedCallbackImpl@http://localhost:3000/node_modules/.vite/deps/chunk-LJQUZ74A.js?v=1f8b7987:14232:24 invokeGuardedCallback@http://localhost:3000/node_modules/.vite/deps/chunk-LJQUZ74A.js?v=1f8b7987:14272:37 beginWork@http://localhost:3000/node_modules/.vite/deps/chunk-LJQUZ74A.js?v=1f8b7987:18290:36 performUnitOfWork@http://localhost:3000/node_modules/.vite/deps/chunk-LJQUZ74A.js?v=1f8b7987:17579:20 workLoopSync@http://localhost:3000/node_modules/.vite/deps/chunk-LJQUZ74A.js?v=1f8b7987:17405:30 renderRootSync@http://localhost:3000/node_modules/.vite/deps/chunk-LJQUZ74A.js?v=1f8b7987:17382:17 performConcurrentWorkOnRoot@http://localhost:3000/node_modules/.vite/deps/chunk-LJQUZ74A.js?v=1f8b7987:16861:83 workLoop@http://localhost:3000/node_modules/.vite/deps/chunk-LJQUZ74A.js?v=1f8b7987:193:50 flushWork@http://localhost:3000/node_modules/.vite/deps/chunk-LJQUZ74A.js?v=1f8b7987:172:22 performWorkUntilDeadline@http://localhost:3000/node_modules/.vite/deps/chunk-LJQUZ74A.js?v=1f8b7987:382:29 EventHandlerNonNull*node_modules/scheduler/cjs/scheduler.development.js/<@http://localhost:3000/node_modules/.vite/deps/chunk-LJQUZ74A.js?v=1f8b7987:403:11 node_modules/scheduler/cjs/scheduler.development.js@http://localhost:3000/node_modules/.vite/deps/chunk-LJQUZ74A.js?v=1f8b7987:450:9 __require@http://localhost:3000/node_modules/.vite/deps/chunk-B46TJ7UL.js?v=1f8b7987:3:50 node_modules/scheduler/index.js@http://localhost:3000/node_modules/.vite/deps/chunk-LJQUZ74A.js?v=1f8b7987:462:24 __require@http://localhost:3000/node_modules/.vite/deps/chunk-B46TJ7UL.js?v=1f8b7987:3:50 node_modules/react-dom/cjs/react-dom.development.js/<@http://localhost:3000/node_modules/.vite/deps/chunk-LJQUZ74A.js?v=1f8b7987:478:25 node_modules/react-dom/cjs/react-dom.development.js@http://localhost:3000/node_modules/.vite/deps/chunk-LJQUZ74A.js?v=1f8b7987:25920:9 __require@http://localhost:3000/node_modules/.vite/deps/chunk-B46TJ7UL.js?v=1f8b7987:3:50 node_modules/react-dom/index.js@http://localhost:3000/node_modules/.vite/deps/chunk-LJQUZ74A.js?v=1f8b7987:25933:24 __require@http://localhost:3000/node_modules/.vite/deps/chunk-B46TJ7UL.js?v=1f8b7987:3:50 node_modules/react-dom/client.js@http://localhost:3000/node_modules/.vite/deps/react-dom_client.js?v=1f8b7987:11:13 __require@http://localhost:3000/node_modules/.vite/deps/chunk-B46TJ7UL.js?v=1f8b7987:3:50 @http://localhost:3000/node_modules/.vite/deps/react-dom_client.js?v=1f8b7987:37:16 ```As a sanity check, some Next.js docs for server components mention this kind of interleaving explicitly: