[JavaScript Series 11] Organize Component-Style Code without a Framework

한국어 버전

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

  1. Component: a standalone slice of UI that bundles state, template, and events.
  2. Component factory: a helper that creates a component with setup, render, and event wiring at once.
  3. Root scope: the DOM range a component controls so it does not disturb others.
  4. Event bus: an EventTarget plus CustomEvent combo that lets components exchange messages loosely.
  5. CustomEvent: a user-defined event that carries extra data in its detail property.

Core ideas

  • Component factory: packages setup, render, and bindEvents so 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 EventTarget and CustomEvent you can pass messages between components without tight coupling.
  • Folder structure: directories such as components/ and lib/ 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 setup and render identical 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 createComponent and wire events via setBinder.
  • Extend: add at least two more components such as statsPanel or logList, then pass messages with EventTarget.
  • Debug: intentionally call bindEvents twice 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.

💬 댓글

이 글에 대한 의견을 남겨주세요