DEV Community

Cover image for Separation of Concerns: Rethinking the Blend of HTML, JavaScript, and CSS
Blake Anderson
Blake Anderson

Posted on

Separation of Concerns: Rethinking the Blend of HTML, JavaScript, and CSS

The Evolution of Front-End Development

Once upon a simpler time in the web's history, developers adhered to a design principle that seemed almost sacred: the strict separation of HTML, JavaScript, and CSS. HTML handled structure, CSS took care of styling, and JavaScript was reserved for interactivity and logic. This was the standard architecture—a clear, tidy division where each technology stayed in its lane, making the code more modular and easier to understand.

But then, the world of web development changed. Enter React, a library that threw a wrench into the gears of this tidy approach. React introduced the concept of building interfaces with JavaScript at its core, using JSX (JavaScript XML) to define HTML-like elements right alongside JavaScript logic. This paradigm was radical at the time because it seemed to blend the traditionally separate concerns into one.

React's rationale? The idea that separating HTML, CSS, and JavaScript was an artificial constraint that led to unnecessary complexity. Why split them apart when they were so intimately related in terms of functionality? See the original discussion of this approach from Pete Hunt. This logic resonated with initially-reluctant developers, especially as it allowed the creation of highly interactive, state-driven applications in a more streamlined way. Soon, the concept of "components" became the new lingua franca of front-end development, and frameworks following this approach, like Angular and Vue, quickly adopted similar strategies.

Let’s take a closer look at React’s approach to this blended paradigm.

React in Action: The Blended Approach

Consider a React component that renders a simple user card with a name and some basic styling. Here's a snippet in JSX that mixes HTML-like syntax with JavaScript logic:

import React from 'react';

const UserCard = ({ name }) => {
  const handleClick = () => {
    alert(`Hello, ${name}!`);
  };

  return (
    <div style={{ border: '1px solid #ddd', padding: '10px', borderRadius: '5px' }}>
      <h2>{name}</h2>
      <button onClick={handleClick}>Say Hello</button>
    </div>
  );
};

export default UserCard;
Enter fullscreen mode Exit fullscreen mode

In this example, the HTML structure (<div>, <h2>, and <button>) is directly intertwined with JavaScript functionality (handleClick event handler) and even CSS styling (style prop). React developers would argue that this encapsulation into a single unit (a "component") provides clarity and cohesion because the logic, structure, and style of each component are bundled together.

Questioning the Status Quo: Is This Blending Ideal?

But is this really the best approach? Sure, React's methodology brings convenience and modularity at first glance, but let’s not ignore the underlying trade-offs. When we mix these technologies together, we often create components that are easier to develop but harder to maintain. Changes to the visual layout might now require a full rewrite of the JavaScript code or vice versa, leading to a tightly coupled mess rather than the loosely coupled elegance we originally aimed for.

More importantly, there’s a philosophical question to consider: Should software engineers be in charge of designing the visual components of an application? Even a seasoned coder might struggle to balance clean, maintainable code with the creativity and fluidity required for good UI/UX. The problem is exacerbated for small businesses, independent developers, and startups working on limited budgets. These teams need to iterate fast, pivot quickly, and maintain code that is not just functional but also visually engaging—all without breaking the bank. Are the tools we use today helping them achieve that?

The reality is that while React and its kin have undeniably transformed how we build web apps, they haven’t necessarily made them simpler or cheaper to develop. This complexity poses a valid reason to revisit the idea of separating concerns in a way that leverages modern advancements.

The Case for Reconsideration: Revisiting Separation of Concerns

HTML, JavaScript, and CSS were designed as independent technologies, each with its own purpose. There's an elegance to letting each technology do what it does best, instead of forcing them to play well together in a confined space. In practice, separating concerns allows for specialization—a different mindset is required to write efficient, scalable JavaScript than is needed to craft a visually stunning UI.

For instance, decoupling the logic from presentation allows developers to focus more effectively on clean code without constantly juggling aesthetic concerns. It could also mean that designers can iterate on the UI/UX independently without waiting for a developer to tweak components buried inside layers of JavaScript.

Fortunately, new technologies are emerging that can bridge the gap between the ease of components and the clarity of separation. Let’s explore two notable advancements that encourage a shift back to this design philosophy.

TypeScript: Real-Time Feedback and Flexibility in Contracts

One of the greatest strengths of TypeScript is not just its ability to enforce contracts but also the way it facilitates a seamless evolution of those contracts in real-time. As your application grows and requirements shift, you can modify the interfaces that define the interaction between your visual and functional elements, receiving immediate feedback on how these changes impact the rest of your codebase.

