Separation of concerns has long been regarded as a good design principle for organizing your codebase. Personally, in my quest for writing clean code, for a long time, I tried to adhere to this principle when creating frontend web apps, while also trying to keep my development experience at a consistently high level.
Fast forward to today and it’s been a while since I decided to no longer follow this principle. I’m still focused on writing clean code, I now value more development experience and code that’s easy to scale, maintain and evolve over time. I’ve come to a difficult and controversial conclusion - separation of concerns actually slows you down.
Separation of concerns in frontend web development
As a web developer, the most common and well-known example of separation of concerns is in the UI - HTML for markup, CSS for styles, and JS for interactivity. This “golden standard” of concern separation was heavily shaken by modern JS toolings such as UI frameworks and compilers.
React through JSX brought markup right into your JS code. This was and still is a big reason why many developers are against it.
const Button = (props) => {
return (
<button onClick={(event) => props.handleClick(event)}>
{props.children}
</button>
);
};
You might argue that other frameworks, such as Vue, did the same thing, just in a different way. However, Vue’s Single File Components (SFCs) still adhere to this principle. While the code for all elements of the component resides in the same file, styling, markup, and JS logic are still separated into clear sections. It’s important to remember that separation of concerns doesn’t necessarily equal separation of code into separate files.
<script setup>
import { ref } from 'vue'
const content = ref('Hello World!')
</script>
<template>
<button class="btn">{{ content }}</button>
</template>
<style>
.button {
background: #333;
color: #fff;
padding: 0.25rem 0.5rem;
border-radius: 1rem;
}
</style>
Now, a similar process has been happening with CSS. Thanks to CSS-in-JS and, more recently, utility CSS solutions like Tailwind CSS, the process of styling also moved into JS code. Combined with JSX, your entire component can now be a single file, where all elements of a component intertwine.
const Button = (props) => {
return (
<button
className="bg-gray-200 hover:bg-gray-300 text-gray-800 px-2 py-1 rounded-lg"
onClick={(event) => props.handleClick(event)}
>
{props.children}
</button>
);
};
Pros and cons of concern separation
Some might look at tools like JSX or Tailwind CSS and say it’s a highway to code spaghetti. They wouldn’t be wrong. Forgoing the separation of concerns completely and building giant-size components can quickly lead to code that’s hard to read and difficult to understand, building up technical debt over time.
However, just because it’s easy to do it wrong, doesn’t mean it can’t be done right. I’d say these modern tools force us to look at web development differently. To see components as complete building blocks, rather than just groups of styles, markup, and code.
When done right, I’d argue JSX and utility CSS (or CSS-in-JS) can speed up development, improve the developer experience and even lead to more organized code. You no longer have to switch “contexts” in your mind or files in your editor to go between markup or styles. You can see components in a new light, optimizing them to serve their intended purpose. But, over all else - you don’t have to name things as often.
If you’ve worked long enough as a software developer long enough you’ll know this job comes down to two things - solving problems and naming things - where the latter is the most difficult. Combining JSX with utility CSS means dramatically less naming required for CSS classes, element IDs, etc. This translates to even further increased developer productivity.
Different kind of separation of concerns
Now, forgoing “standard” separation of concerns doesn’t mean putting everything in a single file and calling it a day. I’ve already touched on why it’s the wrong path and how it can lead you to completely unmaintainable code. Instead, you should approach your code organization from a point of components and business logic.
Extract the smallest parts of your UI - especially ones that can be reused - and organize them correctly. I like to think it’s a more natural way to organize your app’s codebase. Thinking about components, different parts of your UI, their responsibilities, and functions seems more natural than artificially separating styles and markup just for the sake of it.
In addition to that, any complex logic that’s not directly related to the component (i.e. doesn’t change the state of the UI), such as API client, data processing, utility functions, etc., should also be separated whenever possible. In general, any business logic that doesn’t need to directly interact with the UI should be extracted and properly modularized.
Following these two, rather simple rules should lead you to a different kind of concern separation. One where concerns are separated based on their functions and responsibilities in relation to your codebase. One that should come to you more naturally and should improve your productivity rather than slow you down.
Conclusion
Now, the fact is, if you already have strong opinions or work with a framework that imposes a specific approach to the separation of concerns, this post likely won’t make you change your mind. However, if you obsess over clean code, and are trying many different approaches, frameworks, etc., then maybe this convinced you to try something different.
For the time being, this is how I approach web development on pretty much every project I have “architectural control” over. That’s how I worked with Solid.js and Tailwind CSS for the past 2 years. That’s how Vrite is being built. Has worked pretty well so far…