Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/effect-v4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@spiko-tech/effect-react-query": major
---

Migrate to Effect v4. The `runtime` option now accepts a `Context.Context<R>` or a `ManagedRuntime` (v3 `Runtime.Runtime<R>` no longer exists). Cancellation uses Effect's native `signal` run option.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ pnpm add @effect-react-query

```ts
import { useEffectQuery, useEffectMutation } from "@effect-react-query";
import { Effect, Schema, Match } from "effect";
import * as Effect from "effect/Effect";
import * as Schema from "effect/Schema";
import * as Match from "effect/Match";

// Define typed errors
class NetworkError extends Schema.TaggedError<NetworkError>()("NetworkError", {
Expand Down
8 changes: 2 additions & 6 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 11 additions & 7 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const query = useEffectQuery({
| Option | Description |
| --------- | ----------------------------------------------------------------------------- |
| `queryFn` | `(context: QueryFunctionContext) => Effect.Effect<TData, TError, R>` |
| `runtime` | Required when Effect has service requirements (`Runtime` or `ManagedRuntime`) |
| `runtime` | Required when Effect has service requirements (`Context` or `ManagedRuntime`) |
| `...` | All standard React Query options (`staleTime`, `gcTime`, `retry`, etc.) |

### With Runtime Dependencies
Expand Down Expand Up @@ -188,7 +188,7 @@ 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`) |
| `runtime` | Required when Effect has service requirements (`Context` or `ManagedRuntime`) |
| `onError` | Callback on error (receives typed error) |
| `...` | All standard React Query mutation options |

Expand Down Expand Up @@ -308,7 +308,8 @@ await queryClient.fetchQuery(toQueryOptions(protectedQueryOptions("123")));
Errors retain their typed structure and can be matched using Effect's `Match.valueTags`:

```ts
import { Schema, Match } from "effect";
import * as Schema from "effect/Schema";
import * as Match from "effect/Match";
import { useEffectQuery } from "@effect-react-query";

// Define typed errors
Expand Down Expand Up @@ -349,17 +350,20 @@ const mutation = useEffectMutation({

# Dependency Injection

When Effects have service requirements, provide a `ManagedRuntime` or `Runtime`:
When Effects have service requirements, provide a `ManagedRuntime` or a `Context`:

```ts
import { Context, Effect, Layer, ManagedRuntime } from "effect";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as ManagedRuntime from "effect/ManagedRuntime";
import { useEffectQuery } from "@effect-react-query";

// Define a service
class UserService extends Context.Tag("UserService")<
class UserService extends Context.Service<
UserService,
{ readonly getUser: (id: string) => Effect.Effect<User, NetworkError> }
>() {}
>()("UserService") {}

