Back

HTMX vs. Vue and React -- Pros and Cons

HTMX vs. Vue and React -- Pros and Cons

HTMX is a lightweight JavaScript library for building interactive web applications. It leverages HTML attributes, making its syntax more easily learned than alternate JavaScript frameworks. This article compares HTMX, Vue, and the React JavaScript frameworks and provides use cases that would influence a developer’s choice of JavaScript frameworks.

In modern web programming, various languages stand out as popular choices for developing high-performing and intuitive interfaces. Among the most popular languages are Vue and React. Different frameworks implement unique approaches in their development patterns and syntaxes, offer a range of pre-built features, and, as such, influence the choice of adoption.

HTMX empowers developers to craft interactive interfaces using the familiar HTML, CSS, and JavaScript trio. Notably, HTMX shines for its user-friendly approach and remarkable efficiency, eliminating the necessity for intricate JavaScript frameworks such as React or Vue while still delivering good user experiences.

React is a widely embraced JavaScript library renowned for its capability in crafting user interfaces, particularly for single-page applications. Developed by Facebook, React has garnered widespread adoption owing to its efficiency, flexibility, and component-based architecture.

Vue.js is a progressive JavaScript framework renowned for crafting dynamic user interfaces. Widely employed in developing single-page applications and dynamic web interfaces, Vue.js offers many features and concepts that make it a preferred choice among developers.

Growing interest in HTMX and its unique perspective on web development

There has been a noticeable surge in interest surrounding HTMX, which presents a unique perspective on web development. Its emergence has sparked curiosity and intrigue among developers for several compelling reasons.

  • Simplicity and Elegance: HTMX advocates for a straightforward and minimalist approach to web development. It enables developers to enhance interactivity and dynamic behavior in web applications using familiar HTML attributes, thereby streamlining the development process.
  • Progressive Enhancement: With HTMX, developers can embrace the concept of progressive enhancement, where basic HTML structures are augmented with dynamic features as needed. This approach ensures a robust foundation for accessibility and search engine optimization (SEO) while providing an enhanced user experience for modern browsers.
  • Efficiency and Performance: HTMX facilitates seamless Ajax requests and partial page updates without the need for complex JavaScript frameworks. By leveraging the browser’s native capabilities, HTMX minimizes the overhead associated with client-side processing, resulting in faster load times and improved performance.
  • Low Barrier to Entry: One of HTMX’s most appealing aspects is its accessibility to developers of all skill levels. By leveraging existing HTML knowledge, developers can quickly grasp HTMX’s concepts and start incorporating dynamic functionality into their projects with a minimal learning curve.
  • Modern Development Paradigm: HTMX aligns with modern development paradigms such as server-side rendering (SSR) and progressive web applications (PWAs). Its lightweight nature and focus on enhancing the user experience align well with the evolving web development needs in an increasingly interconnected digital landscape.
  • Community Engagement: As interest in HTMX grows, so does its community of developers and enthusiasts. Through collaborative efforts, knowledge sharing, and contributions, the HTMX community continues to evolve, driving innovation and expanding the library’s capabilities.

Core Differences

While HTMX, Vue.js, and React are all tools for creating dynamic web applications, their methods, ideologies, and techniques vary greatly. The following are some of their main differences.

Data Flow

  • HTMX: Follows a traditional server-rendered model where server responses typically drive data flow. It enhances server-rendered HTML with dynamic behavior through Ajax requests and partial page updates. As such, the data flow is primarily server-to-client.
  • Vue.js: Employs a reactive data-binding system, where changes to the data automatically propagate to the UI and vice versa. Vue.js supports one- and two-way data binding, allowing for flexible data flow between components and the application state.
  • React: Utilizes a unidirectional data flow, where data flows downwards from parent to child components via props and upwards via callback functions. React components manage their state, which can be passed down to child components as props. Additionally, React supports global state management solutions like Redux for managing application-wide state.

Code Complexity

  • HTMX: Offers simplicity and reduced code complexity by leveraging HTML attributes to define dynamic behavior. Developers can enhance existing HTML-driven applications without extensive JavaScript code or complex frameworks.
  • Vue.js: Strikes a balance between simplicity and power, providing features like the Vue Template Syntax and reactive data binding to facilitate dynamic UIs. Vue.js components are encapsulated and self-contained, reducing complexity by promoting modularity and reusability.
  • React: Creating components requires a deeper understanding of JavaScript and JSX syntax. While React promotes a component-based architecture, managing state and lifecycle methods can lead to increased complexity as applications grow in size and complexity. However, React’s component-based approach can also contribute to code organization and maintainability.

Learning Curve

  • HTMX: Offers a shallow learning curve, particularly for developers already familiar with HTML. Its simplicity and intuitive use of HTML attributes make it easy to grasp, allowing developers to quickly add dynamic behavior to existing applications.
  • Vue.js: Provides comprehensive documentation and a gentle learning curve, making it accessible to developers of varying skill levels. Vue’s progressive adoption model allows developers to start small and gradually explore more advanced features as needed.
  • React: Features a moderate to steep learning curve, especially for beginners or developers new to component-based development. Understanding JSX, component lifecycle, and state management may require time and practice. However, React’s popularity and extensive ecosystem provide ample learning resources and community support.

