Now that you understand state and events, it is time to break the interface into reusable, manageable components. This article shows how to implement component patterns in plain JavaScript. Follow the concept → code → reason cadence.
Key terms
- Component: a standalone slice of UI that bundles state, template, and events.
- Component factory: a helper that creates a component with
setup,render, and event wiring at once. - Root scope: the DOM range a component controls so it does not disturb others.
- Event bus: an
EventTargetplusCustomEventcombo that lets components exchange messages loosely. CustomEvent: a user-defined event that carries extra data in itsdetailproperty.
Core ideas
- Component factory: packages
setup,render, andbindEventsso every UI piece shares the same life cycle. - Root scope: each component manipulates DOM only within its root element to avoid collisions.
- Event bus: with
EventTargetandCustomEventyou can pass messages between components without tight coupling. - Folder structure: directories such as
components/andlib/signal ownership and keep dashboards tidy.
Code examples
Start by building a component factory that collects state, rendering, and event binding.
function createComponent({ selector, setup, render }) {
const root = document.querySelector(selector);
if (!root) throw new Error(`Could not find ${selector}.`);
const state = setup?.() ?? {};
let bindEvents = () => {};
function update(partial) {
Object.assign(state, partial);
root.innerHTML = render(state);
bindEvents();
}
function setBinder(fn) {
bindEvents = fn;
bindEvents();
}
return { root, state, update, setBinder };
}
Next, create a card-list component that bundles filter and delete actions.
const cardList = createComponent({
selector: "#card-list",
setup: () => ({ items: loadState([]), filter: "all" }),
render: ({ items, filter }) => {
const filtered = filter === "all" ? items : items.filter((item) => item.category === filter);
return `
<div class="card-toolbar">
<button data-filter="all">All</button>
<button data-filter="design">Design</button>
<button data-filter="data">Data</button>
</div>
<ul class="card-grid">
${filtered
.map(
(item) => `
<li class="card">
<h3>${item.title}</h3>
<p>${item.summary}</p>
<button data-id="${item.id}" class="card-remove">Remove</button>
</li>
`,
)
.join("")}
</ul>
`;
},
});
cardList.setBinder(() => {
cardList.root.querySelectorAll("[data-filter]").forEach((button) => {
button.addEventListener("click", () => {
cardList.update({ filter: button.dataset.filter });
});
});
cardList.root.querySelectorAll(".card-remove").forEach((button) => {
button.addEventListener("click", () => {
const nextItems = cardList.state.items.filter((item) => item.id !== button.dataset.id);
cardList.update({ items: nextItems });
saveState(nextItems);
});
});
});
The event bus relays signals between components.
const bus = new EventTarget();
bus.addEventListener("filter-change", (event) => {
const { detail } = event;
statsPanel.update({ filter: detail.filter });
});
cardList.root.addEventListener("click", (event) => {
const target = event.target;
if (target instanceof HTMLButtonElement && target.dataset.filter) {
bus.dispatchEvent(new CustomEvent("filter-change", { detail: { filter: target.dataset.filter } }));
}
});
Organize files like this:
src/
components/
card-list.js
stats-panel.js
log-list.js
lib/
storage.js
format.js
Why it matters
- Keeping DOM work inside the root scope prevents style leaks and duplicate handlers.
- The factory pattern keeps
setupandrenderidentical across components, so scaling up stays manageable. - An event bus lets components share filter or delete signals without hard references.
Practice
- Follow along: build the card list component with
createComponentand wire events viasetBinder. - Extend: add at least two more components such as
statsPanelorlogList, then pass messages withEventTarget. - Debug: intentionally call
bindEventstwice to observe duplicate handlers, then fix it by re-binding only inside the root scope. - Done when: independent components share filter/delete updates and the UI refreshes everywhere in sync.
Wrap-up
Component thinking keeps vanilla-JS projects sturdy. Next we will combine these components into a mini dashboard app.
💬 댓글
이 글에 대한 의견을 남겨주세요