// Create the layer
const UserServiceLive = Layer.succeed(
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
},
"peerDependencies": {
"@tanstack/react-query": "^5.99.2",
"effect": "^3.21.1"
"effect": "^4.0.0"
},
"engines": {
"node": ">=20"
Expand Down
2 changes: 1 addition & 1 deletion src/effectQueryOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import type {
* @example
* ```ts
* import { effectQueryOptions, toQueryOptions, useEffectQuery } from "@effect-react-query";
* import { Effect } from "effect";
* import * as Effect from "effect/Effect";
*
* // Define reusable query options
* const userQueryOptions = (userId: string) => effectQueryOptions({
Expand Down
2 changes: 1 addition & 1 deletion src/infiniteEffectQueryOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import type {
* @example
* ```ts
* import { infiniteEffectQueryOptions, useInfiniteEffectQuery } from "@effect-react-query";
* import { Effect } from "effect";
* import * as Effect from "effect/Effect";
*
* // Define reusable infinite query options
* const postsQueryOptions = () => infiniteEffectQueryOptions({
Expand Down
49 changes: 17 additions & 32 deletions src/internal/createEffectQueryFn.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { Cause, Effect, Exit, ManagedRuntime, Runtime } from "effect";
import { hasProperty } from "effect/Predicate";
import * as Cause from "effect/Cause";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Exit from "effect/Exit";
import * as ManagedRuntime from "effect/ManagedRuntime";

/**
* Creates a query function that wraps an Effect-returning function.
Expand All @@ -9,48 +12,30 @@ import { hasProperty } from "effect/Predicate";
*/
export function createEffectQueryFn<TQueryFnData, TError, TContext, R>(
effectFn: (context: TContext) => Effect.Effect<TQueryFnData, TError, R>,
runtime: Runtime.Runtime<R> | ManagedRuntime.ManagedRuntime<R, unknown> | undefined,
getSignal: (context: TContext) => AbortSignal,
runtime: Context.Context<R> | ManagedRuntime.ManagedRuntime<R, unknown> | undefined,
getSignal: (context: TContext) => AbortSignal | undefined,
): (context: TContext) => Promise<TQueryFnData> {
return async (context: TContext) => {
const effect = effectFn(context);
const signal = getSignal(context);
const options = { signal: getSignal(context) };

// Create an effect that listens to the AbortSignal for cancellation
const withAbort = Effect.raceFirst(
effect,
Effect.async<never, never, never>((resume) => {
if (signal.aborted) {
resume(Effect.interrupt);
return;
}
const onAbort = () => resume(Effect.interrupt);
signal.addEventListener("abort", onAbort);
return Effect.sync(() => signal.removeEventListener("abort", onAbort));
}),
);

// Determine how to run the effect based on runtime type
// Use unknown for error type since ManagedRuntime can add layer errors
let exit: Exit.Exit<TQueryFnData, unknown>;

if (runtime) {
if (hasProperty(runtime, ManagedRuntime.TypeId)) {
exit = await runtime.runPromiseExit(withAbort);
} else {
exit = await Runtime.runPromiseExit(runtime)(withAbort);
}
} else {
exit = await Effect.runPromiseExit(withAbort as Effect.Effect<TQueryFnData, TError, never>);
}
const exit: Exit.Exit<TQueryFnData, unknown> = Context.isContext(runtime)
? await Effect.runPromiseExitWith(runtime)(effect, options)
: runtime !== undefined
? await runtime.runPromiseExit(effect, options)
: await Effect.runPromiseExit(
effect as Effect.Effect<TQueryFnData, TError, never>,
options,
);

if (Exit.isSuccess(exit)) return exit.value;

const cause = exit.cause;

// Check for interruption - don't call onError, just hang
// React Query will handle cleanup
if (Cause.isInterruptedOnly(cause)) {
if (Cause.hasInterruptsOnly(cause)) {
return new Promise<TQueryFnData>(() => {
// Never resolves - query is cancelled
});
Expand Down
2 changes: 1 addition & 1 deletion src/toQueryOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type {
* @example
* ```ts
* import { effectQueryOptions, toQueryOptions } from "@effect-react-query";
* import { Effect } from "effect";
* import * as Effect from "effect/Effect";
*
* // Define reusable query options
* const userQueryOptions = (userId: string) => effectQueryOptions({
Expand Down
24 changes: 17 additions & 7 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ import type {
UseSuspenseQueryOptions,
UseSuspenseQueryResult,
} from "@tanstack/react-query";
import type { Effect, ManagedRuntime, Runtime } from "effect";
import type * as Context from "effect/Context";
import type * as Effect from "effect/Effect";
import type * as ManagedRuntime from "effect/ManagedRuntime";

/**
* Options for useEffectMutation hook.
Expand All @@ -47,11 +49,11 @@ export type UseEffectMutationOptions<

/**
* Runtime option - required when R is not never, forbidden when R is never.
* Accepts either a Runtime or a ManagedRuntime.
* Accepts either a Context or a ManagedRuntime.
*/
export type RuntimeOption<R> = [R] extends [never]
? { runtime?: undefined }
: { runtime: Runtime.Runtime<R> | ManagedRuntime.ManagedRuntime<R, unknown> };
: { runtime: Context.Context<R> | ManagedRuntime.ManagedRuntime<R, unknown> };

/**
* The result of useEffectMutation hook.
Expand Down Expand Up @@ -478,14 +480,18 @@ export type UseEffectQueryOptionsForUseQueries<
* Infers data and error types from the queryFn's Effect return type.
*/
export type EffectQueriesResults<
T extends ReadonlyArray<{ queryFn: (...args: any) => Effect.Effect<any, any, any> }>,
T extends ReadonlyArray<{
queryFn: (...args: any) => Effect.Effect<any, any, any>;
}>,
> = {
-readonly [K in keyof T]: T[K] extends {
queryFn: (...args: any) => Effect.Effect<infer TData, infer TError, any>;
select?: (data: any) => infer TSelected;
}
? UseQueryResult<unknown extends TSelected ? TData : TSelected, TError>
: T[K] extends { queryFn: (...args: any) => Effect.Effect<infer TData, infer TError, any> }
: T[K] extends {
queryFn: (...args: any) => Effect.Effect<infer TData, infer TError, any>;
}
? UseQueryResult<TData, TError>
: UseQueryResult;
};
Expand Down Expand Up @@ -526,14 +532,18 @@ export type UseEffectSuspenseQueryOptionsForUseQueries<
* Infers data and error types from the queryFn's Effect return type.
*/
export type EffectSuspenseQueriesResults<
T extends ReadonlyArray<{ queryFn: (...args: any) => Effect.Effect<any, any, any> }>,
T extends ReadonlyArray<{
queryFn: (...args: any) => Effect.Effect<any, any, any>;
}>,
> = {
-readonly [K in keyof T]: T[K] extends {
queryFn: (...args: any) => Effect.Effect<infer TData, infer TError, any>;
select?: (data: any) => infer TSelected;
}
? UseSuspenseQueryResult<unknown extends TSelected ? TData : TSelected, TError>
: T[K] extends { queryFn: (...args: any) => Effect.Effect<infer TData, infer TError, any> }
: T[K] extends {
queryFn: (...args: any) => Effect.Effect<infer TData, infer TError, any>;
}
? UseSuspenseQueryResult<TData, TError>
: UseSuspenseQueryResult;
};
40 changes: 5 additions & 35 deletions src/useEffectMutation.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useMutation } from "@tanstack/react-query";
import { Cause, Effect, Exit, ManagedRuntime, Runtime } from "effect";
import { hasProperty } from "effect/Predicate";
import { createEffectQueryFn } from "./internal/createEffectQueryFn";
import type { UseEffectMutationOptions, UseEffectMutationResult } from "./types";

/**
Expand All @@ -9,7 +8,8 @@ import type { UseEffectMutationOptions, UseEffectMutationResult } from "./types"
* @example
* ```ts
* import { useEffectMutation } from "@effect-react-query";
* import { Match, Schema } from "effect";
* import * as Match from "effect/Match";
* import * as Schema from "effect/Schema";
*
* // Define your errors with Schema.TaggedError
* class NetworkError extends Schema.TaggedError<NetworkError>()("NetworkError", {
Expand All @@ -27,7 +27,7 @@ import type { UseEffectMutationOptions, UseEffectMutationResult } from "./types"
* // Effect with requirements - runtime is required
* const mutation = useEffectMutation({
* mutationFn: createUserWithService, // Effect<User, NetworkError, UserService>
* runtime: myRuntime, // Runtime<UserService>
* runtime: myRuntime, // Context<UserService> or ManagedRuntime<UserService, E>
* onError: Match.valueTags({
* NetworkError: (e) => toast.error(e.message),
* }),
Expand All @@ -41,37 +41,7 @@ export function useEffectMutation<TData, TError, TVariables = void, TContext = u

const mutation = useMutation<TData, TError, TVariables, TContext>({
...restOptions,
mutationFn: async (variables: TVariables) => {
const effect = mutationFn(variables);

// Determine how to run the effect based on runtime type
// Use unknown for error type since ManagedRuntime can add layer errors
let exit: Exit.Exit<TData, unknown>;

if (runtime) {
if (hasProperty(runtime, ManagedRuntime.TypeId)) {
exit = await runtime.runPromiseExit(effect);
} else {
exit = await Runtime.runPromiseExit(runtime)(effect);
}
} else {
exit = await Effect.runPromiseExit(effect as Effect.Effect<TData, TError, never>);
}

if (Exit.isSuccess(exit)) return exit.value;

const cause = exit.cause;

// Check for interruption - don't call onError, just hang
// React Query will handle cleanup
if (Cause.isInterruptedOnly(cause)) {
return new Promise<TData>(() => {
// Never resolves - mutation is cancelled
});
}

throw Cause.squash(cause);
},
mutationFn: createEffectQueryFn(mutationFn, runtime, () => undefined),
});

return mutation;
Expand Down
8 changes: 5 additions & 3 deletions src/useEffectQueries.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { QueryKey } from "@tanstack/react-query";
import { useQueries } from "@tanstack/react-query";
import type { Effect, ManagedRuntime, Runtime } from "effect";
import type * as Context from "effect/Context";
import type * as Effect from "effect/Effect";
import type * as ManagedRuntime from "effect/ManagedRuntime";
import { createEffectQueryFn } from "./internal/createEffectQueryFn";
import type { EffectQueriesResults } from "./types";

Expand All @@ -12,7 +14,7 @@ import type { EffectQueriesResults } from "./types";
type EffectQueryOptionsBase = {
queryKey: QueryKey;
queryFn: (...args: any[]) => Effect.Effect<any, any, any>;
runtime?: Runtime.Runtime<any> | ManagedRuntime.ManagedRuntime<any, any> | undefined;
runtime?: Context.Context<any> | ManagedRuntime.ManagedRuntime<any, any> | undefined;
};

/**
Expand All @@ -24,7 +26,7 @@ type EffectQueryOptionsBase = {
* @example
* ```ts
* import { useEffectQueries } from "@effect-react-query";
* import { Effect } from "effect";
* import * as Effect from "effect/Effect";
*
* // Multiple queries without requirements
* const results = useEffectQueries({
Expand Down
Loading
Loading