Strengths and Weaknesses

Pros of HTMX

  • Simplicity: HTMX emphasizes simplicity and minimalism, allowing developers to enhance existing HTML applications with dynamic behavior using familiar HTML attributes.
  • Progressive Enhancement: Supports progressive enhancement, enabling developers to add interactive features to server-rendered HTML without rewriting existing code or adopting complex JavaScript frameworks.
  • Server Interaction: Facilitates seamless server interaction through Ajax requests and partial page updates, enhancing the user experience without sacrificing server-side rendering or SEO.
  • Low Learning Curve: HTMX has a shallow learning curve, particularly for developers already proficient in HTML, making it accessible to many developers.

Cons of HTMX

  • Limited Features: HTMX may lack the extensive feature set and ecosystem offered by Vue.js and React, particularly for building complex single-page applications with advanced state management and routing needs.
  • Client-Side Dependencies: While HTMX reduces the need for extensive client-side JavaScript code, it still relies on JavaScript for dynamic behavior, which may introduce additional dependencies and complexity compared to pure server-side rendering.
  • Community and Support: While growing in popularity, HTMX may have a smaller community and fewer resources than established frameworks like Vue.js and React, potentially limiting access to documentation, tutorials, and community support.

Pros of React

  • Flexibility: React provides high flexibility and control over the application’s structure and behavior, making it suitable for building complex and dynamic user interfaces.
  • Performance: React’s virtual DOM and efficient diffing algorithm contribute to optimal performance by minimizing DOM manipulations and improving rendering speed.
  • Large Community and Ecosystem: React has a vast and active community, along with a rich ecosystem of third-party libraries, tools, and extensions, providing resources and support for building scalable and maintainable applications.
  • JSX: JSX simplifies development by allowing developers to write HTML-like syntax directly within JavaScript, promoting code readability and maintainability.

Cons of React

  • Complexity: React’s component-based architecture and ecosystem of tools can introduce complexity, especially for beginners or smaller projects that may not require its full feature set.
  • Learning Curve: React has a moderate to steep learning curve, particularly for developers new to component-based development or functional programming concepts like state management and immutability.

Pros of Vue

  • Reactivity: Vue.js offers a reactive data-binding system, making it easy to synchronize data between components and the UI, leading to more responsive and interactive applications.
  • Component-Based Architecture: Vue.js promotes a modular and reusable component-based architecture, facilitating code organization, maintainability, and scalability.
  • Versatility: Vue.js can build simple interactive components and complex single-page applications, thanks to its progressive framework design.
  • Strong Ecosystem: Vue.js has a robust ecosystem with a wide range of official and community-driven libraries, tools, and plugins for tasks like routing, state management, and UI components.

Cons of Vue

  • Complexity: While Vue.js balances simplicity and power, building complex applications with Vue.js may require understanding advanced concepts like Vuex for state management and Vue Router for routing.
  • Learning Curve: Vue.js has a moderate learning curve, especially for beginners or developers new to component-based development, compared to simpler libraries like HTMX.
  • Performance Overhead: Vue.js introduces a performance overhead for reactive data-binding and virtual DOM manipulation, which may impact performance in applications with many components or frequent updates.

Use Cases

In this section, we will contrast HTMX, React, and Vue, show use cases of their applications, and demonstrate their adoption benefits.

HTMX use case: enhancing Server-Side Rendered (SSR) applications

HTMX enables seamless and dynamic client-server interactions by adopting the use of Ajax requests with custom HTML attributes such as hx-get, hx-target, hx-trigger, etc. Using HTMX developers can create and handle requests to the server without the need for extensive JavaScript code.

HTMX is particularly well-suited for SSR applications as it embraces existing server-rendered content and enhances it incrementally. By integrating with the server-side rendering approach, HTMX minimizes the need for complex client-side frameworks and promotes a more straightforward development process. Fundamental features that enhance HTMX for Server-Side Rendering are given below:

  • Declarative AJAX Requests within HTML: As mentioned earlier, HTMX allows developers to declare AJAX requests directly within the HTML markup using custom attributes. By leveraging this code, developers can make requests to the backend, set up a target element to be updated by the response from the server, and even handle the fetching and result stage with just a few lines of code. Minimal JavaScript Code: HTMX employs a minimalistic approach to client-side scripting in SSR applications. With HTMX, the development process is simplified without reducing site performance, and as such, the framework has a simple learning curve.
  • Seamless Integration with Existing HTML: This is designed to be integrated incrementally, allowing developers to enhance specific elements or sections of an existing SSR-generated page without requiring a complete overhaul. This flexibility ensures compatibility with the current HTML structure, facilitating the adoption of HTMX in projects with established server-side rendering. It is also easily integrated with other frameworks for development, such as Astro and Next.js. Improved User Experience without Full Page Reloads: HTMX enhances the user experience through dynamic updates when changes are made without having to initiate a full page reload. With its dynamic update, the frontendfront end automatically reflects the responses from the backend server.
  • Lightweight and Fast Execution: The HTMX framework has a small bundle size of approximately 10 kb (GZipped and minified) compared to react (with react-dom) with ~50 kb in version 18.2. 0, and Vue 3.2. 37 with ~34 kb. Its simple learning curve gives developers the possibility to make functional applications in a short time, according to LabCodes.

