Type-safe integration between Effect and TanStack React Query.
npm install effect-tanstack-queryimport { make } from "effect-tanstack-query"
import { ApiClient } from "./api/client"
// Create runtime and hooks from a layer
const eq = make(ApiClient.Default)
// Wrap your app
function Root() {
return <eq.RuntimeProvider>{children}</eq.RuntimeProvider>
}
// Create typed query options
import { makeOptions } from "effect-tanstack-query"
const options = makeOptions<ApiClient>()Creates a runtime provider and React hooks from a layer. Returns:
RuntimeProvider- React component to wrap your appuseQuery- Standard query hookuseSuspenseQuery- Suspense query hook (returnsEither)useMutation- Mutation hooktoQueryOptions- Convert to regularQueryOptions
const eq = make(ApiClient.Default)Creates typed helpers for building query and mutation options.
const options = makeOptions<ApiClient>()
const todosQueryOptions = options.queryOptions({
queryKey: ["todos"],
queryFn: () => ApiClient.pipe(Effect.flatMap((client) => client.todos.getAll())),
schema: Schema.Array(Todo),
})useQuery - Standard queries with loading/error states
useSuspenseQuery - Suspense queries that return Either for error handling:
const todos = eq.useSuspenseQuery(todosQueryOptions).pipe(
Either.map(({ data }) => data),
Either.getOrElse(() => [] as Todo[])
)useMutation - Mutations with Effect-based mutationFn
Convert to regular QueryOptions for use with a queryClient:
async loader({ context }) {
await context.queryClient.prefetchQuery(
eq.toQueryOptions(todosQueryOptions, runtime)
)
}This is an example with Tanstack Start
// 1. Setup (lib/effect-react-query/index.ts)
import { make } from "effect-tanstack-query"
import { ApiClient } from "../api/client"
export const eq = make(ApiClient.Default)
// 2. Wrap app (routes/__root.tsx)
function Root() {
return <eq.RuntimeProvider>{children}</eq.RuntimeProvider>
}
// 3. Create query options (routes/index.tsx)
import { makeOptions } from "effect-tanstack-query"
import { Effect, Schema } from "effect"
const options = makeOptions<ApiClient>()
const todosQueryOptions = options.queryOptions({
queryKey: ["todos"],
queryFn: () => ApiClient.pipe(Effect.flatMap((client) => client.todos.getAll())),
schema: Schema.Array(Todo),
})
// 4. Prefetch in loader
export const Route = createFileRoute("/")({
async loader({ context }) {
await context.queryClient.prefetchQuery(eq.toQueryOptions(todosQueryOptions, runtime))
},
component: App,
})
// 5. Use in component
function App() {
const todos = eq.useSuspenseQuery(todosQueryOptions).pipe(
Either.map(({ data }) => data),
Either.getOrElse(() => [] as Todo[])
)
return <div>{/* render todos */}</div>
}- Effectful hooks with full type safety
- Schema support for encode/decode
- Abort signal handling
- OpenTelemetry tracing via
Effect.withSpan - Automatic runtime disposal