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

323 lines
7.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`:
```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 `<hello-dashboard>` 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`
<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`:
```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
<hello-dashboard></hello-dashboard>
```
Run:
```bash
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