I ran Claude Code for a weekend to create a reactive UI library with Effect

Last weekend, I decided to test the limits of AI-assisted development. Armed with Claude Code, my TypeScript vibe coding meta, and a slightly unhinged idea (what if we built a reactive UI library entirely on top of Effect?), I disappeared into a coding marathon.
What emerged 48 hours later was effect-ui: a fully functional reactive UI library that doesn’t just use Effect, it is Effect, all the way down.
Why Effect deserves its own reactive UI library
Effect has quietly become one of TypeScript’s most sophisticated runtime systems. It provides streams, fibers, scopes, contexts, scheduling, error handling: essentially everything you’d need to build complex applications. Yet most UI libraries treat Effect as an afterthought, something you integrate via adapters and wrappers.
But what if we flipped that? What if instead of wrapping Effect around a UI library, we built the UI library from Effect’s primitives?
// Traditional approach: UI library with Effect bolted on
const [state, setState] = useState(0);
useEffect(() => {
const subscription = effectStream.subscribe(setState);
return () => subscription.unsubscribe();
}, []);
// Effect-UI approach: Streams are the UI primitives
const Counter = () => (
<div>{Stream.iterate(0, n => n + 1).pipe(
Stream.schedule(Schedule.spaced(1000))
)}</div>
);
In traditional UI libraries, reactivity is the library’s concern: you have hooks, signals, or observables that the library manages. The developer experience and coding patterns are decided by the very library you are using. In effect-ui, reactivity comes from Effect’s Stream type, which means you inherit all of Effect’s features and compositional power for free.
Cowboy coding not allowed
On Friday evening, I opened Claude Code and started prompting.
This is not going to be cowboy coding. I’ve learned from my TypeScript vibe coding meta that AI assistants need structure to produce quality code. So the first hour was spent setting up rules:
## Project Rules
1. Effect streams are first-class UI primitives
2. Components execute once (ephemeral, like SolidJS)
3. No virtual DOM, use direct DOM manipulation
4. All async operations via Effect
5. TypeScript strict mode, no any
6. Every feature needs specs, mocks, and tests
7. Run validation after every change
Instead of diving into implementation, Claude and I spent the first three hours writing specifications:
// From dom.specs.md
declare function mount(
app: JSXNode,
root: HTMLElement
): Effect<MountHandle, RenderError>;
declare interface MountHandle {
unmount(): Effect<void>;
}
declare type JSXNode =
| Primitive
| Stream<JSXNode>
| Effect<JSXNode>
| JSXElement;
This disciplined approach (specs first, implementation second) meant that by bedtime, we hadn’t written a single line of runtime code, but we had a complete blueprint of the library’s API surface.
Effect’s superpowers start to shine
How do you efficiently update then DOM when streams emit new values without a virtual DOM?
Traditional reactive libraries either diff virtual trees (React), use fine-grained reactivity with proxy traps (Vue, SolidJS), or compile away the reactivity (Svelte). But Effect streams presented a unique opportunity.
By Saturday afternoon, the real magic started happening. Because effect-ui is built on Effect’s primitives, advanced patterns just… worked:
// Scheduling is built-in
const clockStream = Stream.iterate(new Date(), () => new Date()).pipe(
Stream.schedule(Schedule.spaced(1000))
);
// Error handling via Effect's error channel
const DataComponent = () => {
const dataStream = Stream.fromEffect(
Effect.tryPromise({
try: () => fetch('/api/data').then(r => r.json()),
catch: (error) => new FetchError({ cause: error })
})
);
return <div>{dataStream}</div>;
};
// Cancellation via scopes
const mount = (app: JSXNode, root: HTMLElement) =>
Effect.gen(function*() {
const scope = yield* Scope.make();
// All stream subscriptions are forked into this scope
// ...
return {
unmount: () => Scope.close(scope, Exit.void)
};
});
The integration feels natural because we’re simply embracing Effect’s primitives. Streams aren’t adapted to work with the UI; the UI is expressed through streams.
Sunday: The component model crystallizes
Sunday brought the final architectural decision: ephemeral components. Unlike React, which re-executes component functions on every update, effect-ui components run exactly once:
const Timer = ({ initial }: { initial: number }) => {
// This function executes ONCE
console.log("Timer component executed");
// The stream handles all future updates
return (
<div>
Count: {Stream.iterate(initial, n => n + 1).pipe(
Stream.schedule(Schedule.spaced(1000))
)}
</div>
);
};
// Even with changing props, the component doesn't re-execute
// Only the streams update
This model, borrowed from SolidJS, pairs perfectly with Effect streams. The component sets up the reactive graph once, then streams handle all updates. No re-rendering, no reconciliation, just data flowing through the system.
How my TypeScript vibe coding meta proved itself
Throughout the weekend, the disciplined approach from my vibe coding meta kept paying dividends:
-
Specs before code: Every feature started as a specification in
dom.specs.md
. This gave Claude clear acceptance criteria. -
Type-driven development: We wrote the type signatures first, using TypeScript’s
declare
keyword:declare function renderNode(node: JSXNode): Effect.Effect<RenderResult, RenderError, RenderContext>;
-
Test-driven validation: 27 acceptance criteria, each with multiple test cases. After every change, the full test suite ran.
-
Continuous validation loop:
# This ran hundreds of times over the weekend npm test && npm run check && npm run lint
In 48 hours, Claude produced ~1,000 lines of code and ~1,300 lines of tests, with zero runtime errors in the final library.
Technical deep dive: Streams as UI primitives
Let’s examine what makes effect-ui unique. In traditional reactive UI libraries, you have multiple concepts: state, effects, computeds, subscriptions. In effect-ui, everything is a Stream:
// Static values become single-emission streams
<div class="static">Hello</div>
// Internally: Stream.make("static")
// Dynamic values are already streams
<div class={colorStream}>Content</div>
// Effects become streams
<div>{Stream.fromEffect(fetchUserData())}</div>
// Even style properties can be streams
<div style={{
width: Stream.iterate(100, n => n + 10).pipe(
Stream.map(n => `${n}px`),
Stream.take(20)
),
backgroundColor: colorStream
}}>
Animated box
</div>
This unification means you can use Effect’s entire ecosystem of stream operators:
const searchResults = Stream.fromEffect(
Effect.succeed(searchInput)
).pipe(
Stream.debounce(300),
Stream.flatMap(query =>
Stream.fromEffect(searchAPI(query))
),
Stream.retry(Schedule.exponential(100)),
Stream.catchAll(() => Stream.make("Search failed"))
);
return <div>{searchResults}</div>;
Scope-based lifecycle management
One of Effect’s awesome features is its scope system. In effect-ui, every mount creates a supervision scope:
const mount = (app: JSXNode, root: HTMLElement) =>
Effect.gen(function*() {
const runtime = ManagedRuntime.make(Layer.empty);
const scope = yield* Scope.make();
const context = {
runtime,
scope,
streamIdCounter: { current: 0 }
};
// All stream subscriptions are forked into this scope
yield* Effect.forkIn(renderNode(app), scope);
let unmounted = false;
return {
unmount: () => Effect.gen(function*() {
if (unmounted) return;
unmounted = true;
// This cancels ALL stream subscriptions at once
yield* Scope.close(scope, Exit.void);
yield* Effect.promise(() => runtime.dispose());
})
};
});
This is remarkably elegant. Every stream subscription is a fiber forked into the mount scope. When you unmount, closing the scope automatically cancels all fibers, stopping all streams. No manual cleanup, no memory leaks, just Effect’s resource management.
Comparing with the establishment
One of my core goals was to piggy-back on Effect, without creating an entirely new reactivity api. With that in mind; how does effect-ui compare to the current landscape?
vs React:
- No virtual DOM overhead
- Built-in async primitives (no useEffect dance)
- Simpler mental model (streams only, no hooks)
vs SolidJS:
- Similar ephemeral component model
- No proxy traps or reactive primitives needed
vs Vue:
- No reactivity transform or compiler magic
- Streams are explicit, not hidden behind proxies
Though, the real comparison isn’t about being “better”; it’s about the paradigm. As I describe in my Redux in 2025 article: I argued that Redux’s value lies in its predictability and observability. Effect-ui builds on this. Every state change is a stream emission, every async operation is an Effect, every cleanup is scope-managed. The entire system is observable, predictable, and type-safe.
The weekend ends: Reflection on AI-assisted development
Sunday evening. The test suite is green, the playground has some working demos, and effect-ui is real. Although I wouldn’t dare claim that effect-ui is production ready, in a weekend with Claude Code as my programmer, we built something that would typically take weeks or months.
But here’s the crucial point: this wasn’t “prompt and pray” development. The success came from:
- Disciplined structure: Rules, specs, tests: the vibe coding meta in action
- Domain expertise: Understanding Effect’s patterns and UI library architecture
- Iterative refinement: Continuous (self) validation and adjustment
- Clear boundaries: AI wrote code, I provided architecture and direction
I could focus on architecture and design while Claude handled the implementation details. When something didn’t work, we’d adjust the rules and try again.
Some take-aways from effect-ui
This weekend I learned three things:
- Effect is ready for UI: The primitives are there (streams, fibers, scopes, contexts). We just needed to assemble them correctly.
- AI can build complex systems: With proper structure and discipline, AI assistants can tackle sophisticated projects.
- Paradigm shifts are possible: We don’t have to accept that UI libraries must work a certain way. By building on different primitives (Effect instead of traditional reactive systems), entirely new patterns emerge.
Where to go from here
Is effect-ui production-ready? No. At least not if you’re willing to get fired over it. But it’s a proof of concept that works surprisingly well.
More importantly, it demonstrates that Effect’s primitives are powerful enough to build entire categories of libraries that didn’t exist before.
At OWOW, the first bits of Effect code have found their way into our React code bases. The patterns that Effect makes possible (streams as UI primitives, scope-based lifecycle management, ephemeral components with Effect’s runtime) will influence how I think about UI development going forward.
Final thoughts
In a single weekend, with disciplined AI assistance and Effect’s powerful primitives, we created a reactive UI library where streams are JSX, effects are promises, and everything just… works. Whether you’re sold on Effect, curious about AI-assisted development, or just enjoy seeing new approaches to old problems, effect-ui proves that there’s still room for innovation in the seemingly saturated world of JavaScript UI libraries.
The code is on GitHub. Try it, break it, improve it. And next time someone tells you that all the good ideas in UI libraries have been explored, show them what happens when you combine Effect’s runtime with a weekend and some disciplined AI collaboration.