Fix Query lifecycle
Lint / lint (push) Successful in 39s

This commit is contained in:
Julien Valverdé
2026-07-19 20:41:59 +02:00
parent cc01e46997
commit 6551969bb1
4 changed files with 93 additions and 78 deletions
+13 -12
View File
@@ -254,7 +254,17 @@ extends Pipeable.Class implements Query<K, A, E, R> {
}),
})),
Effect.onExit(this.f(previous.key), exit => Lens.update(
Effect.onExit(this.f(previous.key), exit => Effect.all([
Effect.fiberId,
Lens.get(this.fiber),
]).pipe(
Effect.flatMap(([fiberId, fiber]) => Option.match(fiber, {
onSome: v => fiberId === v.id
? Lens.set(this.fiber, Option.none())
: Effect.void,
onNone: () => Effect.void,
})),
Effect.andThen(Lens.updateAndGet(
state,
previous => Exit.match(exit, {
onSuccess: v => ({
@@ -280,17 +290,8 @@ extends Pipeable.Class implements Query<K, A, E, R> {
}),
}),
}),
).pipe(
Effect.andThen(Effect.all([
Effect.fiberId,
Lens.get(this.fiber),
])),
Effect.flatMap(([fiberId, fiber]) => Option.match(fiber, {
onSome: v => fiberId === v.id
? Lens.set(this.fiber, Option.none())
: Effect.void,
onNone: () => Effect.void,
})),
)),
Effect.flatMap(finalState => Lens.set(this.latestFinalState, Option.some(finalState as FinalQueryState<K, A, E>))),
)),
))
-15
View File
@@ -1,15 +0,0 @@
import { Effect } from "effect"
export interface Post {
readonly title: string
readonly body: string
}
export const fetchPost = (id: number) => Effect.tryPromise({
try: () =>
fetch(`https://jsonplaceholder.typicode.com/posts/${id}`)
.then(response => response.json() as Promise<Post>),
catch: () => new Error("Unable to fetch post"),
})
+46 -29
View File
@@ -1,49 +1,68 @@
import { Button, Container, Flex, Heading, Slider, Text } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router"
import { Effect, SubscriptionRef } from "effect"
import { Effect, Schema, SubscriptionRef } from "effect"
import { HttpClient } from "effect/unstable/http"
import { AsyncResult } from "effect/unstable/reactivity"
import { Component, Lens, Mutation, Query, View } from "effect-fc-next"
import { fetchPost, type Post } from "@/post"
import { runtime } from "@/runtime"
const Post = Schema.Struct({
userId: Schema.Int,
id: Schema.Int,
title: Schema.String,
body: Schema.String,
})
interface PostResultViewProps {
readonly result: AsyncResult.AsyncResult<Post, Error>
readonly result: AsyncResult.AsyncResult<typeof Post.Type, Error>
}
const PostResultView = (props: PostResultViewProps) =>
AsyncResult.match(props.result, {
onInitial: () => <Text>Loading...</Text>,
onFailure: () => <Text>Request failed.</Text>,
onSuccess: result => (
<>
const PostResultView = (props: PostResultViewProps) => AsyncResult.match(props.result, {
onInitial: result => result.waiting
? <Text>Loading...</Text>
: <Text>No data.</Text>,
onFailure: result => <Text>Request failed: { result.cause.toString() }</Text>,
onSuccess: result => <>
{result.waiting && <Text>Refreshing...</Text>}
<Heading>{result.value.title}</Heading>
<Text>{result.value.body}</Text>
</>
),
})
</>,
})
const QueryRouteComponent = Component.make("QueryRouteView")(function*() {
const [idLens, query, mutation] = yield* Component.useOnMount(() => Effect.gen(function*() {
const idLens = Lens.fromSubscriptionRef(yield* SubscriptionRef.make(1))
const keyLens = Lens.fromSubscriptionRef(yield* SubscriptionRef.make(["post", 1 as number] as const))
const idLens = Lens.focusTupleAt(keyLens, 1)
const query = yield* Query.service({
key: idLens,
f: fetchPost,
key: keyLens,
f: ([, id]) => HttpClient.HttpClient.pipe(
Effect.tap(Effect.sleep("1 second")),
Effect.andThen(client => client.get(`https://jsonplaceholder.typicode.com/posts/${ id }`)),
Effect.andThen(response => response.json),
Effect.andThen(Schema.decodeUnknownEffect(Post)),
),
staleTime: "10 seconds",
})
const mutation = yield* Mutation.make({
f: fetchPost,
f: ([id]: [id: number]) => HttpClient.HttpClient.pipe(
Effect.tap(Effect.sleep("1 second")),
Effect.andThen(client => client.get(`https://jsonplaceholder.typicode.com/posts/${ id }`)),
Effect.andThen(response => response.json),
Effect.andThen(Schema.decodeUnknownEffect(Post)),
),
})
return [idLens, query, mutation] as const
}))
const [id] = yield* View.useAll([idLens])
const [queryState, mutationResult] = yield* View.useAll([
query.state,
mutation.state,
])
const runPromise = yield* Component.useRunPromise()
const [id, setId] = yield* Lens.useState(idLens)
const [queryState, mutationState] = yield* View.useAll([query.state, mutation.state])
const runSync = yield* Component.useRunSync()
return (
<Container>
@@ -52,25 +71,23 @@ const QueryRouteComponent = Component.make("QueryRouteView")(function*() {
value={[id]}
min={1}
max={10}
onValueChange={([value]) =>
void runPromise(Lens.set(idLens, value ?? 1))
}
onValueChange={([value]) => setId(value ?? 1)}
/>
<PostResultView result={queryState.result} />
<Flex direction="row" justify="center" align="center" gap="1">
<Button onClick={() => void runPromise(query.refresh)}>
<Button onClick={() => runSync(query.refreshView)}>
Refresh
</Button>
<Button onClick={() => void runPromise(query.invalidateCache)}>
<Button onClick={() => runSync(query.invalidateCache)}>
Invalidate cache
</Button>
</Flex>
<PostResultView result={mutationResult} />
<PostResultView result={mutationState} />
<Button onClick={() => void runPromise(mutation.mutate(id))}>
<Button onClick={() => runSync(mutation.mutateView([id]))}>
Mutate
</Button>
</Flex>
+13 -1
View File
@@ -1,4 +1,16 @@
import { Clipboard, Geolocation, Permissions } from "@effect/platform-browser"
import { DateTime, Layer } from "effect"
import { FetchHttpClient } from "effect/unstable/http"
import { QueryClient, ReactRuntime } from "effect-fc-next"
export const runtime = ReactRuntime.make(QueryClient.layer())
export const layer = Layer.empty.pipe(
Layer.provideMerge(QueryClient.layer()),
Layer.provideMerge(DateTime.layerCurrentZoneLocal),
Layer.provideMerge(Clipboard.layer),
Layer.provideMerge(Geolocation.layer),
Layer.provideMerge(Permissions.layer),
Layer.provideMerge(FetchHttpClient.layer),
)
export const runtime = ReactRuntime.make(layer)