Mixing client and server code is the new paradigm in React with Server Components. With the “use server” directive, you can run components exclusively on the server. This means that you can do things like write asynchronous database queries right in the component code. You might even mix SQL or a different language right in your JSX code.
Some thoughts on the benefits and drawbacks of this architecture.
Why is this good?
- More performant (if used correctly). The naive way to deliver modern web applications in React was to serve a large JavaScript bundle, render a shell layout, perform a data fetching request to hydrate the page, and then render the content. Users stared at a blank page until the JavaScript was downloaded, and then a shell under the data was fetched. Some frameworks made optimizations to this, sending the shell HTML first along with the JavaScript. Users at least saw a general layout quickly rather than a blank page. But there was still lots of chatter between client and server before the data was fetched and rendered.
- Colocated code (no context switching). TailwindCSS is popular partly because it allows frontend developers to write CSS in the same files as their other code. I imagine the same will be true of melding code normally reserved for backends into the “frontend”. Quicker iteration time and fewer back-and-forth between developers working on different parts of the codebase. Iterate on the API in tandem with the frontend code (which is normally the case anyway).
- React Component is the new API. This means it might be easier for companies to ship rich components that include server routes. You couldn’t really do this before. For example, maybe a form submission that interacts with an external API with an API Key that isn’t exposed to the client. Before, you would have to import the frontend client code as well as set up an API route to proxy the request with the API Key. Now, you can just import the component and be done.
Why might this cause problems?
- Is this a client or server component? Every component is run on the server by default (even though the “use client” directive refers to how all components used to work). It forces developers to think through where the code is running. This is confusing because it doesn’t actually abstract away any of the complexity that’s normally reserved for a runtime.
- Extends to dependencies. The server component feature is viral — it touches not only the code getting written but all of the dependencies. This is going to lead to a lot of refactoring and difficult migrations that are difficult for library maintainers to support. Server components add another layer to the decision tree of how you organize your components.
- Separation of concerns. Just because you can, doesn’t mean you should. Having the client/server boundary in separate “applications” (languages, folders, projects, deployments) is something that keeps code healthy. Mixing client and server code leads to spaghetti code if you aren’t careful (almost by definition).
- Complicated render pipeline to debug. Things should work, but when they don’t, it will be more difficult to debug. Developers will have trouble debugging where things are rendering. With streaming server-side rendering and Suspense, developers will also have trouble figuring out when things are rendering. Combining both is bound to lead to headaches.
- Implicit infrastructure. Not necessarily a bad thing, but this turns React into a framework that assumes more about what infrastructure it’s running on. Higher-level frameworks like NextJS have already been doing this for years. It’s hard to tell where the cracks will show for this, but it’s bound to have consequences for yet-to-be-seen architectures and environments.