React use case: building large-scale applications

React, a popular and widely adopted JavaScript framework, is particularly well-suited for building large-scale applications due to its architectural approach to development and features designed to efficiently handle application scalability and complexity.

This framework uses JSX (JavaScript XML), best described as HTML-like code within JavaScript, making it easy to understand and a widely adopted syntax. The following are features offered by React that facilitate the development of large-scale applications:

  • Component-Based Architecture: React’s component-based architecture is fundamental to building large-scale applications. Developers can use this architecture pattern to break down the user interface into modular, reusable components. Each component encapsulates its logic, styles, and functionalities, promoting code reusability and maintainability. This modular approach makes it easier to manage the complexity of large applications by organizing code in a more structured and scalable manner. Also, it makes it seamless to work with cross-functional teams during development.
  • Virtual DOM: React uses a virtual representation instead of directly manipulating the browser’s DOM. This virtual representation enables React to efficiently update only the necessary parts of the actual DOM, reducing rendering bottlenecks and enhancing the application’s overall responsiveness.
  • Reusable components and micro-frontend: The reusability of React components is crucial for large-scale applications. Components can be reused across different application parts, promoting consistency and reducing redundancy in the codebase. Additionally, React’s component-based architecture supports the implementation of micro-frontend architectures, allowing teams to work on and deploy independent, self-contained parts of the application. By leveraging this feature, a team of developers can develop, test, and maintain large, complex projects. Strong Community Support: According to a statistic taken in 2024 by Statista, React is extremely popular and the most widely used frontend JavaScript framework. React benefits from a robust and active community. The community-driven ecosystem provides many resources, including tutorials, how-to guides, third-party libraries, tools, and best practices. This strong support system is invaluable for developers working on large-scale applications, as it ensures access to solutions, updates, and a shared knowledge base. Unidirectional Data Flow: React enforces a unidirectional data flow, which is particularly beneficial for managing state in large-scale applications. Because the unidirectional flow guarantees predictable data changes, problems are simpler to track down and debug. This architecture simplifies the management of the application state, reducing the likelihood of bugs and enhancing the maintainability of large codebases. Cross-platform development with React Native: This feature makes React stand out among peer frontend frameworks. React Native allows developers to build and ship web and mobile applications using React without the need to learn a whole different programming language for mobile app development. For large-scale applications that require a presence on multiple platforms, React Native is a powerful extension of React. React Native enables developers to use React to build native mobile applications for iOS and Android platforms with a single codebase. This cross-platform compatibility streamlines development efforts, allowing teams to leverage their existing React knowledge to create consistent user experiences across web and mobile platforms.

Vue case case: Single Page Applications (SPAs)

Vue is exceptionally well-suited for building single-page applications (SPAs), where the entire application runs on a single HTML page, dynamically updating content as users navigate without requiring full page reloads.

Vue’s strength in this context lies in its reactive data binding, allowing seamless synchronization between the application state and the UI. The framework’s component-based architecture promotes modularity, making it easy to manage and scale SPAs by breaking down the application into reusable components. Also, Vue’s built-in router can seamlessly handle client-side navigation, creating a smooth and intuitive user experience. The following are some of Vue’s core features that make it an excellent choice for developing SPAs:

  • Vue Router for Client-Side Navigation: Vue Router is an essential feature in Vue’s use case for Single Page Applications (SPAs). It provides a powerful and flexible routing system that allows developers to manage client-side navigation seamlessly. Vue Router enables the creation of multiple views, and with its nested route capabilities, developers can easily design complex SPA layouts. This feature ensures a smooth and responsive user experience by dynamically updating the content based on the route changes without requiring a full page reload.
  • Declarative Rendering with Vue Directives: Vue’s use of declarative rendering with directives simplifies defining the user interface in SPAs. Vue directives, such as v-for for iterating over data and v-if for conditional rendering, allow developers to express the UI clearly and concisely directly within the HTML markup. Other examples of Vue directives are v-if, v-show, v-else, v-for, v-bind, v-model, and v-on. This declarative approach enhances the readability of code, making it more intuitive and easier to maintain, which is crucial in the context of SPAs. Additionally, Vue allows developers to create custom directives for use in their code base.
  • Reactive Data Binding: This feature empowers Vue to handle seamless UI updates and eliminates the need for manual DOM manipulation. Data binding in Vue uses the mustache syntax {{}}, and also allows special characters to be added to specify the attribute of the variable contained. For example, while the code <p>hello: {{ name }}</p> would display the text “hello” followed by the data stored in the variable name, adding an asterisk before the variable name:
<p>hello: {{ * name }}</p>

This makes the displayed information immutable and unchanged even when the value of name is updated. JavaScript code can also be written in data binding within the mustache syntax: e.g., a ternary operation can be specified with the following:

