# Using Effect

> The cache is written in Effect. Return Effects from queryFns for typed errors, services and interruption, run loaders and actions as Effects, and read the same queries from React.

raf-query's cache is written in [Effect](https://effect.website). Using raf-query doesn't take any Effect: `queryFn`s return promises, and `QueryClient`, the hooks and the integrations speak promises. Apps that do use Effect get the same cache as Effects, with typed errors and [services](https://raf-query.katt.dev/docs/effect-services.md).

`effect` (`^4.0.0`) is a peer dependency of both packages, so an app that uses Effect shares one copy with raf-query.

## The whole picture

An issue page on TanStack Start, from the API to the form that renames the issue. The [Effect example app](https://raf-query.katt.dev/docs/examples.md) is the same thing, in full, with [Effect RPC](https://effect.website/docs/v4/api/effect/rpc/RpcGroup).

### The API: errors and a service

Failures are tagged errors, and the API is a service. Its methods say how they can fail:

```ts title="src/issues.ts"
export class IssueNotFound extends Schema.TaggedError<IssueNotFound>()(
	"IssueNotFound",
	{ id: Schema.Number },
) {}
export class Unauthorized extends Schema.TaggedError<Unauthorized>()(
	"Unauthorized",
	{},
) {}
export class EmptyTitle extends Schema.TaggedError<EmptyTitle>()(
	"EmptyTitle",
	{},
) {}

export class IssueApi extends Context.Service<
	IssueApi,
	{
		readonly get: (
			id: number,
		) => Effect.Effect<Issue, IssueNotFound | Unauthorized>;
		readonly assignee: (
			issueId: number,
		) => Effect.Effect<User, IssueNotFound | Unauthorized>;
		readonly rename: (input: {
			readonly id: number;
			readonly title: string;
		}) => Effect.Effect<Issue, EmptyTitle | Unauthorized>;
	}
>()("app/IssueApi") {}
```

`IssueApiLive`, a [`Layer<IssueApi>`](https://effect.website/docs/v4/requirements-management/layers), implements it: an HTTP client, an RPC client, a database.

### Queries

A `queryFn` returns an Effect that uses the service. The factory's type carries the service and the errors — `ErrorOf` and `ServicesOf` name them, and every cache verb fails or requires them:

```ts title="src/queries.ts"
import { type ErrorOf, queryOptions, type ServicesOf } from "@raf-query/react";

import { IssueApi } from "./issues.ts";

export function issueQuery(input: { id: number }) {
	return queryOptions({
		queryKey: ["issue", input.id],
		queryFn: () => IssueApi.use((api) => api.get(input.id)),
	});
}

export function assigneeQuery(input: { issueId: number }) {
	return queryOptions({
		queryKey: ["issue", input.issueId, "assignee"],
		queryFn: () => IssueApi.use((api) => api.assignee(input.issueId)),
	});
}

export type IssueError = ErrorOf<typeof issueQuery>;
// type IssueError = IssueNotFound | Unauthorized
export type AssigneeError = ErrorOf<typeof assigneeQuery>;
// type AssigneeError = IssueNotFound | Unauthorized
export type Services = ServicesOf<typeof issueQuery>;
// type Services = IssueApi
```

### The client

The `QueryClient`'s `layer` provides the services, and the router context carries the client with their type, so loaders can only start queries the layer can run:

```tsx title="src/router.tsx"
import { QueryClient, QueryClientProvider } from "@raf-query/react";
import { setupRouterIntegration } from "@raf-query/react/tanstack-router";
import { createRouter } from "@tanstack/react-router";

import { IssueApiLive } from "./issues.ts";
import { routeTree } from "./routeTree.gen.ts";

export function getRouter() {
	const queryClient = new QueryClient({
		defaults: { staleTime: 5000 },
		layer: IssueApiLive,
	});
	const router = createRouter({
		context: { queryClient },
		// The query cache decides what's fresh; loaders always run and dedupe there.
		defaultPreloadStaleTime: 0,
		routeTree,
		Wrap: (props) => (
			<QueryClientProvider client={queryClient}>
				{props.children}
			</QueryClientProvider>
		),
	});
	setupRouterIntegration({ queryClient, router });
	return router;
}
```

```tsx title="src/routes/__root.tsx"
import type { QueryClient } from "@raf-query/react";
import { createRootRouteWithContext } from "@tanstack/react-router";

import type { IssueApi } from "~/issues.ts";

export const Route = createRootRouteWithContext<{
	queryClient: QueryClient<IssueApi>;
}>()({});
```

### The route

Loaders use the same Promise verbs as a non-Effect app. Prefetch is sync; `ensure` returns a Promise. A loader that needs a typed catch is an [`Effect.fn`](https://effect.website/docs/v4/api/effect/Effect#fn-variable), like a form action: it `yield*`s the `QueryCache` service, starts its reads together with [`Effect.all`](https://effect.website/docs/v4/api/effect/Effect#all), and catches what it expects. `runPromise` runs it with the layer's services — the same shape as Effect's [`ManagedRuntime`](https://effect.website/docs/v4/api/effect/ManagedRuntime):

```tsx title="src/routes/issues.$issueId.tsx"
import { QueryCache, type QueryHandle, useQuery } from "@raf-query/react";
import { createFileRoute, notFound } from "@tanstack/react-router";
import { Effect, Result } from "effect";
import { Suspense, use } from "react";

import { RenameForm } from "~/components/rename-form.tsx";
import { assigneeQuery, issueQuery } from "~/queries.ts";

const loadIssue = Effect.fn("loadIssue")(
	function* (id: number) {
		const cache = yield* QueryCache.QueryCache;
		return yield* Effect.all(
			{
				assignee: cache.prefetch(assigneeQuery, { issueId: id }),
				issue: cache.ensure(issueQuery, { id }),
			},
			{ concurrency: "unbounded" },
		);
	},
	Effect.catchTag("IssueNotFound", () => Effect.fail(notFound())),
);

export const Route = createFileRoute("/issues/$issueId")({
	loader: (args) =>
		args.context.queryClient.runPromise(loadIssue(Number(args.params.issueId))),
	// (property) queryClient: QueryClient<IssueApi>
	component: IssuePage,
	errorComponent: (props) => (
		<div role="alert">
			<p>Couldn't load the issue.</p>
			<button onClick={props.reset}>Try again</button>
		</div>
	),
});

function IssuePage() {
	const loaderData = Route.useLoaderData();
	// Loader handled IssueNotFound; throw any other failure to errorComponent.
	const [issue] = useQuery(issueQuery, loaderData.issue, {
		throwOnError: true,
	});
	return (
		<article>
			<h1>{issue.title}</h1>
			<RenameForm issue={issue} />
			<Suspense fallback={<p>Loading assignee…</p>}>
				<Assignee handle={loaderData.assignee} />
			</Suspense>
		</article>
	);
}

function Assignee(props: { handle: QueryHandle<typeof assigneeQuery> }) {
	const [assigneePromise, controls] = useQuery(assigneeQuery, props.handle);
	const assignee = use(assigneePromise);
	// const assignee: Result.Result<User, IssueNotFound | Unauthorized>
	return Result.match(assignee, {
		onFailure: (error) =>
			error._tag === "IssueNotFound" ? (
				<p>Unassigned</p>
			) : (
				<div role="alert">
					<p>Couldn't load the assignee.</p>
					<button onClick={() => void controls.refetch()}>Try again</button>
				</div>
			),
		onSuccess: (user) => <p>{user.name}</p>,
	});
}
```

With a `layer`, the client defaults to `throwOnError: false`: Effect `useQuery` returns a [`Result`](https://effect.website/docs/v4/data-types/result) so you can match failures inline ([Typed errors](https://raf-query.katt.dev/docs/effect-errors.md#in-react)). Pass `throwOnError: true` on the call, or set it in the query's options, when you want a rejecting promise instead.

A list page that only prefetches never needs `runPromise`:

```ts
loader: (args) => {
	const qc = args.context.queryClient;
	return {
		counts: qc.prefetch(issueCountsQuery),
		issues: qc.prefetch(issueListQuery, args.deps),
	};
},
```

What the loader doesn't catch, like `Unauthorized`, rejects into `errorComponent`. The components never see the services: the cache runs each `queryFn` with the layer's, whoever starts it.

### A form action

The action is an `Effect.fn` that renames and then waits for the refetched issue. It catches every failure `rename` declares into a message, so the Effect can't fail, and the form shows the message:

```tsx title="src/components/rename-form.tsx"
import { QueryCache } from "@raf-query/react";
import { getRouteApi } from "@tanstack/react-router";
import { Effect } from "effect";
import { useActionState } from "react";

import { type Issue, IssueApi } from "~/issues.ts";

const Route = getRouteApi("/issues/$issueId");

const rename = Effect.fn("rename")(
	function* (input: { readonly id: number; readonly title: string }) {
		const api = yield* IssueApi;
		const cache = yield* QueryCache.QueryCache;
		yield* api.rename(input);
		yield* cache.invalidate({ queryKey: ["issue", input.id] });
		return null;
	},
	Effect.catchTags({
		EmptyTitle: () => Effect.succeed("Title can't be empty"),
		Unauthorized: () => Effect.succeed("Sign in to rename issues"),
	}),
);

export function RenameForm(props: { issue: Issue }) {
	const queryClient = Route.useRouteContext().queryClient;
	const [message, action, isPending] = useActionState(
		(_previous: string | null, formData: FormData) =>
			queryClient.runPromise(
				rename({ id: props.issue.id, title: String(formData.get("title")) }),
			),
		null,
	);
	return (
		<form action={action}>
			<input defaultValue={props.issue.title} name="title" />
			<button disabled={isPending}>Rename</button>
			{message === null ? null : <p role="alert">{message}</p>}
		</form>
	);
}
```

The client comes from the router context, not `useQueryClient()`: that one is a `QueryClient<unknown>`, so it can't check the action's services ([Services](https://raf-query.katt.dev/docs/effect-services.md#in-react)). [Typed errors](https://raf-query.katt.dev/docs/effect-errors.md) covers where else to catch failures, and what React sees when you don't.

## Effect queryFns

What an Effect `queryFn` adds over a promise:

- **Typed errors.** The Effect's error type is the query's: `qc.effect.fetch(issueQuery, input)` fails with `IssueNotFound | Unauthorized | Query.CancelledError`. `ErrorOf<TFactory>` names it; it's `unknown` for promises. See [Typed errors](https://raf-query.katt.dev/docs/effect-errors.md).
- **Services.** What the Effect requires, `ServicesOf<TFactory>`, every verb that can start the query requires. See [Services](https://raf-query.katt.dev/docs/effect-services.md).
- **Defects aren't retried.** A `queryFn` that throws instead of returning, or an Effect that dies, is a defect: it fails the request without retrying.
- **Interruption.** Cancelling the request interrupts the fiber, so its finalizers run. No `AbortSignal` needed.
- **Schedules.** `retry` accepts a [`Schedule`](https://effect.website/docs/v4/scheduling/using-schedules) as well as a count or predicate:

```ts
export function issueQuery(input: { id: number }) {
	return queryOptions({
		queryKey: ["issue", input.id],
		queryFn: () => IssueApi.use((api) => api.get(input.id)),
		retry: Schedule.exponential("200 millis").pipe(Schedule.upTo({ times: 3 })),
	});
}
```

## The cache as an Effect service

`QueryClient` is a Promise wrapper around a `QueryCache` service. `queryClient.runPromise` hands it to loaders and actions, as above, and `qc.effect` is the same service as a value. Effect code outside React builds its own with `QueryCache.make(config)` (scoped: closing the scope interrupts every request and timer) or `QueryCache.layer(config)`:

```ts
import { QueryCache } from "@raf-query/core";
import { Stream } from "effect";

const program = Effect.gen(function* () {
	const cache = yield* QueryCache.QueryCache;
	const issue = yield* cache.fetch(issueQuery, { id: 1 });
	yield* cache.watch(issueQuery, { id: 1 }).pipe(
		Stream.map((state) => state.data?.title),
		Stream.runForEach((title) => Effect.log(title)),
	);
});

Effect.runFork(
	program.pipe(
		Effect.provide(QueryCache.layer()),
		Effect.provide(IssueApiLive),
	),
);
```

The cache has every verb as an Effect: `prefetch`, `ensure`, `fetch`, `invalidate`, `reset`, `remove`, `setData`, `getData` (an [`Option`](https://effect.website/docs/v4/data-types/option)), `isFetching` (a count), `dehydrate` and `hydrate`. `watch` is a [`Stream`](https://effect.website/docs/v4/api/effect/Stream) of an entry's state, and `events` a `Stream` of cache events.

`watch` counts as a reader while it runs, so invalidation and window focus refetch the entry. Outside React, `cache.mount` (`Effect<void, never, Scope>`) refetches read, stale entries on window focus and reconnect while its scope is open.

## Entries and observers

Entries are a data type with module functions, like Effect's own modules:

```ts
import { Query, QueryCache } from "@raf-query/core";

const program = Effect.gen(function* () {
	const cache = yield* QueryCache.QueryCache;
	const query = yield* cache.getQuery(issueQuery, { id: 1 });
	yield* Query.fetch(query);
	yield* Query.invalidate(query);
	return Query.getUnsafe(query); // synchronous read
});
```

`Query.changes(query)` streams the entry's state, and `Query.subscribeUnsafe(query, listener)` is the synchronous version.

`QueryObserver` is one reader's view of a query, as `useQuery` sees it: the controls and the promise, with `whilePending`, `refetchInterval` and the focus and reconnect options. `QueryObserver.make(query, options)` creates one, and `QueryObserver.changes(observer)` streams its results. While it runs, the query counts as read and polls on `refetchInterval`. It's the building block for bindings to other frameworks.

## SSR streaming

The SSR integrations are thin wrappers over Effect APIs:

- `dehydrateStream(options)` (scoped) returns the cache's `initial` state and a `Stream` of `updates`: entries that start or settle later, each sent once, batched per microtask. The stream ends when the scope closes.
- `hydrateStream(stream)` hydrates each state as it arrives.
- [`Stream.toReadableStream`](https://effect.website/docs/v4/api/effect/Stream#toReadableStream) and [`Stream.fromReadableStream`](https://effect.website/docs/v4/api/effect/Stream#fromReadableStream) carry it over a web stream.

The `sent` option (a `Set` of hashes) makes separate `dehydrate` calls of one request skip entries already sent.

## Time and the page

Time comes from Effect's [`Clock`](https://effect.website/docs/v4/api/effect/Clock), and the page from the `Platform` service (focus, visibility, [`navigator.onLine`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/onLine); the default reads the globals). Provide [`TestClock`](https://effect.website/docs/v4/api/effect/testing/TestClock) or another `Platform` to test staleness, garbage collection and offline retries without a browser. See [Testing](https://raf-query.katt.dev/docs/testing.md).
