>,
+): Effect.fn.Return> {
+ const lens = yield* Component.useOnMount(() => Effect.map(
+ SubscriptionRef.make(value),
+ Lens.fromSubscriptionRef,
+ ))
+
+ yield* Component.useReactEffect(() => Effect.forkScoped(Stream.runForEach(
+ Stream.changesWith(lens.changes, options?.equivalence ?? Equivalence.strictEqual()),
+ v => Effect.sync(() => setValue(v)),
+ )), [setValue])
+ yield* Component.useReactEffect(() => Lens.set(lens, value), [value])
+
+ return lens
+})
diff --git a/packages/effect-fc-next/src/Memoized.ts b/packages/effect-fc-next/src/Memoized.ts
new file mode 100644
index 0000000..0c981c8
--- /dev/null
+++ b/packages/effect-fc-next/src/Memoized.ts
@@ -0,0 +1,112 @@
+/** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */
+import { type Equivalence, Function, Predicate } from "effect"
+import * as React from "react"
+import type * as Component from "./Component.js"
+
+
+export const MemoizedTypeId: unique symbol = Symbol.for("@effect-fc/Memoized/Memoized")
+export type MemoizedTypeId = typeof MemoizedTypeId
+
+
+/**
+ * A trait for `Component`'s that uses `React.memo` to optimize re-renders based on prop equality.
+ *
+ * @template P The props type of the component
+ */
+export interface Memoized extends MemoizedPrototype, MemoizedOptions
{}
+
+export interface MemoizedPrototype {
+ readonly [MemoizedTypeId]: MemoizedTypeId
+}
+
+/**
+ * Configuration options for Memoized components.
+ *
+ * @template P The props type of the component
+ */
+export interface MemoizedOptions
{
+ /**
+ * An optional equivalence function for comparing component props.
+ * If provided, this function is used by React.memo to determine if props have changed.
+ * Returns `true` if props are equivalent (no re-render), `false` if they differ (re-render).
+ */
+ readonly propsEquivalence?: Equivalence.Equivalence
+}
+
+
+export const MemoizedPrototype: MemoizedPrototype = Object.freeze({
+ [MemoizedTypeId]: MemoizedTypeId,
+
+ transformFunctionComponent
(
+ this: Memoized
,
+ f: React.FC
,
+ ) {
+ return React.memo(f, this.propsEquivalence)
+ },
+} as const)
+
+
+export const isMemoized = (u: unknown): u is Memoized => Predicate.hasProperty(u, MemoizedTypeId)
+
+/**
+ * Converts a Component into a `Memoized` component that optimizes re-renders using `React.memo`.
+ *
+ * @param self - The component to convert to a Memoized component
+ * @returns A new `Memoized` component with the same body, error, and context types as the input
+ *
+ * @example
+ * ```ts
+ * const MyMemoizedComponent = MyComponent.pipe(
+ * Memoized.memoized,
+ * )
+ * ```
+ */
+export const memoized = (
+ self: T
+): T & Memoized> => Object.setPrototypeOf(
+ Object.assign(function() {}, self),
+ Object.freeze(Object.setPrototypeOf(
+ Object.assign({}, MemoizedPrototype),
+ Object.getPrototypeOf(self),
+ )),
+)
+
+/**
+ * Applies options to a Memoized component, returning a new Memoized component with the updated configuration.
+ *
+ * Supports both curried and uncurried application styles.
+ *
+ * @param self - The Memoized component to apply options to (in uncurried form)
+ * @param options - The options to apply to the component
+ * @returns A Memoized component with the applied options
+ *
+ * @example
+ * ```ts
+ * // Curried
+ * const MyMemoizedComponent = MyComponent.pipe(
+ * Memoized.memoized,
+ * Memoized.withOptions({ propsEquivalence: (a, b) => a.id === b.id }),
+ * )
+ *
+ * // Uncurried
+ * const MyMemoizedComponent = Memoized.withOptions(
+ * Memoized.memoized(MyComponent),
+ * { propsEquivalence: (a, b) => a.id === b.id },
+ * )
+ * ```
+ */
+export const withOptions: {
+ >(
+ options: Partial>>
+ ): (self: T) => T
+ >(
+ self: T,
+ options: Partial>>,
+ ): T
+} = Function.dual(2, >(
+ self: T,
+ options: Partial>>,
+): T => Object.setPrototypeOf(
+ Object.assign(function() {}, self, options),
+ Object.getPrototypeOf(self),
+))
diff --git a/packages/effect-fc-next/src/Mutation.ts b/packages/effect-fc-next/src/Mutation.ts
new file mode 100644
index 0000000..0d873eb
--- /dev/null
+++ b/packages/effect-fc-next/src/Mutation.ts
@@ -0,0 +1,146 @@
+import { type Context, Effect, Equal, type Fiber, Option, Pipeable, Predicate, type Scope, Stream, SubscriptionRef } from "effect"
+import { Subscribable } from "effect-lens"
+import * as Result from "./Result.js"
+
+
+export const MutationTypeId: unique symbol = Symbol.for("@effect-fc/Mutation/Mutation")
+export type MutationTypeId = typeof MutationTypeId
+
+export interface Mutation
+extends Pipeable.Pipeable {
+ readonly [MutationTypeId]: MutationTypeId
+
+ readonly context: Context.Context
+ readonly f: (key: K) => Effect.Effect
+ readonly initialProgress: P
+
+ readonly latestKey: Subscribable.Subscribable>
+ readonly fiber: Subscribable.Subscribable>>
+ readonly result: Subscribable.Subscribable>
+ readonly latestFinalResult: Subscribable.Subscribable>>
+
+ mutate(key: K): Effect.Effect>
+ mutateSubscribable(key: K): Effect.Effect>>
+}
+
+export declare namespace Mutation {
+ export type AnyKey = readonly any[]
+}
+
+export class MutationImpl
+extends Pipeable.Class implements Mutation {
+ readonly [MutationTypeId]: MutationTypeId = MutationTypeId
+ readonly latestKey: Subscribable.Subscribable>
+ readonly fiber: Subscribable.Subscribable>>
+ readonly result: Subscribable.Subscribable>
+ readonly latestFinalResult: Subscribable.Subscribable>>
+
+ constructor(
+ readonly context: Context.Context,
+ readonly f: (key: K) => Effect.Effect,
+ readonly initialProgress: P,
+
+ readonly latestKeyRef: SubscriptionRef.SubscriptionRef>,
+ readonly fiberRef: SubscriptionRef.SubscriptionRef>>,
+ readonly resultRef: SubscriptionRef.SubscriptionRef>,
+ readonly latestFinalResultRef: SubscriptionRef.SubscriptionRef>>,
+ ) {
+ super()
+ this.latestKey = fromSubscriptionRef(latestKeyRef)
+ this.fiber = fromSubscriptionRef(fiberRef)
+ this.result = fromSubscriptionRef(resultRef)
+ this.latestFinalResult = fromSubscriptionRef(latestFinalResultRef)
+ }
+
+ mutate(key: K): Effect.Effect> {
+ return SubscriptionRef.set(this.latestKeyRef, Option.some(key)).pipe(
+ Effect.andThen(this.start(key)),
+ Effect.andThen(sub => this.watch(sub)),
+ Effect.provide(this.context),
+ )
+ }
+ mutateSubscribable(key: K): Effect.Effect>> {
+ return SubscriptionRef.set(this.latestKeyRef, Option.some(key)).pipe(
+ Effect.andThen(this.start(key)),
+ Effect.tap(sub => Effect.forkScoped(this.watch(sub))),
+ Effect.provide(this.context),
+ )
+ }
+
+ start(key: K): Effect.Effect<
+ Subscribable.Subscribable>,
+ never,
+ Scope.Scope | R
+ > {
+ const self = this
+ return Effect.gen(function*() {
+ const initial = yield* SubscriptionRef.get(self.latestFinalResultRef)
+ const [sub, fiber] = yield* Result.unsafeForkEffect(
+ Effect.onExit(self.f(key), () => Effect.andThen(
+ Effect.all([Effect.fiberId, SubscriptionRef.get(self.fiberRef)]),
+ ([currentFiberId, fiber]) => Option.match(fiber, {
+ onSome: v => Equal.equals(currentFiberId, v.id)
+ ? SubscriptionRef.set(self.fiberRef, Option.none())
+ : Effect.succeed(undefined),
+ onNone: () => Effect.succeed(undefined),
+ }),
+ )),
+
+ {
+ initial: Option.isSome(initial) ? Result.willFetch(initial.value) : Result.initial(),
+ initialProgress: self.initialProgress,
+ } as Result.unsafeForkEffect.Options,
+ )
+ yield* SubscriptionRef.set(self.fiberRef, Option.some(fiber))
+ return sub
+ })
+ }
+
+ watch(
+ sub: Subscribable.Subscribable>
+ ): Effect.Effect> {
+ return sub.get.pipe(
+ Effect.andThen(initial => Stream.runFoldEffect(
+ Stream.takeUntil(sub.changes, result => Result.isFinal(result) && !Result.hasFlag(result)),
+ () => initial,
+ (_, result) => Effect.as(SubscriptionRef.set(this.resultRef, result), result),
+ ) as Effect.Effect>),
+ Effect.tap(result => SubscriptionRef.set(this.latestFinalResultRef, Option.some(result))),
+ )
+ }
+}
+
+
+export const isMutation = (u: unknown): u is Mutation => Predicate.hasProperty(u, MutationTypeId)
+
+
+export declare namespace make {
+ export interface Options {
+ readonly f: (key: K) => Effect.Effect>>
+ readonly initialProgress?: P
+ }
+}
+
+export const make = Effect.fnUntraced(function* (
+ options: make.Options
+): Effect.fn.Return<
+ Mutation, P>,
+ never,
+ Scope.Scope | Result.forkEffect.OutputContext
+> {
+ return new MutationImpl(
+ yield* Effect.context>(),
+ options.f as any,
+ options.initialProgress as P,
+
+ yield* SubscriptionRef.make(Option.none()),
+ yield* SubscriptionRef.make(Option.none>()),
+ yield* SubscriptionRef.make(Result.initial()),
+ yield* SubscriptionRef.make(Option.none>()),
+ )
+})
+
+const fromSubscriptionRef = (ref: SubscriptionRef.SubscriptionRef): Subscribable.Subscribable => Subscribable.make({
+ get: SubscriptionRef.get(ref),
+ changes: SubscriptionRef.changes(ref),
+})
diff --git a/packages/effect-fc-next/src/PubSub.ts b/packages/effect-fc-next/src/PubSub.ts
new file mode 100644
index 0000000..27401bc
--- /dev/null
+++ b/packages/effect-fc-next/src/PubSub.ts
@@ -0,0 +1,17 @@
+import { Effect, PubSub, type Scope } from "effect"
+import type * as React from "react"
+import * as Component from "./Component.js"
+
+
+export const useFromReactiveValues = Effect.fnUntraced(function* (
+ values: A
+): Effect.fn.Return, never, Scope.Scope> {
+ const pubsub = yield* Component.useOnMount(() => Effect.acquireRelease(PubSub.unbounded(), PubSub.shutdown))
+ yield* Component.useReactEffect(() => Effect.flatMap(
+ PubSub.isShutdown(pubsub),
+ shutdown => shutdown ? Effect.succeed(undefined) : Effect.asVoid(PubSub.publish(pubsub, values)),
+ ), values)
+ return pubsub
+})
+
+export * from "effect/PubSub"
diff --git a/packages/effect-fc-next/src/Query.ts b/packages/effect-fc-next/src/Query.ts
new file mode 100644
index 0000000..e9e317b
--- /dev/null
+++ b/packages/effect-fc-next/src/Query.ts
@@ -0,0 +1,283 @@
+import {
+ type Cause,
+ type Context,
+ type Duration,
+ Effect,
+ Equal,
+ Fiber,
+ Option,
+ Pipeable,
+ Predicate,
+ type Scope,
+ Semaphore,
+ Stream,
+ SubscriptionRef,
+} from "effect"
+import { Subscribable } from "effect-lens"
+import * as QueryClient from "./QueryClient.js"
+import * as Result from "./Result.js"
+
+
+export const QueryTypeId: unique symbol = Symbol.for("@effect-fc/Query/Query")
+export type QueryTypeId = typeof QueryTypeId
+
+export interface Query
+extends Pipeable.Pipeable {
+ readonly [QueryTypeId]: QueryTypeId
+ readonly context: Context.Context
+ readonly key: Stream.Stream
+ readonly f: (key: K) => Effect.Effect
+ readonly initialProgress: P
+ readonly staleTime: Duration.Input
+ readonly refreshOnWindowFocus: boolean
+ readonly latestKey: Subscribable.Subscribable>
+ readonly fiber: Subscribable.Subscribable>>
+ readonly result: Subscribable.Subscribable>
+ readonly latestFinalResult: Subscribable.Subscribable>>
+ readonly run: Effect.Effect