<p>hello: {{ age>18 ? 'adult' : 'kid' }}</p>

Furthermore, variables can be bound to elements using the v-model directive to dynamically update their value as shown below:

<script setup lang="ts">
import { ref } from "vue";
const message = ref("Hello World");
const userInput = ref("");
</script>

<template>
  <h1>{{ message }}</h1>
  <input v-model="userInput" placeholder="what is your name..." />
  <p>You typed: {{ userInput }}</p>
</template>

Here, the variable userInput is bound to the input field and reflects the data entered into it. upload_0cccd245ee748d3db3e941ac5cd3c575

  • Progressive Framework: Being progressive means developers can adopt Vue incrementally, choosing only the features they need for their project. This flexibility is valuable in SPAs, allowing developers to tailor Vue to the specific requirements of their application. Whether a small project or a large-scale SPA, Vue’s progressive nature adapts to diverse development needs.
  • Nuxt development tools: Nuxt.js is a framework built on top of Vue.js for developing powerful server-side rendering (SSR) and static site generation (SSG) focused web applications. Nuxt.js provides features such as layouts, middleware, and plugins that can enhance the development of SPAs. It simplifies the creation of complex SPAs by offering a convention-over-configuration approach and a modular system.

Working with HTMX

In this section, We will demonstrate setting up an HTMX application, creating a Node.js backend and API routes, and interfacing with the front end using HTMX requests.

Setting up an HTMX application

Open up a directory of your choice to create the HTMX application. In your chosen directory, open up a command-line environment and enter the following command:

# create a package.json file
npm init -y
# to install express.js to manage backend route 
npm install express
# install nodemon to handle automatic server reloads when changes are made
npm install -d nodemon

Creating server files and execution scripts

Once the installations are completed, open up the folder in your code editor and make the following changes to package.json:

{
  "name": "htmx-server-app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "start": "nodemon server.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.18.3",
    "nodemon": "^3.1.0"
  }
}

In the code block above, we specified the type to use module import methods rather than require. We also created a script start to automatically execute a server.js file using nodemon. This file will house the backend of our application. Next, create a server.js and add the following code:

import express from 'express';

// create an express app
const app = express();
// set up the server to serve static files from the 'public' directory
app.use(express.static('public'));
// set up the server to parse the body of the request from the client
app.use(express.urlencoded({ extended: true }));
// parse the body of the request from the client in JSON format
app.use(express.json());

// handle server start on port 8080
app.listen(8080, () => {
  console.log('Server listening on http://localhost:8080');
});

The code above sets up our server to use express, serve static files from a ‘public’ directory, and define how requests will be handled and the server’s port.

Adding HTMX and Tailwind support

To use HTMX, we would require an instance of the framework. For this article, we will be using the online CDN. In your project directory, create a new folder ‘public’ and add a file index.html to it containing the following:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>My HTMX</title>
    <script
      src="https://unpkg.com/htmx.org@1.9.10"
      integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC"
      crossorigin="anonymous"
    ></script>
  </head>
  <body>
    <h1>My HTMX Application</h1>
  </body>
</html>

In the code block above, we have a basic HTML page syntax and a script tag that imports HTMX via CDN. To make it easier to style our application, we will use Tailwind CSS, a frontend CSS library. This can also be added via a CDN in the head:

<!-- import tailwincd cdn -->
<script src="https://cdn.tailwindcss.com"></script>

Initiating GET requests with HTMX

To create a simple GET HTTP request, add the code below to the body element:

<body>
  <h1 class="font-bold text-2xl mb-12">My HTMX Application</h1>
  <button
    hx-get="https://jsonplaceholder.typicode.com/users"
    hx-headers='{"Accept": "application/json"}'
    hx-trigger="click"
    class="bg-slate-500 hover:bg-slate-700 text-white font-bold py-2 px-4 rounded-md"
  >
    Get random user info
  </button>
</body>

In the code above, we have a button with a property hx-get that initiates a GET request to https://jsonplaceholder.typicode.com/users, which returns random user information. We also specified the headers for the request using hx-headers and the trigger condition as a click event using hx-trigger.HTMX makes it seamless to specify how requests would be made and handled right from the element without needing JavaScript.

If we were to initiate the request, the response would override the current text in the button element. To execute the application, enter npm run start in the CLI and open the localhost URL in your browser. In the browser, you will have a result similar to the GIF below:

Specifying target HTMX elements

To pass the response from the request to another element for rendering, we will use the property hx-target.

<button
  hx-get="https://jsonplaceholder.typicode.com/users"
  hx-headers='{"Accept": "application/json"}'
  hx-trigger="click"
  hx-target="#info"
  class="bg-slate-500 hover:bg-slate-700 text-white font-bold py-2 px-4 rounded-md"
>
  Get random user info
</button>
<div id="info" class="mt-12"></div>

In the code above, we specified that the target element for the response should be any element with the id info. Hence, when we click on the button, the div element returns the data instead.

Handling SSR with Node, Express and HTMX

Alternatively, we can also make requests to a backend server in the HTMX application. In server.js, create the following route:

