Isomorphic Development

At first, I wanted to name this post "No, Server Components Don't Make Your Job Easier", but that wouldn't have covered it. It's not server components itself complicating our jobs. It's the isomorphic nature of it. The fact that the same code runs in both server and client, and that they're supposed to bridge the gap between them. Supposed to, because we're not there yet.

Sure, server components will make your react components look simpler. That's because server components come with strict regulations. Don't use hooks, or client state. Don't make them interactive. Guess how complex your client side react components would look, if you'd follow the same rules? Exactly, they'd look simpler too.

Every time someone tries to sell me the idea that moving code to the server makes our lives easier, they fail to explain how. With server components some go as far as saying that not seeing how they simplify stuff, "is the same resistance we showed against JSX years ago", and that "we just need to adapt our mental model". I can tell you, it's not. Moving anything from client to server, is nothing like moving from HTML or createElement to JSX.

I've been writing isomorphic code since Meteor.js was released back in 2012. So I want to be clear that I'm not against fullstack development, and I'm not even against isomorphic development, as I fully embrace its benefits. What I am against, is the false claim that moving parts of our code to the server, would make the job of a frontend developer easier.

Isomorphic Development: Bridging The Network

Let's clear out the first misconception. Frameworks like Next.js and Remix let you write server and client code neatly together, and you'll boot your environment with a single npm run dev command. That doesn't mean that it's one environment tho.

There's still a server, and still a client/browser bundle. In Next the server is code generated by extracting methods like getServerSideProps or the new functions decorated with 'use server' directives , and in Remix it's functions like loader and action. Server code runs on the server, Client code runs in the browser, but depending on your implementation it might also run on the server. Let that last one sink in for a moment. That's what this writing is about.

The thing to understand is that the build tools separate your server and client logic, and run each in their own environment. Server code on the server, client code in the browser, and sometimes, but not always, client code on the server. That, is what makes it not easier.

With this paradigm shift, we're not asking frontend developers to render their code on the server, we're asking them to write code that's runs safely, and performant, in both environments.

Debugging: Juggling Two Environments

With code split between the frontend and backend, you're suddenly managing two separate environments. It means watching two debuggers hitting breakpoints, each with its own context. Or for the console.loggers amongst us, you'll now have log statements in two consoles.

Also remember that your component might render twice. Once in the context of the server, and once in the context of the client. Might, because Next.js took a different direction than Remix. In Remix, your component will render twice. Once on the server, and then on the client during hydration (attach js handlers to make interactive). Next.js chose the route of not supporting state/interactivity on server components, so no hydration/rerender is needed there. Or at least, not for the server components, client components still need hydration.

Security: Protecting Boundaries

Fullstack developers must guard against data leaks and breaches. The server has access to sensitive information that clients shouldn't have. You'll now have to think about how to get those properties safely to the client.

Did you once store a variable on the module scope (out of your react component) as a form of cache between renders? Move that component from 'use client' to 'use server' during a refactor, and you'll leak information between users.

It might not be hard to fix, but it's an easy bug to introduce when your build tool decides where to run the code. Something to keep in mind and be aware of. At any time.

Performance: The Dual Nature

Performance behaves differently on the client and server. On the client, speed matters and we avoid browser bottlenecks. To use our time efficient, we often work under the principle "you don't run the function a thousand times, so fast is fast enough". Well, move that function from the client to the server, and scalability takes center stage. Functions that once ran only a couple of times, now run a couple times on every user request.

And remember, Node.js is single threaded. All sync code that runs on the server, block other requests being made. In the browser you don't notice a 10ms delay, on your server, it can cause requests to queue up and your AWS bill to grow.

Data Querying: Bridging The Gap

Querying data is a joint effort. Frontend make API calls, while backend tackles databases. Fullstack developers must master database performance, query optimizations, and indexing. Don't forget about rate limiting challenges when server interacts with external APIs. Where the client would have made a handful requests to an API from thousands of different origins, you're now making thousands requests from a single origin.

When caching responses for the sake of performance, be aware of where your query runs, for the sake of security. Depending on the query, you might or might not want to share your caches between requests.

Final words

My concern isn't about moving to fullstack development. I've been there for over 15 years. My concern is about the hybrid nature that we're moving into. Code runs in both environments, and boundaries are there, but not clearly visible. It's easy to slip up because we're busy, or because we're missing something during refactor.

Our tools aren't ready for this. We don't have an isomorphic debugger that hits breakpoints on both environments, and we don't have an isomorphic console that merges log statements from client and server. Eslint is merely a linter for formatting, and TypeScript won't warn you about leaking variables either. Last time I've checked, we even don't have a code coverage tool merging results from both sides.

And all of the above can be learned. But it's so much more than "just adapt your mental model". It's new knowledge, it's more knowledge, and it'll make your job harder. If you believe it doesn't, then you're either missing the risks, or you're underselling yourself.

While we're at it, maybe it's time to stop calling it fullstack development. It's no longer enough to be proficient in both environments. We need to start thinking of it like a single environment, even tho our tools don't expose it that way. How does isomorphic development sound?

Liked this article?

If you made it to here, please share your thoughts on Twitter!