Files
testdrive-jsui/tutorials/Tutorial 6 Shared State.md

7.8 KiB
Raw Permalink Blame History

Tutorial 6: Shared State

Now that your components can communicate through events, the sixth tutorial introduces the next natural step in professional UI architecture: shared state (synchronizing multiple components via a central store).

Youll learn how to implement a lightweight reactive store with Lit, integrate it with your existing <hello-world> and <hello-dashboard> components, and keep everything test-driven.


Tutorial 6 — Shared State

“The Store Pattern” — Synchronizing Multiple Components


🎯 Goal

Well create a store that holds the global list of greeter names. All <hello-world> components will automatically update when the store changes. The <hello-dashboard> will use the store to add, remove, and rename greeters — without manually passing props or listening to custom events.

Architecture overview:

+------------------------+
|   HelloDashboard       |
|   (manages store ops)  |
|                        |
|  ┌────────────┐        |
|  | HelloWorld |  <── store.subscribe()
|  └────────────┘        |
|  ┌────────────┐        |
|  | HelloWorld |        |
|  └────────────┘        |
+------------------------+
           │
           ▼
       Shared Store
  (holds array of greeters)

🧪 1. Step 1 — Write the failing test first

Create src/store/hello-store.test.js:

import { helloStore } from "./hello-store.js";

describe("helloStore", () => {
  it("starts with one default greeter", () => {
    const state = helloStore.getState();
    expect(state.greeters).to.deep.equal(["World"]);
  });

  it("can add a new greeter", () => {
    helloStore.addGreeter("Alice");
    const state = helloStore.getState();
    expect(state.greeters).to.include("Alice");
  });

  it("notifies subscribers on change", (done) => {
    const unsubscribe = helloStore.subscribe((state) => {
      expect(state.greeters).to.include("Bob");
      unsubscribe();
      done();
    });
    helloStore.addGreeter("Bob");
  });
});

Run:

npm test

All tests fail (expected).


⚙️ 2. Step 2 — Implement the store

Create a new file: src/store/hello-store.js

class HelloStore {
  constructor() {
    this.state = { greeters: ["World"] };
    this.listeners = new Set();
  }

  getState() {
    return this.state;
  }

  _notify() {
    for (const cb of this.listeners) cb(this.state);
  }

  subscribe(callback) {
    this.listeners.add(callback);
    callback(this.state); // immediate call with current state
    return () => this.listeners.delete(callback);
  }

  addGreeter(name) {
    this.state = {
      ...this.state,
      greeters: [...this.state.greeters, name]
    };
    this._notify();
  }

  renameGreeter(oldName, newName) {
    const updated = this.state.greeters.map((n) =>
      n === oldName ? newName : n
    );
    this.state = { ...this.state, greeters: updated };
    this._notify();
  }
}

export const helloStore = new HelloStore();

Re-run:

npm test

All tests should now pass.


🧩 3. Step 3 — Update <hello-dashboard> to use the store

Modify src/components/hello-dashboard/hello-dashboard.js:

import { LitElement, html, css } from "lit";
import "../hello-world/hello-world.js";
import { helloStore } from "../../store/hello-store.js";

export class HelloDashboard extends LitElement {
  static styles = css`
    .container {
      font-family: system-ui, sans-serif;
      padding: 1rem;
      text-align: center;
    }
    .counter {
      margin: 0.5rem 0 1rem 0;
      font-weight: bold;
      color: #007acc;
    }
    button {
      background: #007acc;
      color: white;
      border: none;
      border-radius: 6px;
      padding: 0.4rem 1rem;
      cursor: pointer;
    }
  `;

  constructor() {
    super();
    this.greeters = [];
  }

  connectedCallback() {
    super.connectedCallback();
    this.unsubscribe = helloStore.subscribe((state) => {
      this.greeters = state.greeters;
    });
  }

  disconnectedCallback() {
    this.unsubscribe?.();
    super.disconnectedCallback();
  }

  _addGreeter() {
    const newName = `User${this.greeters.length + 1}`;
    helloStore.addGreeter(newName);
  }

  render() {
    return html`
      <div class="container">
        <h2>Hello Dashboard</h2>
        <div class="counter">Total greetings: ${this.greeters.length}</div>

        ${this.greeters.map(
          (name) => html`<hello-world name=${name}></hello-world>`
        )}

        <button @click=${this._addGreeter}>Add Greeting</button>
      </div>
    `;
  }
}

customElements.define("hello-dashboard", HelloDashboard);

🧠 4. Step 4 — Update <hello-world> to use store for renaming

Update _onInput() in src/components/hello-world/hello-world.js:

import { helloStore } from "../../store/hello-store.js";

_onInput(event) {
  const oldName = this.name;
  this.name = event.target.value;
  helloStore.renameGreeter(oldName, this.name);
}

Now each greeter updates the shared store when its input changes — all other components subscribed to the store will react automatically.


🧪 5. Step 5 — Integration test (optional)

Add src/integration/dashboard-store.test.js:

import "../components/hello-dashboard/hello-dashboard.js";
import { helloStore } from "../store/hello-store.js";

describe("HelloDashboard and HelloStore integration", () => {
  it("renders as many greeters as store entries", async () => {
    helloStore.addGreeter("Eve");
    const el = document.createElement("hello-dashboard");
    document.body.appendChild(el);
    await el.updateComplete;

    const greeters = el.shadowRoot.querySelectorAll("hello-world");
    expect(greeters.length).to.equal(helloStore.getState().greeters.length);
  });
});

Run:

npm test

All pass again.


🌐 6. Step 6 — Try it live

Add to your index.html:

<hello-dashboard></hello-dashboard>

Run:

npm run dev
  • Click “Add Greeting” → new greeters appear instantly.
  • Type into any greeter → all components referencing that name update automatically.

Congratulations — youve just implemented a reactive state system entirely in vanilla JS + Lit!


📘 7. Key Concepts

Concept Description
Centralized state One store manages the data for all components
Observer pattern Components subscribe to updates, re-render on change
Reactive sync Changes propagate automatically without prop drilling
TDD-first store design Ensures predictable, testable state behavior
Agent-friendliness Clear separation of state, presentation, and tests

8. Summary

At this point, your TestDrive-UI ecosystem includes:

Layer Responsibility
hello-world Simple reactive component
hello-dashboard Container and coordinator
hello-store Shared, observable state
Tests Guard behavior at each level

🔮 9. Next Tutorial (optional direction)

You can now evolve in one of two directions:

  1. Tutorial 7 — Persistence → Save and restore store state via localStorage or IndexedDB (so greetings persist).

  2. Tutorial 8 — Agent Automation → Introduce agent-driven TDD loops: use LLMs to generate new tests, detect regressions, and propose refactorings automatically.


Continue with Tutorial 7: State Persistence next — or move directly into agent-driven TDD automation.

xxx