## Summary - Add `effect-fc-next`, a React 19 integration targeting Effect v4 beta. - Introduce component lifecycles, scoped resources, queries, mutations, forms, lenses, views, and refreshable components. - Add `@effect-view/vite-plugin` for Vite Fast Refresh support. - Add an Effect v4 example application covering the new APIs. - Expand test coverage for both the existing and next-generation packages. - Replace the starter Docusaurus content with complete Effect View documentation while preserving the Effect v3 docs as a versioned snapshot. - Update the landing page, navigation, package scripts, and build output. - Extend CI with linting, tests, package builds, Docker builds, and container publishing. ## Validation - `bun lint:tsc` - `bun lint:biome` - `bun test` - `bun run build` - `bun pack` - Docker image build --------- Co-authored-by: Julien Valverdé <julien.valverde@mailo.com> Reviewed-on: Thilawyn/effect-fc#56
5.3 KiB
Effect View
Write React components as typed Effect programs.
Effect View brings Effect's typed services, resource safety, concurrency, and data modeling into React 19 without replacing React's component model. Yield Effects and services from a component body, let scopes follow the React lifecycle, and return ordinary JSX.
Effect v4 beta: Effect View is built for the Effect v4 beta release. If your application uses Effect v3, use the legacy
effect-fcpackage instead.
npm install effect-view effect@beta react
Effect View does not depend on react-dom. Install the renderer used by your
application—for example, react-dom for the web:
npm install react-dom
You can use Effect View with React Native or any other React renderer instead.
What it looks like
An Effect View component is an Effect program that produces JSX. It can create scoped state, access services, and use React-facing hooks without leaving the generator model:
import { Effect, SubscriptionRef } from "effect"
import { Component, Lens } from "effect-view"
import { runtime } from "./runtime"
const CounterValueView = Component.make("CounterValue")(
function* ({ count }: { readonly count: Lens.Lens<number> }) {
yield* Component.useOnMount(() =>
Effect.addFinalizer(() =>
Effect.log("CounterValue unmounted"),
),
)
const [value, setValue] = yield* Lens.useState(count)
return (
<button onClick={() => setValue((n) => n + 1)}>
Count: {value}
</button>
)
},
)
const CounterView = Component.make("Counter")(function* () {
const count = yield* Component.useOnMount(() =>
Effect.map(
SubscriptionRef.make(0),
Lens.fromSubscriptionRef,
),
)
const CounterValue = yield* CounterValueView.use
return <CounterValue count={count} />
})
export const Counter = CounterView.pipe(
Component.withContext(runtime.context),
)
CounterView composes another Effect View component by yielding its .use
Effect, then renders the resulting component as ordinary JSX. The child can
access the current Effect context and receives its own component scope.
Effect.addFinalizer uses the scope already provided to the component body, so
the finalizer above runs when CounterValue unmounts.
At the outer boundary, Counter is still a normal React component and its Effect requirements remain
typed until they reach the runtime.
Build the runtime and render your first component →
Why Effect View
| Capability | What it gives you | |
|---|---|---|
| Effect components | Component.make |
Yield Effects and typed services directly, then return JSX. |
| Managed lifecycles | Scope.Scope |
Finalizers, fibers, subscriptions, and resources close with their component or dependency lifecycle. |
| One application runtime | ReactRuntime |
Build your Layer once and make its services available throughout the UI. |
| Reactive state | Lens and View |
Focus large state models into small writable values and subscribe only where React needs them. |
| Server state | Query and Mutation |
TanStack Query-style caching, invalidation, staleness, and mutations with typed Effects underneath. |
| Schema-driven forms | MutationForm and LensForm |
Keep input-friendly encoded values while application code receives validated, decoded types. |
| Async rendering | Async and Memoized |
Integrate asynchronous Effects with Suspense while retaining Effect errors and cancellation. |
Designed for both ecosystems
Effect View does not hide Effect behind callbacks or recreate React around a new rendering model. React still owns rendering, JSX, hooks, Suspense, and events. Effect owns services, errors, resource safety, concurrency, streams, and schemas. Effect View provides the lifecycle-aware boundary between them.
Documentation
The complete documentation is available at thila.dev/effect-view.
Requirements
- React 19.2 or newer
- Effect v4 beta (
effect@beta) - TypeScript and
@types/reactfor TypeScript projects
Effect View is renderer-independent and does not require react-dom.
Project status
Effect View is currently beta software. The main APIs are available, but breaking changes and rough edges are still possible before a stable release.
Issues, ideas, and contributions are welcome on GitHub.