// api routes
app.get("/joke", async (req, res) => {
  const response = await fetch("https://icanhazdadjoke.com/", {
    headers: {
      Accept: "application/json",
    },
  });
  const data = await response.json();
  res.send(`<p>${data.joke}</p>`);
});

The code above creates an API route ‘joke’, which fetches a random dad joke and returns the response in a paragraph element. Back in the index.html file, we can make a GET request to the ‘joke’ route:

<body class="flex items-center justify-center flex-col pt-5">
  <h1 class="font-bold text-2xl mb-12">My HTMX Application</h1>
  <button
    hx-get="/joke"
    hx-target="#info"
    hx-indicator=".loading"
    class="bg-slate-500 hover:bg-slate-700 text-white font-bold py-2 px-4 rounded-md"
  >
    Get a Dad's joke
  </button>
  <p class="loading htmx-indicator animate-bounce text-lg mt-12">
    Fetching data
  </p>
  <div id="info" class="font-mono"></div>
</body>

The code above initiates a GET HTTP request to the ‘/joke’ route. Using HTMX, we were also able to easily add a loading placeholder that is displayed while the request awaits a response. Here, we used the property hx-indicator to target an element with a class loading. Also, the element has a class htmx-indicator, which ensures it only appears when the loading event is triggered. Note that since a button has a click event by default, we do not always need to specify a hx-trigger with the value click. The property hx-trigger can also take a variety of other values for its trigger event, such as:

  • Mouseover: This requests the element it is attached to when the mouse hovers over the element.
  • Click[ctrlKey]: This specifies that the condition for the trigger is the combination of a pressed control key and a mouse click.
  • Once: The trigger would only execute the event the first time the action is initiated. This extra modifier can be used in conjunction with other events. For instance, a hx-trigger attribute can be specified with the “click once” value.
  • Every 2s: This specifies that the trigger should be executed continuously every two seconds. It can also be pegged with an extra condition within square brackets.
  • Load: Triggered when the application loads.
  • Revealed: Triggered when the element it is assigned to scrolls into view.
  • Keyup: This listens for a key event and then performs the trigger operation.

If we open the application in the browser, we will get the following results: bandicam2024-03-0810-17-40-234-ezgif.com-video-to-gif-converter

Creating other request types with HTMX

With HTMX, we are not limited to just GET requests but can also make POST, PUT, PATCH, and DELETE requests. To initiate a POST request, we add the attribute hx-put and the target API route:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>My HTMX</title>
    <script
      src="https://unpkg.com/htmx.org@1.9.10"
      integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC"
      crossorigin="anonymous"
    ></script>
    <!-- import tailwincd cdn -->
    <script src="https://cdn.tailwindcss.com"></script>
  </head>
  <body class="flex items-center justify-center flex-col pt-5">
    <h1 class="font-bold text-2xl mb-12">A Put request</h1>
    <form
      hx-post="https://dummyjson.com/products/add"
      hx-target="#info"
      hx-indicator=".loading"
      class="flex flex-col items-center"
    >
      <input
        type="text"
        name="title"
        placeholder="product title"
        class="mb-4 p-2 border border-gray-300 rounded-md"
      />
      <button
        type="submit"
        class="bg-slate-500 hover:bg-slate-700 text-white font-bold py-2 px-4 rounded-md"
      >
        Submit
      </button>
    </form>
    <p class="loading htmx-indicator animate-bounce text-lg mt-12">
      Sending data
    </p>
    <div id="info" class="font-mono"></div>
  </body>
</html>

The code above makes a PUT request for a product title to Dummyjson and returns the product id and title in the response. bandicam2024-03-0512-22-32-768-ezgif.com-video-to-gif-converter

Similarly, we can make other forms of request by specifying the appropriate attribute: hx-put, hx-delete, or hx-patch, depending on the desired operation.

handling validations

Using HTMX, we can also carry out form validations to ensure the desired inputs are correct before executing API requests with the data.

<body class="flex items-center justify-center flex-col pt-5">
  <h1 class="font-bold text-2xl mb-12">Sending Product to Dummy JSON</h1>
  <form
    hx-post="https://dummyjson.com/products/add"
    hx-target="#info"
    hx-indicator=".loading"
    class="flex flex-col items-center"
  >
    <input
      type="text"
      name="title"
      placeholder="product title"
      class="mb-4 p-2 border border-gray-300 rounded-md"
      onkeyup="this.setCustomValidity(/[!@#$%^&*(),.]/.test(this.value) ? 'Special characters not allowed' : '')"
      hx-on:htmx:validation:validate="this.setCustomValidity(/[!@#$%^&*(),.]/.test(this.value) ? 'Special characters not allowed' : '')"
    />

    <button
      type="submit"
      class="bg-slate-500 hover:bg-slate-700 text-white font-bold py-2 px-4 rounded-md"
    >
      Submit
    </button>
  </form>
  <p class="loading htmx-indicator animate-bounce text-lg mt-12">
    Sending data
  </p>
  <div id="info" class="font-mono"></div>
</body>