This real-time feedback loop is a game-changer. It means that when you adjust a component's interface, TypeScript's static analysis will highlight every instance in your code where this change affects other parts of your application. Whether you're adding new props, altering existing ones, or completely rethinking how a component should behave, TypeScript ensures that you won't accidentally break functionality without being promptly alerted.

To demonstrate this flexibility, let's expand on the earlier example of the PrimaryButton component. Imagine you decide to add an optional icon prop to the button interface:

interface ButtonProps {
  label: string;
  onClick: () => void;
  icon?: string; // New optional prop for an icon
}

const PrimaryButton: React.FC<ButtonProps> = ({ label, onClick, icon }) => {
  return (
    <button style={{ backgroundColor: 'blue', color: 'white' }} onClick={onClick}>
      {icon && <img src={icon} alt="icon" style={{ marginRight: '5px' }} />}
      {label}
    </button>
  );
};
Enter fullscreen mode Exit fullscreen mode

With this modification, TypeScript immediately checks all the places where PrimaryButton is used. If any instances of the component do not account for the new icon prop, TypeScript will notify you, ensuring that your UI logic remains consistent. This ability to adapt and evolve the contract between visual and functional elements while maintaining instant feedback helps bridge the gap between front-end design and back-end logic in a structured way.

Vue 3 and the Composition API: A Clean Separation in Action

Vue 3's Composition API is another tool that nudges us back toward a clear separation of concerns. It allows developers to organize code by logical feature instead of by lifecycle method, making the code both modular and intuitive. Let’s look at a simple Vue 3 example using the Composition API that keeps visual presentation and functional logic neatly compartmentalized:

<script setup>
import { ref } from 'vue';

const name = ref('Jane Doe');

function greetUser() {
  alert(`Hello, ${name.value}!`);
}
</script>

<template lang="pug">
.user-card
  h2 {{ name }}
  button(@click='greetUser') Say Hello
</template>

<style scoped>
.user-card {
  border: 1px solid #ddd;
  padding: 10px;
  border-radius: 5px;
}
</style>
Enter fullscreen mode Exit fullscreen mode

In this approach, the template is solely concerned with the layout and presentation, while the logic resides in the JavaScript block. The CSS is scoped to the component, keeping the styling distinct and separated from both the template and the logic. This reinforces the idea that Vue’s Composition API is not just about flexibility but also about bringing back some of that much-needed separation between concerns.

Comparing React and Vue 3: Contrasting the Component Implementations

Now that we have a deeper understanding of TypeScript’s capabilities, let’s compare the implementation of a component in React versus Vue 3, focusing on how each framework handles separation of concerns. By contrasting their approaches, we can identify the benefits of using Vue 3, especially in the context of maintaining clear boundaries between the visual presentation and application logic.

React Implementation

In the React example, we create a UserCard component that uses JSX to blend HTML-like syntax with JavaScript logic and inline styling:

import React from 'react';

const UserCard = ({ name }) => {
  const handleClick = () => {
    alert(`Hello, ${name}!`);
  };

  return (
    <div style={{ border: '1px solid #ddd', padding: '10px', borderRadius: '5px' }}>
      <h2>{name}</h2>
      <button onClick={handleClick}>Say Hello</button>
    </div>
  );
};

export default UserCard;
Enter fullscreen mode Exit fullscreen mode

Key Characteristics of the React Approach:

  • Blended Concerns: The component logic, visual presentation, and styling are tightly integrated within a single file. This can make it easier to understand the component in isolation but also harder to update individual aspects without touching the entire structure.
  • Inline Styling: Styling is often done inline or through JavaScript-based CSS-in-JS solutions, which can lead to less separation and more difficulty in maintaining a consistent design system.
  • Tight Coupling: Changes to the visual layout may require updates to the JavaScript code and vice versa, increasing the maintenance complexity.

Vue 3 Implementation with the Composition API

Now let’s revisit the Vue 3 example, which uses the Composition API to achieve a more modular separation:

<script setup>
import { ref } from 'vue';

const name = ref('Jane Doe');

function greetUser() {
  alert(`Hello, ${name.value}!`);
}
</script>

<template lang="pug">
.user-card
  h2 {{ name }}
  button(@click='greetUser') Say Hello
</template>

<style scoped>
.user-card {
  border: 1px solid #ddd;
  padding: 10px;
  border-radius: 5px;
}
</style>
Enter fullscreen mode Exit fullscreen mode

Key Benefits of the Vue 3 Approach:

  • Clear Separation: The component logic, visual structure, and styles are clearly separated into distinct blocks (<script>, <template>, and <style>). This separation makes it easier to manage each aspect independently, promoting cleaner code organization.
  • Scoped Styling: Vue's scoped CSS ensures that styles are specific to the component, reducing the risk of global style conflicts and keeping the design modular.
  • Reactive Data Handling: Vue's reactivity system handles state changes elegantly without coupling it too tightly to the template, which allows developers to focus on updating data without constantly thinking about DOM manipulations.
  • Flexibility and Readability: The Composition API allows for a more organized and readable code structure, where logic can be easily shared and reused across components without becoming convoluted.

