My favorite example is an API table which shows documentation for UI component or software of any kind. To make everything readable and beautiful, we are sometimes rendering markdown with code blocks inside, but markdown libraries are chunky and often end up in your client bundle. It's even worse if you want to syntax highlight thing for the code blocks.
What I want to be able to do is render a markdown cell without affecting the client code. I know there are some other ways to achieve this, but the most straightforward implementation for me would be something like this as passing a specialized cell which does all the job without negatively affecting the client bundle.
Now, let's deep dive into how we can achieve this by using a pattern which I have called a polymorphic component, a component which can be can render any of its nodes both on the client or the server. To achieve this, we must simply follow some basic rules. First, components should not be rendering their own children. And second, their client logic should be separated.
With those two simple rules, we would be able to render custom components across the whole component tree without breaking client functionality and still be able to morph into a fully client component for application which does not support or require server components. If we apply those rules correctly, the rendering tree should look something like this. Each node in the tree is agnostic, meaning it could be rendered both as a client or as a server component. Additionally, the client parts of a component are extracted into a client shell where they are not affecting the rendering of the original component.
Now let's go over the first rule of not being responsible for your own children in the context of UI components of course. If previously our root data table components was directly rendering the role, we are now introducing an intermediate component. I still haven't found a good name for those, but for now I'm calling them view components. The idea of a view component is to assemble the actual slot which can be customized, in our case the role, and prepare its children so that if the role needs to be either a client or a server component, its children, the cells, would not be affected by this decision, allowing them to be anything according to the application requirements.
Referring back to our use case with the markdown cell, the rendering pattern would allow us to render a server component in a leaf regardless of any other customization which we have made up in the tree. Now to our second rule. Eventually, you or your product manager would want to add a simple role selection feature and wouldn't care much about the performance implication if we bring the whole component to the client.
When I was first playing with server components, I thought that I could simply add anything from React to a component and it would work on the server instead of the client. But if you try to add the state to a component, which might be rendered on the server, like our agnostic parts of the data table, you would quickly end up with an error. The same goes to attaching DOM events, because how else would you know how to change the state? So, what we are seeing on the screen now does not work.
In order to make it work, we must extract all client logic into a separate component. Again, the best name I have come up with is adding a client at the end of the component's name. This allows us to work around the rendering of the component without affecting the tree directly and only providing the client context. This allows us to consume this context from the client's part of our other components. Again, only adding client-specific logic.
In our use case, that would be the role's client part, which must only add a click event and append a class name if the client state indicates that the role is selected. Zooming out to see the full picture, our port of mobile components look something like this, rendering from left to right and its client logic being extracted into a separate component with the use client directive.
Comments