In the code block above, we validated the input field using the hx-on attribute, which checks if the entry contains a special character. If this condition is true, we return as text telling the user “Special characters not allowed”. bandicam2024-03-0513-11-08-190-ezgif.com-video-to-gif-converter

Synchronizing HTMX request

Suppose two operations are intertwined such that the second request depends on the data returned by the first request. HTMX provides a feature that makes this process seamless, allowing the second element to wait until the first has completed its request.

<body class="flex items-center justify-center flex-col pt-5">
  <h1 class="font-bold text-2xl mb-12">Creating User Account</h1>
  <form
    hx-post="/create-user"
    hx-target="#info"
    hx-indicator=".loading"
    class="flex flex-col items-center"
  >
    <input
      type="text"
      name="fullname"
      placeholder="Enter full name"
      class="mb-4 p-2 border border-gray-300 rounded-md"
      hx-post="/generate-username"
      hx-sync="closest form:abort"
      hx-trigger="keyup changed delay:500ms"
      hx-target="#username"
      hx-vars="username"
      onkeyup="this.setCustomValidity(/[!@#$%^&*(),.]/.test(this.value) ? 'Special characters not allowed' : '')"
      hx-on:htmx:validation:validate="this.setCustomValidity(/[!@#$%^&*(),.]/.test(this.value) ? 'Special characters not allowed' : '')"
    />
    <div id="username" class="font-mono"></div>
    <button
      type="submit"
      hx-trigger="submit"
      class="bg-slate-500 hover:bg-slate-700 text-white font-bold py-2 px-4 rounded-md"
    >
      Submit
    </button>
  </form>
  <p class="loading htmx-indicator animate-bounce text-lg mt-12">
    Sending data
  </p>
  <div
    id="info"
    class="font-mono max-w-[500px] text-white bg-blue-600 rounded-md px-2"
  ></div>
</body>

In the code block above, we used the attribute hx-sync in the input element to check if the closest form element is executing a request. If this condition is true, it aborts the request in the input till the form has been resolved. We are using the input field to collect and validate a full name from a user and send it to an API route, which converts it to a username. The form element, on submission, will send the full value to the API route. In server.js, we can create these two routes:

// generate a username from the full name and add some random numbers
app.post("/generate-username", (req, res) => {
  const { fullname } = req.body;
  const username =
    fullname.replace(/\s/g, "").toLowerCase() + Math.floor(Math.random() * 100);
  res.send(`<p>${username} is your assigned username</p>`);
});

// create a new user account
app.post("/create-user", (req, res) => {
  const { fullname } = req.body;
  res.send(
    `Congratulations ${fullname}! You have successfully created an account.`
  );
});
  

The code block above defines operations for two API routes: “/generate-username” and “/create-user.” The first route removes spaces from the fullname value in the request and assigns random numbers to it. The second API route returns text informing the user that they have created an account. bandicam2024-03-0514-27-58-826-ezgif.com-video-to-gif-converter

Confirming requests

With HTMX, we can also mimic JavaScript prompts to allow users to verify an operation to initiate a request. To do this, we use the keyword hx-confirm on a parent element and pass in children with requests as shown below:

<body class="flex items-center justify-center flex-col pt-5">
  <h1 class="font-bold text-2xl mb-12">My HTMX Application</h1>
  <div hx-confirm="Are you sure?">
    <button
      hx-get="/reset-password"
      class="bg-slate-500 hover:bg-slate-700 text-white font-bold py-2 px-4 rounded-md"
    >
      Reset Password
    </button>
  </div>
</body>

The “Reset Password” button triggers the confirm prompt and will execute the request in the button if the selected prompt option is “Ok”. bandicam2024-03-0515-51-31-492-ezgif.com-video-to-gif-converter

Code comparison HTMX vs. Vue and React

In this section, we will set up a simple CRUD task list application using HTMX, Vue, and React and observe the differences between them.

HTMX

in the server.js file, add the following code:

import express from "express";
// import mongo dep
import { MongoClient } from "mongodb";
// atlas conn string
const uri =process.env.MONGODB_URI;
// new client
const client = new MongoClient(uri);

async function run() {
  try {
    await client.connect();
    console.log("Successfully connected to Atlas");
  } catch (err) {
    console.log(err.stack);
  } finally {
    await client.close();
  }
}
run().catch(console.dir);

// create an express app
const app = express();
// set up the server to serve static files from the 'public' directory
app.use(express.static("public"));
// set up the server to parse the body of the request from the client
app.use(express.urlencoded({ extended: true }));
// parse the body of the request from the client in JSON format
app.use(express.json());

// api routes
app.get("/gettasks", async (req, res) => {
  async function fetchData() {
    const client = new MongoClient(uri);
    try {
      await client.connect();
      const database = client.db("article");
      const collection = database.collection("tasklist");
      const data = await collection.find().toArray();
      const datalisthtml = data.map(
        (task) => `
          <div class="mt-8 flex font-medium gap-4 items-center font-mono">
            <h3 class="${task.done ? "line-through" : ""}">${task.task}</h3>
            <button hx-put="/updatetask?id=${task._id}&task=${task.task}&done=${task.done}" class="border ${task.done ? "bg-blue-600 text-white" : "border-blue-600 text-black"} px-6 py-2">
              ${task.done === true ? "completed" : "uncompleted"}
            </button>
            <button hx-delete="/deletetask?task=${task.task}" class="bg-red-600 text-white px-6 py-2">Delete</button>
          </div>`
        );
      const joinedhtml = datalisthtml.join("");
      res.send(joinedhtml);
    } 
    finally {
      await client.close();
    }
  }
  fetchData().catch(console.error);
});

