Vanilla JavaScript Patterns I Use Instead of React
My portfolio is a PHP site with sprinkles of JavaScript—not a SPA. But I still need interactivity. Here's my toolkit.
1. The Store Pattern (No Redux Needed)
// js/store.js
class Store {
constructor(initialState = {}) {
this.state = initialState;
this.listeners = [];
}
getState() {
return this.state;
}
setState(newState) {
this.state = { ...this.state, ...newState };
this.listeners.forEach(fn => fn(this.state));
}
subscribe(listener) {
this.listeners.push(listener);
return () => {
this.listeners = this.listeners.filter(l => l !== listener);
};
}
}
// Usage
const themeStore = new Store({ theme: 'light' });
themeStore.subscribe(({ theme }) => {
document.documentElement.dataset.theme = theme;
localStorage.setItem('theme', theme);
});
// Toggle theme
document.querySelector('#theme-toggle').addEventListener('click', () => {
const current = themeStore.getState().theme;
themeStore.setState({ theme: current === 'light' ? 'dark' : 'light' });
});
2. Web Components for Reusability
// js/components/BlogCard.js
class BlogCard extends HTMLElement {
connectedCallback() {
const { title, excerpt, date, slug } = this.dataset;
this.innerHTML = `
<article class="card">
<h3>${title}</h3>
<time>${new Date(date).toLocaleDateString()}</time>
<p>${excerpt}</p>
<a href="/post.php?slug=${slug}">Read more</a>
</article>
`;
}
}
customElements.define('blog-card', BlogCard);
<!-- Usage in PHP -->
<blog-card
data-title="<?= htmlspecialchars($post['title']) ?>"
data-excerpt="<?= htmlspecialchars($post['excerpt']) ?>"
data-date="<?= $post['date'] ?>"
data-slug="<?= $post['slug'] ?>">
</blog-card>
3. Fetch with Error Handling
// js/api.js
async function fetchJSON(url, options = {}) {
try {
const response = await fetch(url, {
headers: { 'Accept': 'application/json' },
...options
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
console.error(`Failed to fetch ${url}:`, error);
// Return cached data or fallback
return JSON.parse(localStorage.getItem(`cache_${url}`) || 'null');
}
}
// Usage: Load more posts dynamically
let currentPage = 1;
async function loadMorePosts() {
const posts = await fetchJSON(`/api/posts.php?page=${++currentPage}`);
if (posts) {
posts.forEach(post => {
const card = document.createElement('blog-card');
card.dataset = post; // Simplified
document.querySelector('.blog-grid').appendChild(card);
});
}
}
4. Event Delegation (Because PHP Renders HTML)
Since my HTML comes from PHP, I can't add listeners during render. Solution:
document.addEventListener('click', (e) => {
// Handle all "Read More" clicks
if (e.target.matches('.read-more')) {
e.preventDefault();
const slug = e.target.dataset.slug;
loadPostModal(slug);
}
// Handle dynamic elements too
if (e.target.closest('.delete-post')) {
const id = e.target.closest('.delete-post').dataset.id;
deletePost(id);
}
});
The Bundle Size?
2.3KB minified. No build step. No node_modules. Just modern JavaScript.
You don't need React for a blog. You need good patterns.