React vs. Vue 3: A Clear Divide in Philosophy

While React's approach focuses on bringing everything into a single, componentized unit, it often results in blending concerns to the point where logic, structure, and styling are entangled. This can lead to complications when the requirements for one aspect change independently of the others.

Vue 3, on the other hand, provides a cleaner separation of these aspects right out of the box. The Composition API enables better-organized logic, while scoped CSS and the template system ensure that each layer of the component remains distinct. This not only makes individual components easier to maintain but also encourages a better separation of developer and designer roles—allowing both to work in harmony without stepping on each other's toes.

An Advanced Scenario: A Dynamic Multi-Section Form with Validation

Imagine we're building a multi-step registration form using React. The form has several sections, each requiring specific validation, visual feedback (like error messages and input highlighting), and conditional rendering based on the user's progress. This kind of setup can quickly become convoluted in a React-based implementation when we try to handle logic, presentation, and styling all within the same component.

React Implementation: The Blending Problem

Here’s a simplified example of a React component that handles a multi-step form, including the logic for validation and styling:

import React, { useState } from 'react';

const MultiStepForm = () => {
  const [step, setStep] = useState(1);
  const [formData, setFormData] = useState({ username: '', email: '', password: '' });
  const [errors, setErrors] = useState({});

  const handleNextStep = () => {
    const validationErrors = validateForm(step);
    if (Object.keys(validationErrors).length === 0) {
      setStep(step + 1);
    } else {
      setErrors(validationErrors);
    }
  };

  const handleChange = (e) => {
    setFormData({ ...formData, [e.target.name]: e.target.value });
  };

  const validateForm = (currentStep) => {
    const errors = {};
    if (currentStep === 1 && !formData.username) {
      errors.username = 'Username is required';
    }
    if (currentStep === 2 && !formData.email) {
      errors.email = 'Email is required';
    }
    if (currentStep === 3 && formData.password.length < 6) {
      errors.password = 'Password must be at least 6 characters';
    }
    return errors;
  };

  return (
    <div style={{ padding: '20px', border: '1px solid #ddd' }}>
      {step === 1 && (
        <div>
          <label>
            Username:
            <input
              type="text"
              name="username"
              value={formData.username}
              onChange={handleChange}
              style={{
                borderColor: errors.username ? 'red' : '#ddd',
              }}
            />
          </label>
          {errors.username && <span style={{ color: 'red' }}>{errors.username}</span>}
        </div>
      )}
      {step === 2 && (
        <div>
          <label>
            Email:
            <input
              type="email"
              name="email"
              value={formData.email}
              onChange={handleChange}
              style={{
                borderColor: errors.email ? 'red' : '#ddd',
              }}
            />
          </label>
          {errors.email && <span style={{ color: 'red' }}>{errors.email}</span>}
        </div>
      )}
      {step === 3 && (
        <div>
          <label>
            Password:
            <input
              type="password"
              name="password"
              value={formData.password}
              onChange={handleChange}
              style={{
                borderColor: errors.password ? 'red' : '#ddd',
              }}
            />
          </label>
          {errors.password && <span style={{ color: 'red' }}>{errors.password}</span>}
        </div>
      )}
      <button onClick={handleNextStep}>Next</button>
    </div>
  );
};

export default MultiStepForm;
Enter fullscreen mode Exit fullscreen mode

Challenges in This React Implementation

  1. Mixed Concerns: This component includes state management, validation logic, UI rendering, and even inline styles—all in the same file. This blending of concerns makes it difficult to reason about the code, especially as the complexity of the form grows.

  2. Scaling Issues: As the form evolves (e.g., more fields, more complex validation rules), it becomes harder to modify one aspect (such as the validation logic) without potentially affecting the others. This tightly coupled code can lead to "spaghetti code," where changes in one area inadvertently impact others.

  3. Styling Maintenance: The inline styles used for error handling and input feedback make it harder to maintain a consistent design system. Inline styling can also negatively impact performance when dealing with larger applications because styles have to be recalculated on each render.

  4. Testability: Testing becomes a nightmare because the validation logic is deeply embedded in the UI component. It’s difficult to isolate the logic for unit testing, making debugging and maintenance challenging.

Vue 3 Implementation: Clean Separation of Concerns

Now, let's compare this with a Vue 3 implementation using the Composition API, which inherently promotes a cleaner separation of concerns.

<script setup>
import { User } from 'types';
import { ref } from 'vue';

const step = ref(1)
const formData = ref < User > ({ username: '', email: '', password: '' })
const errors = ref({})

