0.1.2 #3

Merged
Thilawyn merged 28 commits from next into master 2025-07-23 21:28:25 +02:00
4 changed files with 110 additions and 94 deletions
Showing only changes of commit aa8feb79fa - Show all commits

View File

@@ -1,7 +1,8 @@
import { Context, Effect, type Equivalence, ExecutionStrategy, Function, pipe, Pipeable, Predicate, Runtime, Scope, String, Tracer, type Utils } from "effect" import { Context, Effect, ExecutionStrategy, Function, pipe, Pipeable, Predicate, Runtime, Scope, String, Tracer, type Utils } from "effect"
import * as React from "react" import * as React from "react"
import * as Hook from "./Hook.js" import * as Hook from "./Hook.js"
import type { ExcludeKeys } from "./utils.js" import * as Memoized from "./Memoized.js"
import * as Suspense from "./Suspense.js"
export const TypeId: unique symbol = Symbol.for("effect-fc/Component") export const TypeId: unique symbol = Symbol.for("effect-fc/Component")
@@ -339,93 +340,15 @@ export const withOptions: {
)) ))
export interface Memoized<P> {
readonly memo: true
readonly memoOptions: Memoized.Options<P>
}
export namespace Memoized {
export interface Options<P> {
readonly propsAreEqual?: Equivalence.Equivalence<P>
}
}
export const memo = <T extends Component<any, any, any> | Component<any, any, any> & Memoized<any>>(
self: T
): T & Memoized<Component.Props<T>> => Object.setPrototypeOf({
...self,
memo: true,
memoOptions: Predicate.hasProperty(self, "memo") ? { ...self.memoOptions } : {},
}, Object.getPrototypeOf(self))
export const withMemoOptions: {
<T extends Component<any, any, any> & Memoized<any>>(
memoOptions: Partial<Memoized.Options<Component.Props<T>>>
): (self: T) => T
<T extends Component<any, any, any> & Memoized<any>>(
self: T,
memoOptions: Partial<Memoized.Options<Component.Props<T>>>,
): T
} = Function.dual(2, <T extends Component<any, any, any> & Memoized<any>>(
self: T,
memoOptions: Partial<Memoized.Options<Component.Props<T>>>,
): T => Object.setPrototypeOf(
{ ...self, memoOptions: { ...self.memoOptions, ...memoOptions } },
Object.getPrototypeOf(self),
))
export interface Suspense {
readonly suspense: true
readonly suspenseOptions: Suspense.Options
}
export namespace Suspense {
export interface Options {
readonly defaultFallback?: React.ReactNode
}
export type Props = Omit<React.SuspenseProps, "children">
}
export const suspense = <T extends Component<any, any, P> | Component<any, any, P> & Suspense, P extends {}>(
self: T & Component<any, any, ExcludeKeys<P, keyof Suspense.Props>>
): (
& T
& Component<Component.Error<T>, Component.Context<T>, P & Suspense.Props>
& Suspense
) => Object.setPrototypeOf({
...self,
suspense: true,
suspenseOptions: Predicate.hasProperty(self, "suspense") ? { ...self.suspenseOptions } : {},
}, Object.getPrototypeOf(self))
export const withSuspenseOptions: {
<T extends Component<any, any, any> & Suspense>(
suspenseOptions: Partial<Suspense.Options>
): (self: T) => T
<T extends Component<any, any, any> & Suspense>(
self: T,
suspenseOptions: Partial<Suspense.Options>,
): T
} = Function.dual(2, <T extends Component<any, any, any> & Suspense>(
self: T,
suspenseOptions: Partial<Suspense.Options>,
): T => Object.setPrototypeOf(
{ ...self, suspense: true, suspenseOptions: { ...self.suspenseOptions, ...suspenseOptions } },
Object.getPrototypeOf(self),
))
export const useFC: { export const useFC: {
<E, R, P extends {}>( <E, R, P extends {}>(
self: Component<E, R, P> & Suspense self: Component<E, R, P> & Suspense.Suspense
): Effect.Effect<React.FC<P & Suspense.Props>, never, Exclude<R, Scope.Scope>> ): Effect.Effect<React.FC<P & Suspense.Suspense.Props>, never, Exclude<R, Scope.Scope>>
<E, R, P extends {}>( <E, R, P extends {}>(
self: Component<E, R, P> self: Component<E, R, P>
): Effect.Effect<React.FC<P>, never, Exclude<R, Scope.Scope>> ): Effect.Effect<React.FC<P>, never, Exclude<R, Scope.Scope>>
} = Effect.fn("useFC")(function* <E, R, P extends {}>( } = Effect.fn("useFC")(function* <E, R, P extends {}>(
self: Component<E, R, P> & (Memoized<P> | Suspense | {}) self: Component<E, R, P> & (Memoized.Memoized<P> | Suspense.Suspense | {})
) { ) {
const runtimeRef = React.useRef<Runtime.Runtime<Exclude<R, Scope.Scope>>>(null!) const runtimeRef = React.useRef<Runtime.Runtime<Exclude<R, Scope.Scope>>>(null!)
runtimeRef.current = yield* Effect.runtime<Exclude<R, Scope.Scope>>() runtimeRef.current = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
@@ -439,20 +362,20 @@ export const useFC: {
)) ))
const FC = React.useMemo(() => { const FC = React.useMemo(() => {
const f: React.FC<P> = Predicate.hasProperty(self, "suspense") const f: React.FC<P> = Suspense.isSuspense(self)
? pipe( ? pipe(
function SuspenseInner(props: { readonly promise: Promise<React.ReactNode> }) { function SuspenseInner(props: { readonly promise: Promise<React.ReactNode> }) {
return React.use(props.promise) return React.use(props.promise)
}, },
SuspenseInner => ({ fallback, name, ...props }: P & Suspense.Props) => { SuspenseInner => ({ fallback, name, ...props }: P & Suspense.Suspense.Props) => {
const promise = Runtime.runPromise(runtimeRef.current)( const promise = Runtime.runPromise(runtimeRef.current)(
Effect.provideService(self.body(props as P), Scope.Scope, scope) Effect.provideService(self.body(props as P), Scope.Scope, scope)
) )
return React.createElement( return React.createElement(
React.Suspense, React.Suspense,
{ fallback: fallback ?? self.suspenseOptions.defaultFallback, name }, { fallback: fallback ?? self.defaultFallback, name },
React.createElement(SuspenseInner, { promise }), React.createElement(SuspenseInner, { promise }),
) )
}, },
@@ -462,8 +385,8 @@ export const useFC: {
) )
f.displayName = self.displayName ?? "Anonymous" f.displayName = self.displayName ?? "Anonymous"
return Predicate.hasProperty(self, "memo") return Memoized.isMemoized(self)
? React.memo(f, self.memoOptions.propsAreEqual) ? React.memo(f, self.propsAreEqual)
: f : f
}, [scope]) }, [scope])
@@ -473,8 +396,8 @@ export const useFC: {
export const use: { export const use: {
<E, R, P extends {}>( <E, R, P extends {}>(
self: Component<E, R, P> & Suspense, self: Component<E, R, P> & Suspense.Suspense,
fn: (Component: React.FC<P & Suspense.Props>) => React.ReactNode, fn: (Component: React.FC<P & Suspense.Suspense.Props>) => React.ReactNode,
): Effect.Effect<React.ReactNode, never, Exclude<R, Scope.Scope>> ): Effect.Effect<React.ReactNode, never, Exclude<R, Scope.Scope>>
<E, R, P extends {}>( <E, R, P extends {}>(
self: Component<E, R, P>, self: Component<E, R, P>,
@@ -487,14 +410,14 @@ export const use: {
export const withRuntime: { export const withRuntime: {
<T extends Component<any, R, any>, R>( <T extends Component<any, R, any>, R>(
context: React.Context<Runtime.Runtime<R>>, context: React.Context<Runtime.Runtime<R>>,
): (self: T) => React.FC<T extends Suspense ): (self: T) => React.FC<T extends Suspense.Suspense
? Component.Props<T> & Suspense.Props ? Component.Props<T> & Suspense.Suspense.Props
: Component.Props<T> : Component.Props<T>
> >
<E, R, P extends {}>( <E, R, P extends {}>(
self: Component<E, R, P> & Suspense, self: Component<E, R, P> & Suspense.Suspense,
context: React.Context<Runtime.Runtime<R>>, context: React.Context<Runtime.Runtime<R>>,
): React.FC<P & Suspense.Props> ): React.FC<P & Suspense.Suspense.Props>
<E, R, P extends {}>( <E, R, P extends {}>(
self: Component<E, R, P>, self: Component<E, R, P>,
context: React.Context<Runtime.Runtime<R>>, context: React.Context<Runtime.Runtime<R>>,

View File

@@ -0,0 +1,42 @@
import { type Equivalence, Function, Predicate } from "effect"
import type * as Component from "./Component.js"
export const TypeId: unique symbol = Symbol.for("effect-fc/Memoized")
export type TypeId = typeof TypeId
export interface Memoized<P> extends Memoized.Options<P> {
readonly [TypeId]: true
}
export namespace Memoized {
export interface Options<P> {
readonly propsAreEqual?: Equivalence.Equivalence<P>
}
}
export const isMemoized = (u: unknown): u is Memoized<any> => Predicate.hasProperty(u, TypeId)
export const memo = <T extends Component.Component<any, any, any>>(
self: T
): T & Memoized<Component.Component.Props<T>> => Object.setPrototypeOf(
{ ...self, [TypeId]: true },
Object.getPrototypeOf(self),
)
export const withOptions: {
<T extends Component.Component<any, any, any> & Memoized<any>>(
options: Partial<Memoized.Options<Component.Component.Props<T>>>
): (self: T) => T
<T extends Component.Component<any, any, any> & Memoized<any>>(
self: T,
options: Partial<Memoized.Options<Component.Component.Props<T>>>,
): T
} = Function.dual(2, <T extends Component.Component<any, any, any> & Memoized<any>>(
self: T,
options: Partial<Memoized.Options<Component.Component.Props<T>>>,
): T => Object.setPrototypeOf(
{ ...self, ...options },
Object.getPrototypeOf(self),
))

View File

@@ -0,0 +1,49 @@
import { Function, Predicate } from "effect"
import type * as Component from "./Component.js"
import type { ExcludeKeys } from "./utils.js"
export const TypeId: unique symbol = Symbol.for("effect-fc/Suspense")
export type TypeId = typeof TypeId
export interface Suspense extends Suspense.Options {
readonly [TypeId]: true
}
export namespace Suspense {
export interface Options {
readonly defaultFallback?: React.ReactNode
}
export type Props = Omit<React.SuspenseProps, "children">
}
export const isSuspense = (u: unknown): u is Suspense => Predicate.hasProperty(u, TypeId)
export const suspense = <T extends Component.Component<any, any, P>, P extends {}>(
self: T & Component.Component<any, any, ExcludeKeys<P, keyof Suspense.Props>>
): (
& T
& Component.Component<Component.Component.Error<T>, Component.Component.Context<T>, P & Suspense.Props>
& Suspense
) => Object.setPrototypeOf(
{ ...self, [TypeId]: true },
Object.getPrototypeOf(self),
)
export const withOptions: {
<T extends Component.Component<any, any, any> & Suspense>(
options: Partial<Suspense.Options>
): (self: T) => T
<T extends Component.Component<any, any, any> & Suspense>(
self: T,
options: Partial<Suspense.Options>,
): T
} = Function.dual(2, <T extends Component.Component<any, any, any> & Suspense>(
self: T,
options: Partial<Suspense.Options>,
): T => Object.setPrototypeOf(
{ ...self, ...options },
Object.getPrototypeOf(self),
))

View File

@@ -1,3 +1,5 @@
export * as Component from "./Component.js" export * as Component from "./Component.js"
export * as Hook from "./Hook.js" export * as Hook from "./Hook.js"
export * as Memoized from "./Memoized.js"
export * as ReactManagedRuntime from "./ReactManagedRuntime.js" export * as ReactManagedRuntime from "./ReactManagedRuntime.js"
export * as Suspense from "./Suspense.js"