# 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). You’ll learn how to implement a **lightweight reactive store** with Lit, integrate it with your existing `` and `` components, and keep everything **test-driven**. --- # ⚡ Tutorial 6 — Shared State ### “The Store Pattern” — Synchronizing Multiple Components --- ## 🎯 Goal We’ll create a **store** that holds the global list of greeter names. All `` components will automatically update when the store changes. The `` 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`: ```javascript 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: ```bash npm test ``` All tests fail (expected). --- ## ⚙️ 2. Step 2 — Implement the store Create a new file: `src/store/hello-store.js` ```javascript 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: ```bash npm test ``` ✅ All tests should now pass. --- ## 🧩 3. Step 3 — Update `` to use the store Modify `src/components/hello-dashboard/hello-dashboard.js`: ```javascript 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`

Hello Dashboard

Total greetings: ${this.greeters.length}
${this.greeters.map( (name) => html`` )}
`; } } customElements.define("hello-dashboard", HelloDashboard); ``` --- ## 🧠 4. Step 4 — Update `` to use store for renaming Update `_onInput()` in `src/components/hello-world/hello-world.js`: ```javascript 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`: ```javascript 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: ```bash npm test ``` ✅ All pass again. --- ## 🌐 6. Step 6 — Try it live Add to your `index.html`: ```html ``` Run: ```bash npm run dev ``` * Click “Add Greeting” → new greeters appear instantly. * Type into any greeter → all components referencing that name update automatically. Congratulations — you’ve 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