7.8 KiB
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 <hello-world> and <hello-dashboard> 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 <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 — 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:
-
Tutorial 7 — Persistence → Save and restore store state via
localStorageor IndexedDB (so greetings persist). -
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