app.post("/addtask", async (req, res) => {
  const { task } = req.body;
  async function addData() {
    const client = new MongoClient(uri);
    try {
      await client.connect();
      const database = client.db("article");
      const collection = database.collection("tasklist");
      const result = await collection.insertOne({ task, done: false });
      res.redirect("/gettasks");
    } finally {
      await client.close();
    }
  }
  addData().catch(console.error);
});

app.put("/updatetask", async (req, res) => {
  const { task, done } = req.query;
  let newdone = done === "true" ? false : true;
  async function updateData() {
    const client = new MongoClient(uri);
    try {
      await client.connect();
      const database = client.db("article");
      const collection = database.collection("tasklist");
      const filter = { task: task };
      const update = { $set: { done: newdone } };
      const result = await collection.updateOne(filter, update);
      console.log(`${result.modifiedCount} document(s) was/were updated.`);
    } finally {
      await client.close();
    }
  }
  updateData().catch(console.error);
});

app.delete("/deletetask", async (req, res) => {
  const { task } = req.query;
  async function deleteData() {
    const client = new MongoClient(uri);
    try {
      await client.connect();
      const database = client.db("article");
      const collection = database.collection("tasklist");
      const filter = { task: task };
      const result = await collection.deleteOne(filter);
      console.log(`${result.deletedCount} document(s) was/were deleted.`);
    } finally {
      await client.close();
    }
  }
  deleteData().catch(console.error);
});
// handle server start on port 8080
app.listen(8080, () => {
  console.log("Server listening on http://localhost:8080");
});

The code above connects the application to Mongodb and creates the API routes for requests. Since we need to map through data to render individual tasks, we use JavaScript to create the appropriate HTML elements and specify the possible requests with HTMX.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>My HTMX</title>
    <script
      src="https://unpkg.com/htmx.org@1.9.10"
      integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC"
      crossorigin="anonymous"
    ></script>
    <!-- import tailwincd cdn -->
    <script src="https://cdn.tailwindcss.com"></script>
  </head>
  <body class="flex items-center justify-center flex-col pt-5 gap-5">
    <h1 class="font-bold text-2xl mb-12">My HTMX Application</h1>
    <form
      hx-post="/addtask"
      hx-trigger="submit"
      hx-target="#task-list"
      class="flex items -center justify-center gap-5"
    >
      <input
        type="text"
        name="task"
        class="border-2 border-gray-300 p-2 rounded-md"
        placeholder="Add a task"
      />
      <button
        type="submit"
        class="bg-blue-500 text-white p-2 rounded-md hover:bg-blue-600"
      >
        Add Task
      </button>
    </form>
    <div
      class="flex items-center justify-center flex-col"
      hx-get="/gettasks"
      hx-trigger="load"
      hx-target="#task-list"
    ></div>
    <div id="task-list"></div>
  </body>
</html>

In the code block above, we have an input field that makes a POST request to create a new task and a div element that makes the GET request to fetch content on page load. We also have the target element for the returned data. HTMX made it possible to easily specify the type of requests to be performed by the input field, the task completion, and delete buttons with a short amount of code. bandicam2024-03-0717-06-39-627-ezgif.com-video-to-gif-converter

React

Using the React framework, we cannot easily specify request types in elements as in HTMX. Instead, we must collect user input with a state, create a request using a promise-based HTTP library such as Axios or Fetch, and return the response in the JSX.

import { useState, useEffect } from "react";
import "./App.css";

