- useEffectQuery
- useEffectSuspenseQuery
- useEffectQueries
- useEffectSuspenseQueries
- useEffectMutation
- useInfiniteEffectQuery
- useInfiniteEffectSuspenseQuery
- effectQueryOptions
- infiniteEffectQueryOptions
- toQueryOptions
- Type-Safe Error Handling
- Dependency Injection
- Advanced Patterns
React Query's useQuery that accepts Effect-returning query functions.
import { useEffectQuery } from "@effect-react-query";
const query = useEffectQuery({
queryKey: ["user", userId],
queryFn: () => fetchUser(userId), // Effect<User, NetworkError>
});| Option | Description |
|---|---|
queryFn |
(context: QueryFunctionContext) => Effect.Effect<TData, TError, R> |
runtime |
Required when Effect has service requirements (Runtime or ManagedRuntime) |
... |
All standard React Query options (staleTime, gcTime, retry, etc.) |
When your Effect requires services, provide a runtime:
const query = useEffectQuery({
queryKey: ["user", userId],
queryFn: () =>
Effect.gen(function* () {
const service = yield* UserService;
return yield* service.getUser(userId);
}),
runtime: myRuntime, // Required when Effect has service requirements
});Suspense version of useEffectQuery. Data is always defined (component suspends until loaded).
import { useEffectSuspenseQuery } from "@effect-react-query";
// Wrap in React Suspense boundary
const query = useEffectSuspenseQuery({
queryKey: ["user", userId],
queryFn: () => fetchUser(userId),
});Does not support enabled, throwOnError, or placeholderData options.
React Query's useQueries for running multiple Effect queries in parallel.
import { useEffectQueries } from "@effect-react-query";
const results = useEffectQueries({
queries: [
{
queryKey: ["user", "1"],
queryFn: () => fetchUser("1"), // Effect<User, NetworkError>
},
{
queryKey: ["user", "2"],
queryFn: () => fetchUser("2"),
},
],
});
// Access individual results
const user1 = results[0].data;
const user2 = results[1].data;| Option | Description |
|---|---|
queries |
Array of query options, each with queryKey, queryFn, runtime |
combine |
Optional function to derive a computed result from all queries |
Use combine to derive computed values from multiple query results:
const { users, isLoading, isAnyError } = useEffectQueries({
queries: [
{ queryKey: ["user", "1"], queryFn: () => fetchUser("1") },
{ queryKey: ["user", "2"], queryFn: () => fetchUser("2") },
],
combine: (results) => ({
users: results.map((r) => r.data).filter(Boolean),
isLoading: results.some((r) => r.isLoading),
isAnyError: results.some((r) => r.isError),
}),
});Each query can have its own runtime:
const results = useEffectQueries({
queries: [
{
queryKey: ["user", userId],
queryFn: () =>
Effect.gen(function* () {
const service = yield* UserService;
return yield* service.getUser(userId);
}),
runtime: myRuntime,
},
],
});Suspense version of useEffectQueries. Data is always defined (component suspends until all queries are loaded).
import { useEffectSuspenseQueries } from "@effect-react-query";
const results = useEffectSuspenseQueries({
queries: [
{ queryKey: ["user", "1"], queryFn: () => fetchUser("1") },
{ queryKey: ["user", "2"], queryFn: () => fetchUser("2") },
],
});const users = useEffectSuspenseQueries({
queries: [
{ queryKey: ["user", "1"], queryFn: () => fetchUser("1") },
{ queryKey: ["user", "2"], queryFn: () => fetchUser("2") },
],
combine: (results) => results.map((r) => r.data),
});React Query's useMutation for Effect-returning mutation functions.
import { useEffectMutation } from "@effect-react-query";
const mutation = useEffectMutation({
mutationFn: (data: CreateUserInput) => createUser(data),
onSuccess: (user) => console.log("Created:", user.name),
onError: (error) => console.error(error),
});
// Trigger mutation
mutation.mutate({ name: "John", email: "john@example.com" });| Option | Description |
|---|---|
mutationFn |
(variables: TVariables) => Effect.Effect<TData, TError, R> |
runtime |
Required when Effect has service requirements (Runtime or ManagedRuntime) |
onError |
Callback on error (receives typed error) |
... |
All standard React Query mutation options |
React Query's useInfiniteQuery for paginated Effect queries.
import { useInfiniteEffectQuery } from "@effect-react-query";
interface PostsPage {
items: Post[];
nextCursor: number | null;
}
const query = useInfiniteEffectQuery({
queryKey: ["posts"],
queryFn: ({ pageParam }) => fetchPosts(pageParam), // Effect<PostsPage, Error>
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor,
});
// Access paginated data
const allPosts = query.data?.pages.flatMap((page) => page.items);
// Load more
if (query.hasNextPage) {
query.fetchNextPage();
}Suspense version of useInfiniteEffectQuery. Data is always defined.
Creates reusable, type-safe query options for useEffectQuery or useEffectSuspenseQuery.
import { effectQueryOptions, useEffectQuery } from "@effect-react-query";
// Define reusable query options
const userQueryOptions = (userId: string) =>
effectQueryOptions({
queryKey: ["user", userId] as const,
queryFn: () => fetchUser(userId),
staleTime: 5000,
});
// Use in multiple components
const query = useEffectQuery(userQueryOptions("123"));Creates reusable, type-safe options for useInfiniteEffectQuery.
import { infiniteEffectQueryOptions } from "@effect-react-query";
const postsQueryOptions = () =>
infiniteEffectQueryOptions({
queryKey: ["posts"] as const,
queryFn: ({ pageParam }) => fetchPosts(pageParam),
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor,
});Converts Effect-based query options to standard React Query options for use with queryClient methods like fetchQuery, ensureQueryData, and prefetchQuery.
import { effectQueryOptions, toQueryOptions } from "@effect-react-query";
// Define reusable query options
const userQueryOptions = (userId: string) =>
effectQueryOptions({
queryKey: ["user", userId] as const,
queryFn: () => fetchUser(userId),
});
// Use with queryClient methods
await queryClient.fetchQuery(toQueryOptions(userQueryOptions("123")));
await queryClient.ensureQueryData(toQueryOptions(userQueryOptions("456")));
await queryClient.prefetchQuery(toQueryOptions(userQueryOptions("789")));- The
selectoption is not supported byqueryClientmethods and will be stripped from the output - All other options (
staleTime,gcTime,retry,initialData, etc.) are preserved - Works with both
ManagedRuntimeand standardRuntime
const protectedQueryOptions = (userId: string) =>
effectQueryOptions({
queryKey: ["user", userId] as const,
queryFn: () =>
Effect.gen(function* () {
const service = yield* UserService;
return yield* service.getUser(userId);
}),
runtime: myRuntime,
});
// Runtime is automatically used when executing
await queryClient.fetchQuery(toQueryOptions(protectedQueryOptions("123")));Errors retain their typed structure and can be matched using Effect's Match.valueTags:
import { Schema, Match } from "effect";
import { useEffectQuery } from "@effect-react-query";
// Define typed errors
class NetworkError extends Schema.TaggedError<NetworkError>()("NetworkError", {
message: Schema.String,
}) {}
class NotFoundError extends Schema.TaggedError<NotFoundError>()("NotFoundError", {
resourceId: Schema.String,
}) {}
// Errors are fully typed
const query = useEffectQuery({
queryKey: ["user", id],
queryFn: () => fetchUser(id), // Effect<User, NetworkError | NotFoundError>
});
// Match on error types
if (query.error) {
const message = Match.valueTags(query.error, {
NetworkError: (e) => `Network issue: ${e.message}`,
NotFoundError: (e) => `User ${e.resourceId} not found`,
});
}const mutation = useEffectMutation({
mutationFn: (data: CreateUserInput) => createUser(data),
onError: Match.valueTags({
NetworkError: (e) => toast.error(e.message),
ValidationError: (e) => toast.error(`Invalid fields`),
}),
});When Effects have service requirements, provide a ManagedRuntime or Runtime:
import { Context, Effect, Layer, ManagedRuntime } from "effect";
import { useEffectQuery } from "@effect-react-query";
// Define a service
class UserService extends Context.Tag("UserService")<
UserService,
{ readonly getUser: (id: string) => Effect.Effect<User, NetworkError> }
>() {}
// Create the layer
const UserServiceLive = Layer.succeed(
UserService,
UserService.of({
getUser: (id) =>
Effect.succeed({
id,
name: "User",
createdAt: new Date(),
}),
}),
);
// Create runtime
const runtime = ManagedRuntime.make(UserServiceLive);
// Use in hook - TypeScript enforces runtime when Effect has requirements
const query = useEffectQuery({
queryKey: ["user", userId],
queryFn: () =>
Effect.gen(function* () {
const service = yield* UserService;
return yield* service.getUser(userId);
}),
runtime,
});When React Query cancels a query (component unmount, new query, etc.), the Effect is properly interrupted:
const query = useEffectQuery({
queryKey: ["user", userId],
queryFn: () =>
Effect.gen(function* () {
yield* Effect.sleep("10 seconds");
return { id: userId, name: "User" };
}).pipe(Effect.onInterrupt(() => Effect.sync(() => console.log("Query was cancelled")))),
});With defined initial data, TypeScript knows data is never undefined:
const query = useEffectQuery({
queryKey: ["user", userId],
queryFn: () => fetchUser(userId),
initialData: { id: "0", name: "Loading..." },
});
// query.data is { id: string; name: string } (not undefined)
console.log(query.data.name);Effect defects (unexpected errors from Effect.die) are thrown as-is:
const query = useEffectQuery({
queryKey: ["data"],
queryFn: () => Effect.die(new Error("Unexpected crash")),
});