You Might Not Need React
Hot take: for a lot of projects, vanilla JavaScript is fine. Better, even.
I'm not anti-framework. I use React Native for mobile apps, and I've built plenty of React web apps for work. But this portfolio site? Pure vanilla JS. And it was the right call.
Here's the thing: I spent years building production React apps. I know the ecosystem. I've debugged re-render loops at 3am, optimized bundle sizes, and explained why we need both useState and useReducer to junior devs. React is great at what it does. But what it does isn't always what you need.
The default reach for React
I notice a pattern in the dev community. Someone wants to build a personal site, a blog, a landing page. First thing they do? npm create vite@latest and pick React.
Then they spend hours configuring routing, setting up state management, figuring out how to handle CSS, choosing between 47 different component libraries. By the time they're ready to actually build the thing, they're exhausted.
For a site that's going to have like... 5 pages and some animations? That's a lot of overhead.
The starfield on this site
See those twinkling stars in the background? Click on one. A little quote pops up. Try it.
That's about 200 lines of vanilla JavaScript. It handles:
- Generating random star positions on page load
- Twinkling animations via CSS
- Click detection (with bigger hitboxes than the visual stars)
- Picking a random quote from an array
- Positioning the popup so it doesn't go off-screen
- Fade in/out animations
- Closing the popup when you click elsewhere or wait
Here's roughly what it looks like:
function createStars(count) {
const container = document.querySelector('.starfield');
for (let i = 0; i < count; i++) {
const star = document.createElement('div');
star.className = 'star';
star.style.left = `${Math.random() * 100}%`;
star.style.top = `${Math.random() * 100}%`;
star.addEventListener('click', showQuote);
container.appendChild(star);
}
}
Could I have done this in React? Sure. Would it have been better? Nope. It would have been more code, with more dependencies, solving the exact same problem. The React version would need useState for the popup, useRef for positioning, useEffect for click-outside handling... it's just more stuff.
Let's be real about what the React version looks like:
function Starfield() {
const [stars, setStars] = useState([]);
const [activeQuote, setActiveQuote] = useState(null);
const popupRef = useRef(null);
useEffect(() => {
// Generate stars on mount
const newStars = Array.from({ length: 100 }, (_, i) => ({
id: i,
left: Math.random() * 100,
top: Math.random() * 100,
}));
setStars(newStars);
}, []);
useEffect(() => {
// Click outside to close
function handleClick(e) {
if (popupRef.current && !popupRef.current.contains(e.target)) {
setActiveQuote(null);
}
}
document.addEventListener('click', handleClick);
return () => document.removeEventListener('click', handleClick);
}, []);
// ... more code for quote handling, positioning, etc
}
That's already 20+ lines and we haven't even rendered anything yet. The vanilla version? Direct DOM manipulation. No re-renders. No dependency arrays. No reconciliation algorithm running to figure out which stars changed (hint: they didn't).
What vanilla JS looks like in 2025
The browser has gotten really good. Here's what you get for free now:
ES Modules: Import/export works natively. No bundler needed.
<script type='module' src='/js/main.js'></script>
Fetch API: No more XMLHttpRequest nightmares.
const data = await fetch('/api/posts').then(r => r.json());
Template literals: Building HTML strings is way less painful.
const html = `
<article class='post'>
<h2>${post.title}</h2>
<p>${post.excerpt}</p>
</article>
`;
Query selectors: CSS selectors for DOM access.
const buttons = document.querySelectorAll('.btn');
CSS custom properties: Theming without JavaScript.
:root { --accent: #22d3ee; }
.button { background: var(--accent); }
You don't need jQuery anymore. You don't need a virtual DOM for a blog. The platform is good now.
The stuff that actually trips people up
Okay, vanilla JS isn't all rainbows. There are real gotchas. Let me save you some pain:
Event delegation vs direct binding: If you're adding/removing DOM elements dynamically, bind events to a parent container, not individual elements.
// Bad: event listeners disappear when you remove/re-add elements
document.querySelectorAll('.btn').forEach(btn => {
btn.addEventListener('click', handleClick);
});
// Good: one listener on the parent, works for all current and future buttons
document.querySelector('.container').addEventListener('click', (e) => {
if (e.target.matches('.btn')) {
handleClick(e);
}
});
XSS is on you: React escapes content by default. With vanilla JS and template literals, you need to remember to sanitize user input. Always.
// DANGEROUS - user input can inject scripts
element.innerHTML = `<div>${userContent}</div>`;
// Safe - creates text nodes, not HTML
element.textContent = userContent;
// Also safe - use a library like DOMPurify for rich content
element.innerHTML = DOMPurify.sanitize(userContent);
Memory leaks from forgotten listeners: React cleans up effects automatically. You have to remember:
function initTooltip(element) {
function showTooltip() { /* ... */ }
element.addEventListener('mouseenter', showTooltip);
// Return cleanup function
return () => element.removeEventListener('mouseenter', showTooltip);
}
// Later:
const cleanup = initTooltip(el);
// When removing the element:
cleanup();
These are real concerns! But they're not insurmountable. You just need to be mindful. React abstracts these away, which is great until you need to understand what's actually happening.
When to skip the framework
If your site is mostly static content with some interactivity sprinkled in, you probably don't need React:
- Portfolios: A few pages, maybe some animations
- Blogs: Server-rendered HTML with a comment form
- Landing pages: Hero, features, pricing, footer
- Documentation sites: Mostly text with some interactive examples
- Marketing sites: Static content that changes rarely
All of these are great candidates for vanilla JS (or no JS at all). Server-render the HTML, add a little interactivity where needed, done.
A real example: building a modal
Let's walk through something concrete. Here's a reusable modal in vanilla JS:
class Modal {
constructor(contentHtml) {
this.modal = this.createModal(contentHtml);
this.isOpen = false;
}
createModal(contentHtml) {
const modal = document.createElement('div');
modal.className = 'modal';
modal.innerHTML = `
<div class="modal-overlay"></div>
<div class="modal-content">
<button class="modal-close">×</button>
<div class="modal-body">${contentHtml}</div>
</div>
`;
// Close on overlay click or close button
modal.querySelector('.modal-overlay').addEventListener('click', () => this.close());
modal.querySelector('.modal-close').addEventListener('click', () => this.close());
// Close on Escape key
this.handleEscape = (e) => {
if (e.key === 'Escape') this.close();
};
return modal;
}
open() {
if (this.isOpen) return;
document.body.appendChild(this.modal);
document.addEventListener('keydown', this.handleEscape);
document.body.style.overflow = 'hidden'; // Prevent background scroll
this.isOpen = true;
// Trigger animation
requestAnimationFrame(() => {
this.modal.classList.add('modal-open');
});
}
close() {
if (!this.isOpen) return;
this.modal.classList.remove('modal-open');
// Wait for animation before removing
setTimeout(() => {
this.modal.remove();
document.removeEventListener('keydown', this.handleEscape);
document.body.style.overflow = '';
this.isOpen = false;
}, 300);
}
}
// Usage
const modal = new Modal('<h2>Hello!</h2><p>This is a modal.</p>');
modal.open();
Is this more manual than using a React modal library? Sure. But you know exactly what it does. No hidden magic. No bundle size bloat. No peer dependency conflicts. And it's reusable across any project - just drop in the class.
The CSS is straightforward too:
.modal {
position: fixed;
inset: 0;
z-index: 1000;
}
.modal-overlay {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0);
transition: background 0.3s;
}
.modal-open .modal-overlay {
background: rgba(0, 0, 0, 0.5);
}
.modal-content {
position: relative;
max-width: 500px;
margin: 100px auto;
background: white;
border-radius: 8px;
padding: 24px;
opacity: 0;
transform: translateY(-20px);
transition: all 0.3s;
}
.modal-open .modal-content {
opacity: 1;
transform: translateY(0);
}
This pattern works for tooltips, dropdowns, slide-outs - basically any UI component. Write it once, use it everywhere.
When you DO need React
I'm not saying never use React. Use it when it makes sense:
- Complex state: Multiple components that share and update state
- Real-time updates: Chat apps, dashboards, collaborative tools
- Rich interactions: Drag-and-drop, complex forms, data grids
- Large teams: Conventions and component patterns help coordination
- Existing ecosystem: You need a specific library that's React-only
If you're building an actual application - something with user accounts, complex flows, lots of dynamic content - yeah, a framework helps. That's what they're designed for.
The performance question
"But what about performance? Doesn't React optimize re-renders?"
Yes. And for 90% of sites, it doesn't matter.
Think about it: the starfield on this site creates 100 DOM elements once. They never change. React would create a virtual DOM, reconcile it with the real DOM, and... arrive at the same result. That's overhead, not optimization.
Where React shines is when you have complex state that triggers frequent UI updates. Like a spreadsheet where editing one cell could affect dozens of other cells. React's diffing algorithm figures out the minimal set of DOM changes needed. That's legitimately useful.
But a blog? A portfolio? A landing page? You're not re-rendering on every keystroke. You're showing static content with occasional interactions. The browser can handle that just fine.
Real performance wins for content sites:
- Server-side rendering (works with or without React)
- Code splitting (also framework-agnostic)
- Image optimization (same)
- Fewer dependencies (advantage: vanilla)
- Smaller bundle size (advantage: vanilla)
For this site, the entire JavaScript bundle is under 15KB. The starfield, navigation, theme toggle, everything. That beats the React runtime alone (45KB minified + gzipped).
The htmx middle ground
There's also a middle path I've been exploring: htmx. It lets you add interactivity to server-rendered HTML with just attributes:
<button hx-post='/like' hx-swap='outerHTML'>
Like (42)
</button>
Click the button, it POSTs to the server, swaps in the response. No JavaScript to write. For CRUD apps and content sites, it's surprisingly capable.
Here's a more complete example - an infinite scroll blog:
<div id="posts">
<article>Post 1</article>
<article>Post 2</article>
<!-- ... -->
</div>
<div
hx-get="/posts?page=2"
hx-trigger="intersect once"
hx-swap="afterend"
>
<span class="loading">Loading more...</span>
</div>
That's it. When the loading div scrolls into view (intersect), htmx fetches the next page and inserts it below. The server just returns HTML. No JSON parsing, no client-side templating, no framework.
I'm not saying htmx is always the answer. But it's a tool worth knowing about. Especially if you're already rendering HTML on the server (Flask, Django, Rails, Laravel, whatever).
Building your vanilla JS toolkit
If you decide to go the vanilla route, here are some patterns I use all the time:
Component initialization pattern
I like to write components as classes that auto-initialize:
class Dropdown {
constructor(element) {
this.element = element;
this.toggle = element.querySelector('[data-dropdown-toggle]');
this.menu = element.querySelector('[data-dropdown-menu]');
this.isOpen = false;
this.toggle.addEventListener('click', () => this.toggleMenu());
document.addEventListener('click', (e) => {
if (!this.element.contains(e.target)) this.close();
});
}
toggleMenu() {
this.isOpen ? this.close() : this.open();
}
open() {
this.menu.classList.add('open');
this.isOpen = true;
}
close() {
this.menu.classList.remove('open');
this.isOpen = false;
}
}
// Auto-initialize all dropdowns
document.querySelectorAll('[data-dropdown]').forEach(el => new Dropdown(el));
The data attributes make it easy to hook things up in HTML without writing custom JavaScript for each instance.
State management for simple apps
You don't need Redux. For small apps, a simple observable pattern works great:
class Store {
constructor(initialState) {
this.state = initialState;
this.listeners = [];
}
getState() {
return this.state;
}
setState(newState) {
this.state = { ...this.state, ...newState };
this.listeners.forEach(listener => listener(this.state));
}
subscribe(listener) {
this.listeners.push(listener);
return () => {
this.listeners = this.listeners.filter(l => l !== listener);
};
}
}
// Usage
const store = new Store({ count: 0, user: null });
store.subscribe((state) => {
document.querySelector('#count').textContent = state.count;
});
store.setState({ count: 1 }); // Updates UI automatically
About 20 lines of code gets you reactive state management. Good enough for most small apps.
Fetching data without tangled promises
Async/await made API calls so much cleaner:
async function loadPosts() {
const container = document.querySelector('#posts');
container.innerHTML = '<div class="loading">Loading...</div>';
try {
const response = await fetch('/api/posts');
if (!response.ok) throw new Error('Failed to fetch');
const posts = await response.json();
container.innerHTML = posts.map(post => `
<article class="post">
<h2>${post.title}</h2>
<p>${post.excerpt}</p>
</article>
`).join('');
} catch (error) {
container.innerHTML = `<div class="error">Failed to load posts: ${error.message}</div>`;
}
}
No useEffect dependencies. No loading states in multiple components. Just straight-line code that does what it says.
The cost-benefit analysis
Let's be honest about the tradeoffs.
React wins when:
- Your team already knows React (learning curve is real)
- You're building a highly interactive SPA
- You need the ecosystem (libraries, dev tools, hiring pool)
- Component reuse across large apps is critical
- You want TypeScript integration out of the box (though vanilla TS is fine too)
Vanilla wins when:
- You're shipping to bandwidth-constrained users
- Initial load time is critical
- The project is small to medium sized
- You don't want to maintain dependencies
- You value simplicity over abstraction
- Your backend already renders HTML
There's no universal answer. But I think the industry defaults to React too often for projects that would be simpler without it.
The meta point
The real issue isn't React vs vanilla. It's reaching for tools by default instead of thinking about what you actually need.
Every dependency is a liability. Every abstraction has a cost. Sometimes that cost is worth it! React is a great tool. But sometimes you're adding complexity that doesn't pay for itself.
I've watched developers spend hours debugging why their React app won't deploy, when the underlying problem could have been solved with 50 lines of vanilla JavaScript. I've seen bundle sizes balloon to megabytes for sites that show basically static content.
This isn't about being a purist. It's about using the right tool for the job. Sometimes that's React. Sometimes it's vanilla JS. Sometimes it's htmx, or Svelte, or web components, or server-side rendering with no JS at all.
Getting started
If you want to try building something without a framework, here's my advice:
Start small. Don't rewrite your company's flagship product. Build a side project. Make your portfolio site. Create a simple tool you'll actually use.
Don't abstract too early. Write some "bad" code first. Copy and paste is okay initially. You'll start to see patterns emerge, and that's when you extract reusable components. Not before.
Use the platform. Read MDN docs. Learn what the browser can do. Web APIs have gotten really good - Intersection Observer, Resize Observer, Web Animations, etc. You don't need a library for everything.
Ship it. Deploy something real. See what it feels like to have zero build step. No compile errors. No dependency updates. Just HTML, CSS, and JavaScript.
Next time you start a project, ask yourself: what's the simplest thing that could work? You might be surprised how far vanilla JS takes you.