When I first started building websites, JavaScript was mostly about adding simple interactivity to static pages. You’d write a few lines to handle button clicks or form validations, and that was it. But as web applications grew more complex, this approach became messy and hard to maintain. That’s where JavaScript frameworks came in. They provide a structured way to build dynamic, interactive user interfaces without getting bogged down in manual DOM manipulations. Over the years, I’ve worked with many of these tools, and I’ve seen how they transform the development process. In this article, I’ll walk you through seven frameworks that are changing how we build for the web today. I’ll explain each one in simple terms, with plenty of code examples to show how they work in practice.
Let’s begin with React. Developed by Facebook, React introduced a component-based architecture that lets you build UIs as collections of reusable pieces. Instead of directly manipulating the browser’s DOM, you describe what the UI should look like using JSX, a syntax that mixes HTML with JavaScript. React then handles updating the DOM efficiently through a virtual DOM, which is a lightweight copy of the real DOM. This means changes are applied in a smart way, reducing performance bottlenecks. One thing I appreciate about React is its use of hooks, which allow functional components to manage state and side effects. For example, useState lets you add state to a function component, and useEffect handles operations like data fetching. Here’s a more detailed example showing a todo list component:
import React, { useState } from 'react';
function TodoList() {
const [todos, setTodos] = useState([]);
const [inputValue, setInputValue] = useState('');
const addTodo = () => {
if (inputValue.trim()) {
setTodos([...todos, { id: Date.now(), text: inputValue }]);
setInputValue('');
}
};
const removeTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
return (
<div>
<h2>My Todo List</h2>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="Add a new todo"
/>
<button onClick={addTodo}>Add</button>
<ul>
{todos.map(todo => (
<li key={todo.id}>
{todo.text}
<button onClick={() => removeTodo(todo.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
}
export default TodoList;
In this code, we use useState to manage the list of todos and the input field’s value. The component re-renders whenever state changes, but React optimizes this by only updating the parts that changed. I’ve found this approach makes it easier to reason about complex UIs, especially when working in teams. The ecosystem around React is vast, with libraries for routing, state management, and more, which speeds up development significantly.
Next up is Vue.js. Vue stands out for its gentle learning curve and flexible design. It combines reactivity with a template syntax that feels familiar if you know HTML. Vue’s core library focuses on the view layer, but it scales well to full-featured applications. One feature I like is the composition API, introduced in Vue 3, which lets you organize code into reusable functions. This is great for keeping logic separate and testable. Here’s an example of a shopping cart component using the composition API:
import { ref, computed } from 'vue';
export default {
setup() {
const items = ref([]);
const newItemName = ref('');
const newItemPrice = ref(0);
const addItem = () => {
if (newItemName.value.trim() && newItemPrice.value > 0) {
items.value.push({
id: Date.now(),
name: newItemName.value,
price: newItemPrice.value
});
newItemName.value = '';
newItemPrice.value = 0;
}
};
const totalCost = computed(() => {
return items.value.reduce((sum, item) => sum + item.price, 0);
});
const removeItem = (id) => {
items.value = items.value.filter(item => item.id !== id);
};
return {
items,
newItemName,
newItemPrice,
addItem,
totalCost,
removeItem
};
},
template: `
<div>
<h2>Shopping Cart</h2>
<input v-model="newItemName" placeholder="Item name" />
<input type="number" v-model.number="newItemPrice" placeholder="Price" />
<button @click="addItem">Add Item</button>
<ul>
<li v-for="item in items" :key="item.id">
{{ item.name }} - ${{ item.price }}
<button @click="removeItem(item.id)">Remove</button>
</li>
</ul>
<p>Total: ${{ totalCost }}</p>
</div>
`
};
In this example, ref is used for reactive data, and computed creates a derived value that updates automatically. The v-model directive handles two-way data binding for form inputs, which saves a lot of boilerplate code. I’ve used Vue in projects where rapid prototyping was key, and its intuitive nature helped onboard new developers quickly. The tooling, like Vue DevTools, also makes debugging straightforward.
Angular is a full-fledged platform maintained by Google, ideal for large-scale applications. It uses TypeScript by default, which adds static typing to JavaScript, helping catch errors early. Angular’s dependency injection system promotes modularity and testability. When I worked on enterprise projects, Angular’s built-in solutions for routing, forms, and HTTP requests reduced the need for external libraries. Here’s a component that fetches and displays user data:
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
interface User {
id: number;
name: string;
email: string;
}
@Component({
selector: 'app-user-list',
template: `
<h2>User List</h2>
<ul>
<li *ngFor="let user of users">
{{ user.name }} ({{ user.email }})
</li>
</ul>
<div *ngIf="error" class="error">{{ error }}</div>
`,
styles: [`
.error { color: red; }
`]
})
export class UserListComponent implements OnInit {
users: User[] = [];
error: string = '';
constructor(private http: HttpClient) {}
ngOnInit() {
this.http.get<User[]>('https://jsonplaceholder.typicode.com/users')
.subscribe({
next: (data) => this.users = data,
error: (err) => this.error = 'Failed to load users'
});
}
}
This component uses Angular’s HttpClient to make an HTTP request when it initializes. The *ngFor directive loops through the users array, and *ngIf conditionally shows an error message. Angular’s powerful template syntax and built-in directives handle many common tasks. I’ve found that its opinionated structure enforces best practices, which is beneficial in long-term projects.
Svelte takes a different approach by moving work from runtime to compile time. It compiles components into highly efficient vanilla JavaScript, so there’s no framework overhead in the browser. This results in faster applications with smaller bundle sizes. Svelte’s syntax is clean and minimal, making it easy to learn. Here’s a example of a reactive component that handles a form with validation:
<script>
let name = '';
let email = '';
let errors = {};
function validateForm() {
errors = {};
if (!name) errors.name = 'Name is required';
if (!email.includes('@')) errors.email = 'Invalid email';
return Object.keys(errors).length === 0;
}
function handleSubmit() {
if (validateForm()) {
alert(`Form submitted: ${name}, ${email}`);
name = '';
email = '';
}
}
</script>
<form on:submit|preventDefault={handleSubmit}>
<h2>Contact Form</h2>
<div>
<label for="name">Name:</label>
<input id="name" type="text" bind:value={name} />
{#if errors.name}
<span style="color: red;">{errors.name}</span>
{/if}
</div>
<div>
<label for="email">Email:</label>
<input id="email" type="email" bind:value={email} />
{#if errors.email}
<span style="color: red;">{errors.email}</span>
{/if}
</div>
<button type="submit">Submit</button>
</form>
In Svelte, assignments are reactive by default, so when name or email changes, the UI updates automatically. The bind:value directive syncs input values with variables. I’ve built small to medium apps with Svelte, and the lack of runtime framework makes them feel snappy. The compiler handles optimizations, so you write less code and get better performance.
Next.js is a framework built on top of React that adds server-side rendering and static site generation. This improves SEO and initial load times because pages are pre-rendered on the server. Next.js also has file-based routing, which means the file structure defines your app’s routes. I’ve used it for blogs and e-commerce sites where performance is critical. Here’s an example of a page that uses getStaticProps to fetch data at build time:
// pages/posts.js
import React from 'react';
export async function getStaticProps() {
// Simulate fetching data from an API
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
const posts = await res.json();
return {
props: {
posts: posts.slice(0, 5) // Limit to 5 posts for example
},
revalidate: 60 // Regenerate page every 60 seconds if using ISR
};
}
export default function PostsPage({ posts }) {
return (
<div>
<h1>Blog Posts</h1>
<ul>
{posts.map(post => (
<li key={post.id}>
<h2>{post.title}</h2>
<p>{post.body}</p>
</li>
))}
</ul>
</div>
);
}
This code defines a page that fetches posts during the build process and renders them as static HTML. If you enable incremental static regeneration, the page can update in the background. Next.js handles code splitting automatically, so users only download the JavaScript they need. I’ve found that this setup simplifies deployment and improves user experience, especially on slow networks.
Nuxt.js does for Vue what Next.js does for React. It provides server-side rendering, static site generation, and a modular architecture. Nuxt’s conventions reduce configuration, letting you focus on building features. The asyncData method allows you to fetch data before a page is rendered. Here’s a example of a Nuxt page that displays a list of products:
// pages/products.vue
<template>
<div>
<h1>Our Products</h1>
<div v-if="pending">Loading...</div>
<ul v-else>
<li v-for="product in products" :key="product.id">
<h2>{{ product.title }}</h2>
<p>Price: ${{ product.price }}</p>
</li>
</ul>
</div>
</template>
<script>
export default {
async asyncData({ $axios, error }) {
try {
const products = await $axios.$get('https://fakestoreapi.com/products');
return { products };
} catch (err) {
error({ statusCode: 500, message: 'Unable to fetch products' });
}
},
data() {
return {
pending: true
};
},
mounted() {
this.pending = false;
}
};
</script>
In this example, asyncData fetches products when the page is requested on the server. The $axios module is injected into the context, making HTTP requests easy. Nuxt also supports static site generation, where you can pre-render pages at build time. I’ve used Nuxt for universal apps that need to work with and without JavaScript, and its ecosystem of modules adds functionality without extra configuration.
Solid.js is a newer framework that emphasizes performance through fine-grained reactivity. It compiles JSX to direct DOM manipulations, avoiding the virtual DOM altogether. This means updates are precise and fast, with a small runtime. Solid’s API is similar to React’s hooks, but it uses signals for state management. Here’s a example of a component that manages a list of tasks with real-time updates:
import { createSignal, createEffect } from 'solid-js';
function TaskManager() {
const [tasks, setTasks] = createSignal([]);
const [newTask, setNewTask] = createSignal('');
createEffect(() => {
console.log('Tasks updated:', tasks());
});
const addTask = () => {
const taskText = newTask().trim();
if (taskText) {
setTasks([...tasks(), { id: Date.now(), text: taskText, completed: false }]);
setNewTask('');
}
};
const toggleTask = (id) => {
setTasks(tasks().map(task =>
task.id === id ? { ...task, completed: !task.completed } : task
));
};
const deleteTask = (id) => {
setTasks(tasks().filter(task => task.id !== id));
};
return (
<div>
<h2>Task Manager</h2>
<input
type="text"
value={newTask()}
onInput={(e) => setNewTask(e.target.value)}
placeholder="Enter a new task"
/>
<button onClick={addTask}>Add Task</button>
<ul>
{tasks().map(task => (
<li>
<span style={{ 'text-decoration': task.completed ? 'line-through' : 'none' }}>
{task.text}
</span>
<button onClick={() => toggleTask(task.id)}>
{task.completed ? 'Undo' : 'Complete'}
</button>
<button onClick={() => deleteTask(task.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
}
export default TaskManager;
In Solid, createSignal creates reactive state, and functions that read signals are reactive themselves. The UI updates only when the specific signal changes. I’ve experimented with Solid in performance-critical applications, and its efficiency is impressive. The learning curve is low if you know React, but the mental model is different since there’s no re-rendering of components.
Each of these frameworks has its strengths, and the best choice depends on your project’s needs. For instance, if you’re building a quick prototype, Vue or Svelte might be ideal due to their simplicity. For large teams, Angular’s structure can prevent errors. React and its ecosystem are great for complex UIs, while Next.js and Nuxt.js add powerful rendering options. Solid.js excels where performance is paramount. In my experience, trying out a few frameworks on small projects helps you understand which one fits your workflow. The key is to start simple and scale as needed, leveraging the tools that make development faster and more enjoyable.
I hope this overview helps you see how these frameworks can improve your web development process. By using them, you can build applications that are not only functional but also maintainable and performant. If you’re just starting, pick one and build something small—you’ll quickly appreciate the benefits they bring to modern web development.