From 3794f56a86137731910d04bd19b3a470555d1124 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Tue, 3 Mar 2026 13:37:27 +0100 Subject: [PATCH] Async example --- packages/example/src/routeTree.gen.ts | 42 +++++----- packages/example/src/routes/async.tsx | 61 +++++++++++++++ .../src/routes/dev/async-rendering.tsx | 78 ------------------- 3 files changed, 82 insertions(+), 99 deletions(-) create mode 100644 packages/example/src/routes/async.tsx delete mode 100644 packages/example/src/routes/dev/async-rendering.tsx diff --git a/packages/example/src/routeTree.gen.ts b/packages/example/src/routeTree.gen.ts index 779c018..0e6de17 100644 --- a/packages/example/src/routeTree.gen.ts +++ b/packages/example/src/routeTree.gen.ts @@ -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, } diff --git a/packages/example/src/routes/async.tsx b/packages/example/src/routes/async.tsx new file mode 100644 index 0000000..d2c9d25 --- /dev/null +++ b/packages/example/src/routes/async.tsx @@ -0,0 +1,61 @@ +import { HttpClient } from "@effect/platform" +import { Container, Flex, Heading, Slider, Text } 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
+ {post.title} + {post.body} +
+}).pipe( + Async.async, + Memoized.memoized, +) {} + + +const AsyncRouteComponent = Component.make("AsyncRouteView")(function*() { + const [id, setId] = React.useState(1) + const AsyncFetchPost = yield* AsyncFetchPostView.use + + return ( + + + + + + + + ) +}).pipe( + Component.withRuntime(runtime.context) +) + +export const Route = createFileRoute("/async")({ + component: AsyncRouteComponent, +}) diff --git a/packages/example/src/routes/dev/async-rendering.tsx b/packages/example/src/routes/dev/async-rendering.tsx deleted file mode 100644 index c057745..0000000 --- a/packages/example/src/routes/dev/async-rendering.tsx +++ /dev/null @@ -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 ( - - setInput(e.target.value)} - /> - -

Loading memoized...

, [])} /> - -
- ) -}).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 }) => -// -// setInput(e.target.value)} -// /> - -// -// -// -// ), -// ).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 ( - - Rendered! - - - ) -}).pipe( - Async.async, - Async.withOptions({ defaultFallback:

Loading...

}), -) {} -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 {state} -}) {} - -export const Route = createFileRoute("/dev/async-rendering")({ - component: RouteComponent -})