146 lines
3.3 KiB
Markdown
146 lines
3.3 KiB
Markdown
# zulip
|
|
|
|
A modern, no-build web application using Lit web components with Zustand state management.
|
|
|
|
## Architecture Overview
|
|
|
|
* **Lit-based web components** - Standards-based, framework-agnostic UI
|
|
* **No-build tooling** - Import maps only, no bundler required
|
|
* **Zustand state management** - Lightweight, vanilla JS store
|
|
* **StoreController** - Reactive controller that bridges Zustand and Lit
|
|
|
|
## Project Structure
|
|
|
|
```
|
|
components/ -> standard lit components, including top-level "app-root"
|
|
controllers/ -> lit reactive controllers (such as Zustand<->Lit bridge)
|
|
store/ -> defines specific accessor sets
|
|
index.html -> entry point with inline importmap and app-root
|
|
README.md
|
|
```
|
|
|
|
## Data Flow Diagram
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant User
|
|
participant LitComponent
|
|
participant StoreController
|
|
participant ZustandStore
|
|
|
|
User->>LitComponent: Click button
|
|
LitComponent->>ZustandStore: Call action (e.g., addItem)
|
|
ZustandStore->>ZustandStore: Update state
|
|
ZustandStore->>StoreController: Notify subscribers
|
|
StoreController->>LitComponent: requestUpdate()
|
|
LitComponent->>LitComponent: Re-render
|
|
LitComponent->>User: Show updated UI
|
|
```
|
|
|
|
## Example
|
|
|
|
### 1. Define Store (Zustand Vanilla)
|
|
|
|
```javascript
|
|
// store/index.js
|
|
import { createStore } from 'zustand/vanilla'
|
|
|
|
export const store = createStore((set, get) => ({
|
|
count: 0,
|
|
increment: () => set(s => ({ count: s.count + 1 })),
|
|
decrement: () => set(s => ({ count: s.count - 1 })),
|
|
}))
|
|
```
|
|
|
|
### 2. Create StoreController (Bridge)
|
|
|
|
```javascript
|
|
// controllers/store.js
|
|
export class StoreController {
|
|
constructor(host, store, selector) {
|
|
this.host = host
|
|
this.store = store
|
|
this.selector = selector
|
|
host.addController(this)
|
|
}
|
|
|
|
hostConnected() {
|
|
this._unsub = this.store.subscribe((state) => {
|
|
const next = this.selector(state)
|
|
if (next !== this.value) {
|
|
this.value = next
|
|
this.host.requestUpdate()
|
|
}
|
|
})
|
|
this.value = this.selector(this.store.getState())
|
|
}
|
|
|
|
hostDisconnected() {
|
|
this._unsub?.()
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3. Create Component (Lit + StoreController)
|
|
|
|
```javascript
|
|
// components/counter.js
|
|
import { LitElement, html, css } from 'lit'
|
|
|
|
class Counter extends LitElement {
|
|
static properties = {
|
|
count: { type: Number, state: true }
|
|
}
|
|
|
|
constructor() {
|
|
super()
|
|
this.count = 0
|
|
}
|
|
|
|
static styles = css`
|
|
button { padding: 1rem; font-size: 1.2rem; margin: 0.5rem; }
|
|
`
|
|
|
|
render() {
|
|
return html`
|
|
<div>
|
|
<p>Count: ${this.#count.value}</p>
|
|
<button @click=${() => store.getState().decrement()}>-</button>
|
|
<button @click=${() => store.getState().increment()}>+</button>
|
|
</div>
|
|
`
|
|
}
|
|
}
|
|
|
|
customElements.define('my-counter', Counter)
|
|
```
|
|
|
|
### 4. Instantiate in parent (such as app-root)
|
|
|
|
```js
|
|
import counter from "./counter.js":
|
|
|
|
class AppRoot extends LitElement {
|
|
render() {
|
|
return html`
|
|
<counter></counter>
|
|
`;
|
|
}
|
|
}
|
|
```
|
|
|
|
## Benefits Over TSX
|
|
|
|
1. **No build step** - Edit and refresh, instant feedback
|
|
|
|
1. **Smaller bundle** - No framework runtime, just standards
|
|
|
|
1. **Better encapsulation** - Shadow DOM, scoped styles
|
|
|
|
1. **Framework agnostic** - Works anywhere, even in React apps
|
|
|
|
1. **Future-proof** - Built on web standards
|
|
|
|
1. **Familiar patterns** - Zustand works like Redux/Context
|
|
|
|
1. **Type-safe** - Can add JSDoc or TypeScript without build step
|