Compare commits
23 Commits
result
...
renovate/v
| Author | SHA1 | Date | |
|---|---|---|---|
| 2d61549807 | |||
|
|
2457b1c536 | ||
|
|
3cb3f6d103 | ||
|
|
46d7aacc69 | ||
|
|
2f118c5f98 | ||
|
|
0ba00a0b4f | ||
|
|
c644f8c44b | ||
|
|
6917c72101 | ||
|
|
9b3ce62d3e | ||
|
|
8b69d4e500 | ||
|
|
a9c0590b7c | ||
|
|
b63d1ab2c7 | ||
|
|
df86af839e | ||
|
|
dbe42aadb1 | ||
|
|
355e179fbd | ||
|
|
8dd40d3365 | ||
|
|
929f835e94 | ||
|
|
1f47887643 | ||
|
|
3794f56a86 | ||
|
|
7f8f91bfc5 | ||
|
|
45b38d6c1f | ||
|
|
2080d35b2c | ||
|
|
346ba9066b |
@@ -1,31 +1,47 @@
|
||||
/** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */
|
||||
import { Effect, Function, Predicate, Runtime, Scope } from "effect"
|
||||
import { Effect, type Equivalence, Function, Predicate, Runtime, Scope } from "effect"
|
||||
import * as React from "react"
|
||||
import * as Component from "./Component.js"
|
||||
|
||||
|
||||
export const TypeId: unique symbol = Symbol.for("@effect-fc/Async/Async")
|
||||
export type TypeId = typeof TypeId
|
||||
export const AsyncTypeId: unique symbol = Symbol.for("@effect-fc/Async/Async")
|
||||
export type AsyncTypeId = typeof AsyncTypeId
|
||||
|
||||
export interface Async extends AsyncOptions {
|
||||
readonly [TypeId]: TypeId
|
||||
|
||||
/**
|
||||
* A trait for `Component`'s that allows them running asynchronous effects.
|
||||
*/
|
||||
export interface Async extends AsyncPrototype, AsyncOptions {}
|
||||
|
||||
export interface AsyncPrototype {
|
||||
readonly [AsyncTypeId]: AsyncTypeId
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for `Async` components.
|
||||
*/
|
||||
export interface AsyncOptions {
|
||||
/**
|
||||
* The default fallback React node to display while the async operation is pending.
|
||||
* Used if no fallback is provided to the component when rendering.
|
||||
*/
|
||||
readonly defaultFallback?: React.ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for `Async` components.
|
||||
*/
|
||||
export type AsyncProps = Omit<React.SuspenseProps, "children">
|
||||
|
||||
|
||||
export const AsyncPrototype = Object.freeze({
|
||||
[TypeId]: TypeId,
|
||||
export const AsyncPrototype: AsyncPrototype = Object.freeze({
|
||||
[AsyncTypeId]: AsyncTypeId,
|
||||
|
||||
asFunctionComponent<P extends {}, A extends React.ReactNode, E, R>(
|
||||
this: Component.Component<P, A, E, R> & Async,
|
||||
runtimeRef: React.RefObject<Runtime.Runtime<Exclude<R, Scope.Scope>>>,
|
||||
) {
|
||||
const SuspenseInner = (props: { readonly promise: Promise<React.ReactNode> }) => React.use(props.promise)
|
||||
const Inner = (props: { readonly promise: Promise<React.ReactNode> }) => React.use(props.promise)
|
||||
|
||||
return ({ fallback, name, ...props }: AsyncProps) => {
|
||||
const promise = Runtime.runPromise(runtimeRef.current)(
|
||||
@@ -38,17 +54,64 @@ export const AsyncPrototype = Object.freeze({
|
||||
return React.createElement(
|
||||
React.Suspense,
|
||||
{ fallback: fallback ?? this.defaultFallback, name },
|
||||
React.createElement(SuspenseInner, { promise }),
|
||||
React.createElement(Inner, { promise }),
|
||||
)
|
||||
}
|
||||
},
|
||||
} as const)
|
||||
|
||||
/**
|
||||
* An equivalence function for comparing `AsyncProps` that ignores the `fallback` property.
|
||||
* Used by default by async components with `Memoized.memoized` applied.
|
||||
*/
|
||||
export const defaultPropsEquivalence: Equivalence.Equivalence<AsyncProps> = (
|
||||
self: Record<string, unknown>,
|
||||
that: Record<string, unknown>,
|
||||
) => {
|
||||
if (self === that)
|
||||
return true
|
||||
|
||||
export const isAsync = (u: unknown): u is Async => Predicate.hasProperty(u, TypeId)
|
||||
for (const key in self) {
|
||||
if (key === "fallback")
|
||||
continue
|
||||
if (!(key in that) || !Object.is(self[key], that[key]))
|
||||
return false
|
||||
}
|
||||
|
||||
for (const key in that) {
|
||||
if (key === "fallback")
|
||||
continue
|
||||
if (!(key in self))
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
export const isAsync = (u: unknown): u is Async => Predicate.hasProperty(u, AsyncTypeId)
|
||||
|
||||
/**
|
||||
* Converts a Component into an `Async` component that supports running asynchronous effects.
|
||||
*
|
||||
* Note: The component cannot have a prop named "promise" as it's reserved for internal use.
|
||||
*
|
||||
* @param self - The component to convert to an Async component
|
||||
* @returns A new `Async` component with the same body, error, and context types as the input
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const MyAsyncComponent = MyComponent.pipe(
|
||||
* Async.async,
|
||||
* )
|
||||
* ```
|
||||
*/
|
||||
export const async = <T extends Component.Component<any, any, any, any>>(
|
||||
self: T
|
||||
self: T & (
|
||||
"promise" extends keyof Component.Component.Props<T>
|
||||
? "The 'promise' prop name is restricted for Async components. Please rename the 'promise' prop to something else."
|
||||
: T
|
||||
)
|
||||
): (
|
||||
& Omit<T, keyof Component.Component.AsComponent<T>>
|
||||
& Component.Component<
|
||||
@@ -59,13 +122,37 @@ export const async = <T extends Component.Component<any, any, any, any>>(
|
||||
>
|
||||
& Async
|
||||
) => Object.setPrototypeOf(
|
||||
Object.assign(function() {}, self),
|
||||
Object.assign(function() {}, self, { propsEquivalence: defaultPropsEquivalence }),
|
||||
Object.freeze(Object.setPrototypeOf(
|
||||
Object.assign({}, AsyncPrototype),
|
||||
Object.getPrototypeOf(self),
|
||||
)),
|
||||
)
|
||||
|
||||
/**
|
||||
* Applies options to an Async component, returning a new Async component with the updated configuration.
|
||||
*
|
||||
* Supports both curried and uncurried application styles.
|
||||
*
|
||||
* @param self - The Async component to apply options to (in uncurried form)
|
||||
* @param options - The options to apply to the component
|
||||
* @returns An Async component with the applied options
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Curried
|
||||
* const MyAsyncComponent = MyComponent.pipe(
|
||||
* Async.async,
|
||||
* Async.withOptions({ defaultFallback: <p>Loading...</p> }),
|
||||
* )
|
||||
*
|
||||
* // Uncurried
|
||||
* const MyAsyncComponent = Async.withOptions(
|
||||
* Async.async(MyComponent),
|
||||
* { defaultFallback: <p>Loading...</p> },
|
||||
* )
|
||||
* ```
|
||||
*/
|
||||
export const withOptions: {
|
||||
<T extends Component.Component<any, any, any, any> & Async>(
|
||||
options: Partial<AsyncOptions>
|
||||
|
||||
@@ -4,8 +4,8 @@ import { Context, type Duration, Effect, Equivalence, ExecutionStrategy, Exit, F
|
||||
import * as React from "react"
|
||||
|
||||
|
||||
export const TypeId: unique symbol = Symbol.for("@effect-fc/Component/Component")
|
||||
export type TypeId = typeof TypeId
|
||||
export const ComponentTypeId: unique symbol = Symbol.for("@effect-fc/Component/Component")
|
||||
export type ComponentTypeId = typeof ComponentTypeId
|
||||
|
||||
/**
|
||||
* Represents an Effect-based React Component that integrates the Effect system with React.
|
||||
@@ -13,7 +13,7 @@ export type TypeId = typeof TypeId
|
||||
export interface Component<P extends {}, A extends React.ReactNode, E, R>
|
||||
extends ComponentPrototype<P, A, R>, ComponentOptions {
|
||||
new(_: never): Record<string, never>
|
||||
readonly [TypeId]: TypeId
|
||||
readonly [ComponentTypeId]: ComponentTypeId
|
||||
readonly "~Props": P
|
||||
readonly "~Success": A
|
||||
readonly "~Error": E
|
||||
@@ -34,7 +34,7 @@ export declare namespace Component {
|
||||
|
||||
export interface ComponentPrototype<P extends {}, A extends React.ReactNode, R>
|
||||
extends Pipeable.Pipeable {
|
||||
readonly [TypeId]: TypeId
|
||||
readonly [ComponentTypeId]: ComponentTypeId
|
||||
readonly use: Effect.Effect<(props: P) => A, never, Exclude<R, Scope.Scope>>
|
||||
|
||||
asFunctionComponent(
|
||||
@@ -46,7 +46,7 @@ extends Pipeable.Pipeable {
|
||||
}
|
||||
|
||||
export const ComponentPrototype: ComponentPrototype<any, any, any> = Object.freeze({
|
||||
[TypeId]: TypeId,
|
||||
[ComponentTypeId]: ComponentTypeId,
|
||||
...Pipeable.Prototype,
|
||||
|
||||
get use() { return use(this) },
|
||||
@@ -88,7 +88,7 @@ const use = Effect.fnUntraced(function* <P extends {}, A extends React.ReactNode
|
||||
}),
|
||||
Equivalence.array(Equivalence.strict()),
|
||||
)))[0](Array.from(
|
||||
Context.omit(...nonReactiveTags)(runtimeRef.current.context).unsafeMap.values()
|
||||
Context.omit(...self.nonReactiveTags)(runtimeRef.current.context).unsafeMap.values()
|
||||
))
|
||||
})
|
||||
|
||||
@@ -96,10 +96,16 @@ const use = Effect.fnUntraced(function* <P extends {}, A extends React.ReactNode
|
||||
export interface ComponentOptions {
|
||||
/**
|
||||
* Custom display name for the component in React DevTools and debugging utilities.
|
||||
* Improves developer experience by providing meaningful component identification.
|
||||
*/
|
||||
readonly displayName?: string
|
||||
|
||||
/**
|
||||
* Context tags that should not trigger component remount when their values change.
|
||||
*
|
||||
* @default [Tracer.ParentSpan]
|
||||
*/
|
||||
readonly nonReactiveTags: readonly Context.Tag<any, any>[]
|
||||
|
||||
/**
|
||||
* Specifies the execution strategy for finalizers when the component unmounts or its scope closes.
|
||||
* Determines whether finalizers execute sequentially or in parallel.
|
||||
@@ -119,15 +125,13 @@ export interface ComponentOptions {
|
||||
}
|
||||
|
||||
export const defaultOptions: ComponentOptions = {
|
||||
nonReactiveTags: [Tracer.ParentSpan],
|
||||
finalizerExecutionStrategy: ExecutionStrategy.sequential,
|
||||
finalizerExecutionDebounce: "100 millis",
|
||||
}
|
||||
|
||||
|
||||
export const nonReactiveTags = [Tracer.ParentSpan] as const
|
||||
|
||||
|
||||
export const isComponent = (u: unknown): u is Component<{}, React.ReactNode, unknown, unknown> => Predicate.hasProperty(u, TypeId)
|
||||
export const isComponent = (u: unknown): u is Component<{}, React.ReactNode, unknown, unknown> => Predicate.hasProperty(u, ComponentTypeId)
|
||||
|
||||
export declare namespace make {
|
||||
export type Gen = {
|
||||
@@ -507,7 +511,7 @@ export const makeUntraced: (
|
||||
* const MyComponentWithCustomOptions = MyComponent.pipe(
|
||||
* Component.withOptions({
|
||||
* finalizerExecutionStrategy: ExecutionStrategy.parallel,
|
||||
* finalizerExecutionDebounce: "50 millis"
|
||||
* finalizerExecutionDebounce: "50 millis",
|
||||
* })
|
||||
* )
|
||||
* ```
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { type Cause, Context, Effect, Exit, Layer, Option, Pipeable, Predicate, PubSub, type Queue, type Scope, Supervisor } from "effect"
|
||||
|
||||
|
||||
export const TypeId: unique symbol = Symbol.for("@effect-fc/ErrorObserver/ErrorObserver")
|
||||
export type TypeId = typeof TypeId
|
||||
export const ErrorObserverTypeId: unique symbol = Symbol.for("@effect-fc/ErrorObserver/ErrorObserver")
|
||||
export type ErrorObserverTypeId = typeof ErrorObserverTypeId
|
||||
|
||||
export interface ErrorObserver<in out E = never> extends Pipeable.Pipeable {
|
||||
readonly [TypeId]: TypeId
|
||||
readonly [ErrorObserverTypeId]: ErrorObserverTypeId
|
||||
handle<A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R>
|
||||
readonly subscribe: Effect.Effect<Queue.Dequeue<Cause.Cause<E>>, never, Scope.Scope>
|
||||
}
|
||||
|
||||
export const ErrorObserver = <E = never>(): Context.Tag<ErrorObserver, ErrorObserver<E>> => Context.GenericTag("@effect-fc/ErrorObserver/ErrorObserver")
|
||||
|
||||
class ErrorObserverImpl<in out E = never>
|
||||
export class ErrorObserverImpl<in out E = never>
|
||||
extends Pipeable.Class() implements ErrorObserver<E> {
|
||||
readonly [TypeId]: TypeId = TypeId
|
||||
readonly [ErrorObserverTypeId]: ErrorObserverTypeId = ErrorObserverTypeId
|
||||
readonly subscribe: Effect.Effect<Queue.Dequeue<Cause.Cause<E>>, never, Scope.Scope>
|
||||
|
||||
constructor(
|
||||
@@ -29,7 +29,7 @@ extends Pipeable.Class() implements ErrorObserver<E> {
|
||||
}
|
||||
}
|
||||
|
||||
class ErrorObserverSupervisorImpl extends Supervisor.AbstractSupervisor<void> {
|
||||
export class ErrorObserverSupervisorImpl extends Supervisor.AbstractSupervisor<void> {
|
||||
readonly value = Effect.void
|
||||
constructor(readonly pubsub: PubSub.PubSub<Cause.Cause<never>>) {
|
||||
super()
|
||||
@@ -43,7 +43,7 @@ class ErrorObserverSupervisorImpl extends Supervisor.AbstractSupervisor<void> {
|
||||
}
|
||||
|
||||
|
||||
export const isErrorObserver = (u: unknown): u is ErrorObserver<unknown> => Predicate.hasProperty(u, TypeId)
|
||||
export const isErrorObserver = (u: unknown): u is ErrorObserver<unknown> => Predicate.hasProperty(u, ErrorObserverTypeId)
|
||||
|
||||
export const layer: Layer.Layer<ErrorObserver> = Layer.unwrapEffect(Effect.map(
|
||||
PubSub.unbounded<Cause.Cause<never>>(),
|
||||
|
||||
@@ -4,20 +4,38 @@ import * as React from "react"
|
||||
import type * as Component from "./Component.js"
|
||||
|
||||
|
||||
export const TypeId: unique symbol = Symbol.for("@effect-fc/Memoized/Memoized")
|
||||
export type TypeId = typeof TypeId
|
||||
export const MemoizedTypeId: unique symbol = Symbol.for("@effect-fc/Memoized/Memoized")
|
||||
export type MemoizedTypeId = typeof MemoizedTypeId
|
||||
|
||||
export interface Memoized<P> extends MemoizedOptions<P> {
|
||||
readonly [TypeId]: TypeId
|
||||
|
||||
/**
|
||||
* 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<P> extends MemoizedPrototype, MemoizedOptions<P> {}
|
||||
|
||||
export interface MemoizedPrototype {
|
||||
readonly [MemoizedTypeId]: MemoizedTypeId
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for Memoized components.
|
||||
*
|
||||
* @template P The props type of the component
|
||||
*/
|
||||
export interface MemoizedOptions<P> {
|
||||
/**
|
||||
* 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<P>
|
||||
}
|
||||
|
||||
|
||||
export const MemoizedPrototype = Object.freeze({
|
||||
[TypeId]: TypeId,
|
||||
export const MemoizedPrototype: MemoizedPrototype = Object.freeze({
|
||||
[MemoizedTypeId]: MemoizedTypeId,
|
||||
|
||||
transformFunctionComponent<P extends {}>(
|
||||
this: Memoized<P>,
|
||||
@@ -28,8 +46,21 @@ export const MemoizedPrototype = Object.freeze({
|
||||
} as const)
|
||||
|
||||
|
||||
export const isMemoized = (u: unknown): u is Memoized<unknown> => Predicate.hasProperty(u, TypeId)
|
||||
export const isMemoized = (u: unknown): u is Memoized<unknown> => 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 = <T extends Component.Component<any, any, any, any>>(
|
||||
self: T
|
||||
): T & Memoized<Component.Component.Props<T>> => Object.setPrototypeOf(
|
||||
@@ -40,6 +71,30 @@ export const memoized = <T extends Component.Component<any, any, any, any>>(
|
||||
)),
|
||||
)
|
||||
|
||||
/**
|
||||
* 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: {
|
||||
<T extends Component.Component<any, any, any, any> & Memoized<any>>(
|
||||
options: Partial<MemoizedOptions<Component.Component.Props<T>>>
|
||||
|
||||
@@ -10,7 +10,7 @@ export interface Query<in out K extends Query.AnyKey, in out A, in out KE = neve
|
||||
extends Pipeable.Pipeable {
|
||||
readonly [QueryTypeId]: QueryTypeId
|
||||
|
||||
readonly context: Context.Context<Scope.Scope | QueryClient.QueryClient | R>
|
||||
readonly context: Context.Context<Scope.Scope | QueryClient.QueryClient | KR | R>
|
||||
readonly key: Stream.Stream<K, KE, KR>
|
||||
readonly f: (key: K) => Effect.Effect<A, E, R>
|
||||
readonly initialProgress: P
|
||||
@@ -287,7 +287,7 @@ export const make = Effect.fnUntraced(function* <K extends Query.AnyKey, A, KE =
|
||||
> {
|
||||
const client = yield* QueryClient.QueryClient
|
||||
|
||||
return new QueryImpl(
|
||||
return new QueryImpl<K, A, KE, KR, E, Result.forkEffect.OutputContext<A, E, R, P>, P>(
|
||||
yield* Effect.context<Scope.Scope | QueryClient.QueryClient | KR | Result.forkEffect.OutputContext<A, E, R, P>>(),
|
||||
options.key,
|
||||
options.f as any,
|
||||
|
||||
@@ -240,16 +240,16 @@ export const unsafeForkEffect = <A, E, R, P = never>(
|
||||
Effect.provide(Layer.empty.pipe(
|
||||
Layer.provideMerge(makeProgressLayer<A, E, P>()),
|
||||
Layer.provideMerge(Layer.succeed(State<A, E, P>(), {
|
||||
get: ref,
|
||||
get: Ref.get(ref),
|
||||
set: v => Effect.andThen(Ref.set(ref, v), PubSub.publish(pubsub, v))
|
||||
})),
|
||||
)),
|
||||
))),
|
||||
Effect.map(({ ref, pubsub, fiber }) => [
|
||||
Subscribable.make({
|
||||
get: ref,
|
||||
get: Ref.get(ref),
|
||||
changes: Stream.unwrapScoped(Effect.map(
|
||||
Effect.all([ref, Stream.fromPubSub(pubsub, { scoped: true })]),
|
||||
Effect.all([Ref.get(ref), Stream.fromPubSub(pubsub, { scoped: true })]),
|
||||
([latest, stream]) => Stream.concat(Stream.make(latest), stream),
|
||||
)),
|
||||
}),
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"@tanstack/router-plugin": "^1.154.12",
|
||||
"@types/react": "^19.2.9",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"@vitejs/plugin-react": "^6.0.0",
|
||||
"globals": "^17.0.0",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
|
||||
@@ -13,10 +13,10 @@ import { Route as ResultRouteImport } from './routes/result'
|
||||
import { Route as QueryRouteImport } from './routes/query'
|
||||
import { Route as FormRouteImport } from './routes/form'
|
||||
import { Route as BlankRouteImport } from './routes/blank'
|
||||
import { Route as AsyncRouteImport } from './routes/async'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
import { Route as DevMemoRouteImport } from './routes/dev/memo'
|
||||
import { Route as DevContextRouteImport } from './routes/dev/context'
|
||||
import { Route as DevAsyncRenderingRouteImport } from './routes/dev/async-rendering'
|
||||
|
||||
const ResultRoute = ResultRouteImport.update({
|
||||
id: '/result',
|
||||
@@ -38,6 +38,11 @@ const BlankRoute = BlankRouteImport.update({
|
||||
path: '/blank',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const AsyncRoute = AsyncRouteImport.update({
|
||||
id: '/async',
|
||||
path: '/async',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
@@ -53,40 +58,35 @@ const DevContextRoute = DevContextRouteImport.update({
|
||||
path: '/dev/context',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const DevAsyncRenderingRoute = DevAsyncRenderingRouteImport.update({
|
||||
id: '/dev/async-rendering',
|
||||
path: '/dev/async-rendering',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/async': typeof AsyncRoute
|
||||
'/blank': typeof BlankRoute
|
||||
'/form': typeof FormRoute
|
||||
'/query': typeof QueryRoute
|
||||
'/result': typeof ResultRoute
|
||||
'/dev/async-rendering': typeof DevAsyncRenderingRoute
|
||||
'/dev/context': typeof DevContextRoute
|
||||
'/dev/memo': typeof DevMemoRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/async': typeof AsyncRoute
|
||||
'/blank': typeof BlankRoute
|
||||
'/form': typeof FormRoute
|
||||
'/query': typeof QueryRoute
|
||||
'/result': typeof ResultRoute
|
||||
'/dev/async-rendering': typeof DevAsyncRenderingRoute
|
||||
'/dev/context': typeof DevContextRoute
|
||||
'/dev/memo': typeof DevMemoRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
'/async': typeof AsyncRoute
|
||||
'/blank': typeof BlankRoute
|
||||
'/form': typeof FormRoute
|
||||
'/query': typeof QueryRoute
|
||||
'/result': typeof ResultRoute
|
||||
'/dev/async-rendering': typeof DevAsyncRenderingRoute
|
||||
'/dev/context': typeof DevContextRoute
|
||||
'/dev/memo': typeof DevMemoRoute
|
||||
}
|
||||
@@ -94,42 +94,42 @@ export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths:
|
||||
| '/'
|
||||
| '/async'
|
||||
| '/blank'
|
||||
| '/form'
|
||||
| '/query'
|
||||
| '/result'
|
||||
| '/dev/async-rendering'
|
||||
| '/dev/context'
|
||||
| '/dev/memo'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
| '/'
|
||||
| '/async'
|
||||
| '/blank'
|
||||
| '/form'
|
||||
| '/query'
|
||||
| '/result'
|
||||
| '/dev/async-rendering'
|
||||
| '/dev/context'
|
||||
| '/dev/memo'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
| '/async'
|
||||
| '/blank'
|
||||
| '/form'
|
||||
| '/query'
|
||||
| '/result'
|
||||
| '/dev/async-rendering'
|
||||
| '/dev/context'
|
||||
| '/dev/memo'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
AsyncRoute: typeof AsyncRoute
|
||||
BlankRoute: typeof BlankRoute
|
||||
FormRoute: typeof FormRoute
|
||||
QueryRoute: typeof QueryRoute
|
||||
ResultRoute: typeof ResultRoute
|
||||
DevAsyncRenderingRoute: typeof DevAsyncRenderingRoute
|
||||
DevContextRoute: typeof DevContextRoute
|
||||
DevMemoRoute: typeof DevMemoRoute
|
||||
}
|
||||
@@ -164,6 +164,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof BlankRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/async': {
|
||||
id: '/async'
|
||||
path: '/async'
|
||||
fullPath: '/async'
|
||||
preLoaderRoute: typeof AsyncRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/': {
|
||||
id: '/'
|
||||
path: '/'
|
||||
@@ -185,23 +192,16 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof DevContextRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/dev/async-rendering': {
|
||||
id: '/dev/async-rendering'
|
||||
path: '/dev/async-rendering'
|
||||
fullPath: '/dev/async-rendering'
|
||||
preLoaderRoute: typeof DevAsyncRenderingRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
AsyncRoute: AsyncRoute,
|
||||
BlankRoute: BlankRoute,
|
||||
FormRoute: FormRoute,
|
||||
QueryRoute: QueryRoute,
|
||||
ResultRoute: ResultRoute,
|
||||
DevAsyncRenderingRoute: DevAsyncRenderingRoute,
|
||||
DevContextRoute: DevContextRoute,
|
||||
DevMemoRoute: DevMemoRoute,
|
||||
}
|
||||
|
||||
71
packages/example/src/routes/async.tsx
Normal file
71
packages/example/src/routes/async.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { HttpClient } from "@effect/platform"
|
||||
import { Container, Flex, Heading, Slider, Text, TextField } from "@radix-ui/themes"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { Array, Effect, flow, Option, Schema } from "effect"
|
||||
import { Async, Component, Memoized } from "effect-fc"
|
||||
import * as React from "react"
|
||||
import { runtime } from "@/runtime"
|
||||
|
||||
|
||||
const Post = Schema.Struct({
|
||||
userId: Schema.Int,
|
||||
id: Schema.Int,
|
||||
title: Schema.String,
|
||||
body: Schema.String,
|
||||
})
|
||||
|
||||
interface AsyncFetchPostViewProps {
|
||||
readonly id: number
|
||||
}
|
||||
|
||||
class AsyncFetchPostView extends Component.make("AsyncFetchPostView")(function*(props: AsyncFetchPostViewProps) {
|
||||
const post = yield* Component.useOnChange(() => HttpClient.HttpClient.pipe(
|
||||
Effect.tap(Effect.sleep("500 millis")),
|
||||
Effect.andThen(client => client.get(`https://jsonplaceholder.typicode.com/posts/${ props.id }`)),
|
||||
Effect.andThen(response => response.json),
|
||||
Effect.andThen(Schema.decodeUnknown(Post)),
|
||||
), [props.id])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Heading>{post.title}</Heading>
|
||||
<Text>{post.body}</Text>
|
||||
</div>
|
||||
)
|
||||
}).pipe(
|
||||
Async.async,
|
||||
Async.withOptions({ defaultFallback: <Text>Default fallback</Text> }),
|
||||
Memoized.memoized,
|
||||
) {}
|
||||
|
||||
|
||||
const AsyncRouteComponent = Component.make("AsyncRouteView")(function*() {
|
||||
const [text, setText] = React.useState("Typing here should not trigger a refetch of the post")
|
||||
const [id, setId] = React.useState(1)
|
||||
|
||||
const AsyncFetchPost = yield* AsyncFetchPostView.use
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Flex direction="column" align="stretch" gap="2">
|
||||
<TextField.Root
|
||||
value={text}
|
||||
onChange={e => setText(e.currentTarget.value)}
|
||||
/>
|
||||
|
||||
<Slider
|
||||
value={[id]}
|
||||
onValueChange={flow(Array.head, Option.getOrThrow, setId)}
|
||||
/>
|
||||
|
||||
<AsyncFetchPost id={id} fallback={<Text>Loading post...</Text>} />
|
||||
</Flex>
|
||||
</Container>
|
||||
)
|
||||
}).pipe(
|
||||
Component.withRuntime(runtime.context)
|
||||
)
|
||||
|
||||
export const Route = createFileRoute("/async")({
|
||||
component: AsyncRouteComponent,
|
||||
})
|
||||
@@ -1,78 +0,0 @@
|
||||
import { Flex, Text, TextField } from "@radix-ui/themes"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
||||
import { Effect } from "effect"
|
||||
import { Async, Component, Memoized } from "effect-fc"
|
||||
import * as React from "react"
|
||||
import { runtime } from "@/runtime"
|
||||
|
||||
|
||||
// Generator version
|
||||
const RouteComponent = Component.makeUntraced(function* AsyncRendering() {
|
||||
const MemoizedAsyncComponentFC = yield* MemoizedAsyncComponent.use
|
||||
const AsyncComponentFC = yield* AsyncComponent.use
|
||||
const [input, setInput] = React.useState("")
|
||||
|
||||
return (
|
||||
<Flex direction="column" align="stretch" gap="2">
|
||||
<TextField.Root
|
||||
value={input}
|
||||
onChange={e => setInput(e.target.value)}
|
||||
/>
|
||||
|
||||
<MemoizedAsyncComponentFC fallback={React.useMemo(() => <p>Loading memoized...</p>, [])} />
|
||||
<AsyncComponentFC />
|
||||
</Flex>
|
||||
)
|
||||
}).pipe(
|
||||
Component.withRuntime(runtime.context)
|
||||
)
|
||||
|
||||
// Pipeline version
|
||||
// const RouteComponent = Component.make("RouteComponent")(() => Effect.Do,
|
||||
// Effect.bind("VMemoizedAsyncComponent", () => Component.useFC(MemoizedAsyncComponent)),
|
||||
// Effect.bind("VAsyncComponent", () => Component.useFC(AsyncComponent)),
|
||||
// Effect.let("input", () => React.useState("")),
|
||||
|
||||
// Effect.map(({ input: [input, setInput], VAsyncComponent, VMemoizedAsyncComponent }) =>
|
||||
// <Flex direction="column" align="stretch" gap="2">
|
||||
// <TextField.Root
|
||||
// value={input}
|
||||
// onChange={e => setInput(e.target.value)}
|
||||
// />
|
||||
|
||||
// <VMemoizedAsyncComponent />
|
||||
// <VAsyncComponent />
|
||||
// </Flex>
|
||||
// ),
|
||||
// ).pipe(
|
||||
// Component.withRuntime(runtime.context)
|
||||
// )
|
||||
|
||||
|
||||
class AsyncComponent extends Component.makeUntraced("AsyncComponent")(function*() {
|
||||
const SubComponentFC = yield* SubComponent.use
|
||||
|
||||
yield* Effect.sleep("500 millis") // Async operation
|
||||
// Cannot use React hooks after the async operation
|
||||
|
||||
return (
|
||||
<Flex direction="column" align="stretch">
|
||||
<Text>Rendered!</Text>
|
||||
<SubComponentFC />
|
||||
</Flex>
|
||||
)
|
||||
}).pipe(
|
||||
Async.async,
|
||||
Async.withOptions({ defaultFallback: <p>Loading...</p> }),
|
||||
) {}
|
||||
class MemoizedAsyncComponent extends Memoized.memoized(AsyncComponent) {}
|
||||
|
||||
class SubComponent extends Component.makeUntraced("SubComponent")(function*() {
|
||||
const [state] = React.useState(yield* Component.useOnMount(() => Effect.provide(makeUuid4, GetRandomValues.CryptoRandom)))
|
||||
return <Text>{state}</Text>
|
||||
}) {}
|
||||
|
||||
export const Route = createFileRoute("/dev/async-rendering")({
|
||||
component: RouteComponent
|
||||
})
|
||||
Reference in New Issue
Block a user