function App() {
  const [search, setSearch] = useState("");
  const [tasks, setTasks] = useState([]);

  useEffect(() => {
    fetchTasks();
  }, []);

  const fetchTasks = async () => {
    await fetch("http://localhost:8080/gettasks", {
      method: "GET",
    })
      .then((res) => res.json())
      .then((data) => {
        setTasks(data);
      });
  };

  const handleSubmit = async () => {
    await fetch("http://localhost:8080/addtask", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ task: search, done: false }),
    })
      .then((res) => res.json())
      .then((data) => {
        setTasks([...tasks, data]);
      });
    setSearch("");
  };

  const handleTaskChange = async (task) => {
    await fetch(
      `http://localhost:8080/updatetask?task=${task.task}&done=${task.done}`,
      {
        method: "PUT",
        headers: {
          "Content-Type": "application/json",
        },
      }
    ).then(window.location.reload());
  };

  const handleDelete = async (task) => {
    await fetch(`http://localhost:8080/deletetask?task=${task.task}`, {
      method: "DELETE",
      headers: {
        "Content-Type": "application/json",
      },
    }).then(window.location.reload());
  };

  return (
    <>
      <h1 className="font-bold text-2xl mb-12">My React Application</h1>
      <input
        type="text"
        name="task"
        value={search}
        onChange={(e) => setSearch(e.target.value)}
        className="border-2 border-gray-300 p-2 rounded-md"
        placeholder="Add a task"
      />
      <button
        onClick={handleSubmit}
        className="bg-blue-500 text-white p-2 rounded-md hover:bg-blue-600"
      >
        Add Task
      </button>
      <div className="flex items-center justify-center flex-col">
        {tasks.map((task) => (
          <div
            key={task._id}
            className="flex items-center justify-between flex-row gap-5 p-2 rounded-md mt-5"
          >
            <p className={`${task.done ? "line-through" : ""}`}>{task.task}</p>
            <button
              onClick={() => handleTaskChange(task)}
              className={`border ${
                task.done
                  ? "bg-blue-600 text-white"
                  : "border-blue-600 bg-white text-black"
              } px-6 py-2`}
            >
              {task.done === true ? "completed" : "uncompleted"}
            </button>
            <button
              onClick={() => handleDelete(task)}
              className="bg-red-600 text-white px-6 py-2"
            >
              Delete
            </button>
          </div>
        ))}
      </div>
    </>
  );
}

export default App;

In the code block above, we are making requests to API routes on a Nodejs backend. Compared to when we used HTMX, in React, we needed more lines of code to specify the request type and headers, the requested content, and how the response should be handled. bandicam2024-03-0800-49-29-168-ezgif.com-video-to-gif-converter

Vue

In this section, we will create a similar task list application in Vuejs.

<template>
  <div class="flex flex-col items-center justify-center h-screen">
    <h1 class="font-bold text-2xl mb-12">My Vue Application</h1>
    <div>
      <input
        type="text"
        v-model="search"
        class="border-2 border-gray-300 p-2 rounded-md"
        placeholder="Add a task"
      />
      <button @click="handleSubmit" class="bg-blue-500 text-white p-2 rounded-md hover:bg-blue-600">
        Add Task
      </button>
    </div>
    <div class="flex items-center justify-center flex-col">
      <div
        v-for="task in tasks"
        :key="task._id"
        class="flex items-center justify-between flex-row gap-5 p-2 rounded-md mt-5"
      >
        <p :class="{ 'line-through': task.done }">{{ task.task }}</p>
        <button
          @click="handleTaskChange(task)"
          :class="{
            'bg-blue-600 text-white': task.done,
            'border-blue-600 bg-white text-black': !task.done,
            border: true,
            'px-6 py-2': true
          }"
        >
          {{ task.done ? 'completed' : 'uncompleted' }}
        </button>
        <button @click="handleDelete(task)" class="bg-red-600 text-white px-6 py-2">Delete</button>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      search: '',
      tasks: []
    }
  },
  mounted() {
    this.fetchTasks()
  },
  methods: {
    async fetchTasks() {
      try {
        const response = await fetch('http://localhost:8080/gettasks', {
          method: 'GET'
        })
        const data = await response.json()
        this.tasks = data
      } catch (error) {
        console.error('Error fetching tasks:', error)
      }
    },
    async handleSubmit() {
      try {
        const response = await fetch('http://localhost:8080/addtask', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({ task: this.search, done: false })
        })
        const data = await response.json()
        this.tasks = [...this.tasks, data]
      } catch (error) {
        console.error('Error adding task:', error)
      }
      this.search = ''
    },
    async handleTaskChange(task) {
      try {
        const response = await fetch(
          `http://localhost:8080/updatetask?task=${task.task}&done=${task.done}`,
          {
            method: 'PUT',
            headers: {
              'Content-Type': 'application/json'
            }
          }
        )
        if (!response.ok) {
          throw new Error(`HTTP error! Status: ${response.status}`)
        }
      } catch (error) {
        console.error('Error updating task:', error)
      }
      this.fetchTasks()
    },
    async handleDelete(task) {
      try {
        const response = await fetch(`http://localhost:8080/deletetask?task=${task.task}`, {
          method: 'DELETE',
          headers: {
            'Content-Type': 'application/json'
          }
        })
        if (!response.ok) {
          throw new Error(`HTTP error! Status: ${response.status}`)
        }
      } catch (error) {
        console.error('Error deleting task:', error)
      }
      this.fetchTasks()
    }
  }
}
</script>

<style scoped>
* {
  font-family: 'Nunito', sans-serif;
  color: #000;
}
body {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  background: #fff;
}
</style>

Similar to Reactjs, we also had to create and specify the type of request to be made and its parameters using JavaScript, as opposed to specifying the request type and attributes using Ajax-like HTMX syntax in HTML, which offered a shorter and seamless means of creating requests. bandicam2024-03-0801-21-08-954-ezgif.com-video-to-gif-converter

Conclusion

In the course of this article, we discussed the HTMX, Vue, and React frameworks extensively, highlighting their differences, pros, and cons and also evaluating specific use cases to determine how they perform in such scenarios and the features provided by the framework, which would enhance its performance when chosen for such purpose.

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay