https://github.com/lloydrichards/proj_effect-box-web-components
Some experimentation in creating web components using lit and effect
https://github.com/lloydrichards/proj_effect-box-web-components
effect-ts lit tailwindcss web-components
Last synced: about 2 months ago
JSON representation
Some experimentation in creating web components using lit and effect
- Host: GitHub
- URL: https://github.com/lloydrichards/proj_effect-box-web-components
- Owner: lloydrichards
- Created: 2025-10-03T06:47:13.000Z (9 months ago)
- Default Branch: main
- Last Pushed: 2026-02-18T21:03:49.000Z (4 months ago)
- Last Synced: 2026-02-19T01:01:48.208Z (4 months ago)
- Topics: effect-ts, lit, tailwindcss, web-components
- Language: TypeScript
- Homepage: https://proj-effect-box-web-components.vercel.app
- Size: 819 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- Agents: AGENTS.md
Awesome Lists containing this project
README
# Effect-Atom Web Components
A deep-dive into the integration of [Effect](https://effect.website/) with
[Lit](https://lit.dev/) web components using the
[Effect-Atom](https://github.com/tim-smart/effect-atom) state management
library. Building a suite of web-components that leverage Effect for building
applications on the client-side
## Project Structure
```
lib/
├── components/ # Web component examples
│ ├── ui/ # Reusable UI components
│ ├── atom-counter.ts
│ ├── atom-stream-counter.ts
│ ├── scoped-counter.ts
│ └── atom-secrets.ts
├── shared/
│ ├── atomMixin.ts # Core AtomMixin implementation
│ ├── tailwindMixin.ts # Tailwind CSS integration
│ └── utils.ts # Shared utilities
└── main.ts # Entry point
```
## Getting Started
```bash
bun install
# Start dev server
bun run dev
# Build library
bun run build
# Type check
bun run type-check
# Lint & format
bun run lint
bun run format
```
## Quick Start
### 1. Create an Atom
Atoms are reactive containers for state. Use `Atom.fn` to create atoms that
execute Effect programs:
```typescript
import { Atom, Result } from "@effect-atom/atom";
import { Effect, Data } from "effect";
class CountError extends Data.TaggedError("CountError")<{ message: string }> {}
// Create a writable atom that runs an Effect
const countAtom = Atom.fn(
(newValue: number) =>
Effect.gen(function* () {
if (newValue < 0) {
return yield* new CountError({ message: "Count must be non-negative" });
}
yield* Effect.sleep("100 millis");
return newValue;
}),
{ initialValue: 0 }
);
```
### 2. Use AtomMixin in Your Component
The `AtomMixin` provides the bridge between Effect-Atom and Lit components:
```typescript
import { html, LitElement } from "lit";
import { customElement } from "lit/decorators.js";
import { AtomMixin, atomState } from "./shared/atomMixin";
@customElement("my-counter")
export class MyCounter extends AtomMixin(LitElement) {
// Automatically sync atom state to component property
@atomState(countAtom) declare count: Result.Result;
render() {
return html`
Increment
Count: ${Result.isSuccess(this.count) ? this.count.value : 0}
`;
}
private _increment() {
const setCount = this.useAtomSet(countAtom);
const current = Result.isSuccess(this.count) ? this.count.value : 0;
setCount(current + 1);
}
}
```
## Core Concepts
### AtomMixin
The `AtomMixin` is a Lit mixin that adds reactive atom capabilities to your
components. It provides several methods for working with atoms:
#### `useAtom(atom)`
Get both value and setter for a writable atom:
```typescript
const [count, setCount] = this.useAtom(countAtom);
setCount(5); // Set directly
setCount((prev) => prev + 1); // Update based on previous value
```
#### `useAtomValue(atom)`
Read-only access to an atom's value:
```typescript
const count = this.useAtomValue(countAtom);
```
#### `useAtomSet(atom)`
Get just the setter function without reading the value:
```typescript
const setCount = this.useAtomSet(countAtom);
setCount(10);
```
#### `useAtomPromise(atom)`
Convert a Result atom into a Promise:
```typescript
const data = await this.useAtomPromise(dataAtom);
```
#### `useAtomRefresh(atom)`
Get a function to manually refresh an atom:
```typescript
const refresh = this.useAtomRefresh(dataAtom);
refresh(); // Re-evaluate the atom
```
#### `useAtomMount(atom, options?)`
Explicitly mount an atom with optional reactivity keys:
```typescript
this.useAtomMount(dataAtom, { reactivityKeys: ["user", "settings"] });
```
#### `invalidate(keys)`
Manually refresh atoms associated with specific reactivity keys:
```typescript
this.invalidate(["user"]); // Refresh all atoms tagged with "user"
```
### @atomState Decorator
The `@atomState` decorator automatically syncs atom values to component
properties and triggers re-renders on changes:
```typescript
@atomState(myAtom) declare myValue: number;
@atomState(resultAtom) declare myResult: Result.Result;
```
It uses Lit's `@state()` internally, making the property reactive but private
(not exposed as an HTML attribute).
### Result Pattern
Effect-Atom's `Result` type represents async operations with four states:
- **Initial** - Not yet executed
- **Waiting** - In progress
- **Success** - Completed successfully with value `A`
- **Failure** - Failed with error `E`
Use the `matchResult` helper to handle all states:
```typescript
import { matchResult } from "./shared/atomMixin";
render() {
return matchResult(this.result, {
onInitial: () => html`Not started`,
onWaiting: () => html`Loading...`,
onSuccess: (value) => html`Value: ${value}`,
onFailure: (error) => html`Error: ${error.message}`,
});
}
```
### Global vs Scoped State
**Global Registry (default)** - Share state across all component instances:
```typescript
const countAtom = Atom.make(0);
// Both instances share the same count
@customElement("counter-a")
class CounterA extends AtomMixin(LitElement) {}
@customElement("counter-b")
class CounterB extends AtomMixin(LitElement) {}
```
**Scoped Registry** - Isolate state per component instance or component tree:
```typescript
import { Registry } from "@effect-atom/atom";
const scopedRegistry = Registry.make({
scheduleTask: (f) => queueMicrotask(f),
timeoutResolution: 1000,
defaultIdleTTL: 30_000,
});
const scopedAtom = Atom.make(0);
// Each instance has its own independent count
@customElement("isolated-counter")
class IsolatedCounter extends AtomMixin(LitElement, scopedRegistry) {
@atomState(scopedAtom) declare count: number;
}
```
## Learn More
- [Effect Documentation](https://effect.website/docs/introduction)
- [Effect-Atom](https://github.com/tim-smart/effect-atom)
- [Lit Documentation](https://lit.dev/)