const validateForm = (currentStep) => {
  const validationErrors = {};
  if (currentStep === 1 && !formData.username) {
    validationErrors.username = 'Username is required';
  }
  if (currentStep === 2 && !formData.email) {
    validationErrors.email = 'Email is required';
  }
  if (currentStep === 3 && formData.password.length < 6) {
    validationErrors.password = 'Password must be at least 6 characters';
  }
  errors.value = validationErrors;
  return Object.keys(validationErrors).length === 0;
};

const handleNextStep = () => {
  if (validateForm(step.value)) {
    step.value += 1;
  }
};
</script>

<template lang="pug">
.form-container
  div(v-if='step === 1')
    label
      | Username:
      input(type='text' v-model='formData.username' :class="{ 'error': errors.username }")
    span.error-message(v-if='errors.username') {{ errors.username }}
  div(v-if='step === 2')
    label
      | Email:
      input(type='email' v-model='formData.email' :class="{ 'error': errors.email }")
    span.error-message(v-if='errors.email') {{ errors.email }}
  div(v-if='step === 3')
    label
      | Password:
      input(type='password' v-model='formData.password' :class="{ 'error': errors.password }")
    span.error-message(v-if='errors.password') {{ errors.password }}
  button(@click='handleNextStep') Next
</template>

<style scoped>
.form-container {
  padding: 20px;
  border: 1px solid #ddd;
}

.error {
  border-color: red;
}

.error-message {
  color: red;
}
</style>
Enter fullscreen mode Exit fullscreen mode

Benefits of the Vue 3 Approach

  1. Logical Separation: Vue 3's Composition API keeps the logic (<script setup>), template (<template>), and styling (<style scoped>) separate, making the code easier to read and maintain. You can adjust the validation rules or update the UI without diving into a single monolithic file.

  2. Scoped Styling: The use of scoped styles ensures that styles defined for this form component do not unintentionally bleed into other parts of the application, maintaining a modular design system.

  3. Reactivity System: Vue’s reactive state handling (ref and reactive) allows for a more intuitive way to manage data and automatically trigger updates in the UI without overly complex state management logic.

  4. Improved Testability: With Vue 3, it's easier to isolate and test the logic independently of the template. The validation logic, for example, can be extracted into standalone functions that can be unit tested separately, enhancing code reliability.

  5. Declarative Syntax: Vue’s template syntax is more declarative, making it clearer to read and understand what the component does. You can easily identify data bindings, event handlers, and state-dependent conditions.

Why Vue 3 Maintains Better Separation of Concerns

Vue 3 shines in situations where separation of concerns is essential because its design naturally encourages developers to keep logic, presentation, and styling in well-defined boundaries. In contrast, React's flexibility and "everything is JavaScript" philosophy can lead to tight coupling between these aspects, making complex components difficult to manage.

React favors integration, often at the cost of entangling code, while Vue encourages clear demarcation between the layers of an application, which ultimately results in code that's easier to understand, debug, and scale.

In the end, the choice between React and Vue 3 comes down to your project’s requirements and your team's preference for code organization. But if maintaining a clean, modular architecture is a priority, Vue 3's structure often leads to a more maintainable and scalable codebase.

Separation Without Compromise

The tools we have today are powerful enough to allow us to revisit the principle of separating HTML, CSS, and JavaScript while still reaping the benefits of a component-based architecture. TypeScript gives us the assurance of strict contracts, reducing the potential for errors, while Vue 3’s Composition API allows us to organize code in a way that respects the boundaries between visual presentation and functional logic.

The modern landscape of web development offers us a chance to blend the best of both worlds: the modular, intuitive nature of components with the clarity and maintainability of separated concerns. We might have embraced blending HTML, JavaScript, and CSS with open arms, but perhaps it’s time to consider stepping back—to let each technology do what it does best, in its own rightful space.

This article represents my personal opinion on the Separation of Concerns and the choice among my teams to use Vue 3 as a framework for more maintainable websites and apps. As this is a controversial topic and React is a popular choice among other dev shops, I would love to hear your thoughts in the comments below.

Top comments (2)

Collapse
 
psiho profile image
Mirko Vukušić

I just can't agree that separation improves maintainability. I think its vice versa. Separate css is easier to write, but harder to maintain.

Collapse
 
blakeanderson profile image
Blake Anderson

I am actually pretty confused by the common practice of inlining styles, because it seems antithetical to what CSS is intended to do. Sure, there are rare times when inlining makes sense, but I see at least three advantages to separating CSS out. First, it improves component reuse, since you can create general solutions in different apps or contexts. Second, it consolidates style choices and improves site-wide consistency. Third, it allows a designer/non-coder to make visual changes without digging through code blocks.