Skip to content
Open
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
19 changes: 19 additions & 0 deletions .changeset/add-skip-token.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
"@spiko-tech/effect-react-query": minor
---

Add `skipToken` support for conditional query execution

- Re-export `skipToken` from `@tanstack/react-query` for convenience
- All hooks (`useEffectQuery`, `useInfiniteEffectQuery`, `useEffectQueries`) now accept `skipToken` as `queryFn`
- `toQueryOptions` and `effectQueryOptions` support `skipToken` for use with `useQuery` directly
- When using `skipToken`, the `runtime` option is not required (typed as `never`)

This enables the idiomatic TanStack Query pattern for conditional queries:

```ts
const { data } = useEffectQuery({
queryKey: ["user", userId],
queryFn: userId ? () => Effect.succeed({ id: userId }) : skipToken,
});
```
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { skipToken } from "@tanstack/react-query";
export { effectQueryOptions } from "./effectQueryOptions";
export { infiniteEffectQueryOptions } from "./infiniteEffectQueryOptions";
export { toQueryOptions } from "./toQueryOptions";
Expand Down
19 changes: 17 additions & 2 deletions src/toQueryOptions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type { FetchQueryOptions, QueryKey } from "@tanstack/react-query";
import type { FetchQueryOptions, QueryFunctionContext, QueryKey, SkipToken } from "@tanstack/react-query";
import { skipToken } from "@tanstack/react-query";
import type { Effect, ManagedRuntime, Runtime } from "effect";
import { createEffectQueryFn } from "./internal/createEffectQueryFn";
import type {
DefinedInitialDataEffectQueryOptionsResult,
Expand Down Expand Up @@ -52,7 +54,20 @@ export function toQueryOptions<TQueryFnData, TError, TData, TQueryKey extends Qu
export function toQueryOptions<TQueryFnData, TError, TData, TQueryKey extends QueryKey, R>(
options: UseEffectQueryOptionsResult<TQueryFnData, TError, TData, TQueryKey, R>,
): FetchQueryOptions<TQueryFnData, TError, TQueryFnData, TQueryKey> {
const { queryFn, runtime, select: _select, ...restOptions } = options;
const { queryFn, runtime, select: _select, ...restOptions } = options as {
queryFn:
| ((context: QueryFunctionContext<TQueryKey>) => Effect.Effect<TQueryFnData, TError, R>)
| SkipToken;
runtime?: Runtime.Runtime<R> | ManagedRuntime.ManagedRuntime<R, unknown>;
select?: unknown;
} & Omit<UseEffectQueryOptionsResult<TQueryFnData, TError, TData, TQueryKey, R>, "queryFn" | "runtime" | "select">;

if (queryFn === skipToken) {
return {
...restOptions,
queryFn: skipToken,
};
}
Comment thread
antomorel marked this conversation as resolved.
Comment on lines +65 to +70

return {
...restOptions,
Expand Down
160 changes: 111 additions & 49 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
QueriesPlaceholderDataFunction,
QueryFunctionContext,
QueryKey,
SkipToken,
UseInfiniteQueryOptions,
UseInfiniteQueryResult,
UseMutationOptions,
Expand Down Expand Up @@ -78,13 +79,22 @@ export type UseEffectQueryOptions<
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
R = never,
> = Omit<UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>, "queryFn"> & {
/**
* The query function that returns an Effect.
* Receives the same QueryFunctionContext as standard useQuery.
*/
queryFn: (context: QueryFunctionContext<TQueryKey>) => Effect.Effect<TQueryFnData, TError, R>;
} & RuntimeOption<R>;
> = Omit<UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>, "queryFn"> &
(
| ({
/**
* The query function that returns an Effect.
* Receives the same QueryFunctionContext as standard useQuery.
*/
queryFn: (
context: QueryFunctionContext<TQueryKey>,
) => Effect.Effect<TQueryFnData, TError, R>;
} & RuntimeOption<R>)
| {
queryFn: SkipToken;
runtime?: never;
}
Comment thread
antomorel marked this conversation as resolved.
);

/**
* Options for useEffectQuery with defined initial data.
Expand All @@ -96,9 +106,16 @@ export type DefinedInitialDataEffectQueryOptions<
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
R = never,
> = Omit<UseEffectQueryOptions<TQueryFnData, TError, TData, TQueryKey, R>, "initialData"> & {
> = Omit<UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>, "queryFn" | "initialData"> & {
initialData: NonUndefinedGuard<TQueryFnData> | (() => NonUndefinedGuard<TQueryFnData>);
};
} & (
| ({
queryFn: (
context: QueryFunctionContext<TQueryKey>,
) => Effect.Effect<TQueryFnData, TError, R>;
} & RuntimeOption<R>)
| { queryFn: SkipToken; runtime?: never }
);

/**
* Options for useEffectQuery with undefined initial data.
Expand All @@ -109,12 +126,19 @@ export type UndefinedInitialDataEffectQueryOptions<
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
R = never,
> = Omit<UseEffectQueryOptions<TQueryFnData, TError, TData, TQueryKey, R>, "initialData"> & {
> = Omit<UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>, "queryFn" | "initialData"> & {
initialData?:
| undefined
| InitialDataFunction<NonUndefinedGuard<TQueryFnData>>
| NonUndefinedGuard<TQueryFnData>;
};
} & (
| ({
queryFn: (
context: QueryFunctionContext<TQueryKey>,
) => Effect.Effect<TQueryFnData, TError, R>;
} & RuntimeOption<R>)
| { queryFn: SkipToken; runtime?: never }
);

/**
* The result of useEffectQuery hook.
Expand Down Expand Up @@ -233,26 +257,35 @@ export type UseInfiniteEffectQueryOptions<
> = Omit<
UseInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>,
"queryFn" | "getNextPageParam" | "getPreviousPageParam"
> & {
/**
* The query function that returns an Effect.
* Receives the same QueryFunctionContext as standard useInfiniteQuery,
* including pageParam for pagination.
*/
queryFn: (
context: QueryFunctionContext<TQueryKey, TPageParam>,
) => Effect.Effect<TQueryFnData, TError, R>;
/**
* Function to get the next page parameter.
* Uses NoInfer to ensure TQueryFnData is inferred from queryFn, not from this callback.
*/
getNextPageParam: GetNextPageParamFunction<TPageParam, NoInfer<TQueryFnData>>;
/**
* Optional function to get the previous page parameter.
* Uses NoInfer to ensure TQueryFnData is inferred from queryFn, not from this callback.
*/
getPreviousPageParam?: GetPreviousPageParamFunction<TPageParam, NoInfer<TQueryFnData>>;
} & RuntimeOption<R>;
> &
(
| ({
/**
* The query function that returns an Effect.
* Receives the same QueryFunctionContext as standard useInfiniteQuery,
* including pageParam for pagination.
*/
queryFn: (
context: QueryFunctionContext<TQueryKey, TPageParam>,
) => Effect.Effect<TQueryFnData, TError, R>;
/**
* Function to get the next page parameter.
* Uses NoInfer to ensure TQueryFnData is inferred from queryFn, not from this callback.
*/
getNextPageParam: GetNextPageParamFunction<TPageParam, NoInfer<TQueryFnData>>;
/**
* Optional function to get the previous page parameter.
* Uses NoInfer to ensure TQueryFnData is inferred from queryFn, not from this callback.
*/
getPreviousPageParam?: GetPreviousPageParamFunction<TPageParam, NoInfer<TQueryFnData>>;
} & RuntimeOption<R>)
| {
queryFn: SkipToken;
runtime?: never;
getNextPageParam?: GetNextPageParamFunction<TPageParam, NoInfer<TQueryFnData>>;
getPreviousPageParam?: GetPreviousPageParamFunction<TPageParam, NoInfer<TQueryFnData>>;
}
Comment thread
antomorel marked this conversation as resolved.
);

/**
* Options for useInfiniteEffectQuery with defined initial data.
Expand All @@ -272,15 +305,24 @@ export type DefinedInitialDataInfiniteEffectQueryOptions<
UseInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>,
"queryFn" | "getNextPageParam" | "getPreviousPageParam" | "initialData"
> & {
queryFn: (
context: QueryFunctionContext<TQueryKey, TPageParam>,
) => Effect.Effect<TQueryFnData, TError, R>;
getNextPageParam: GetNextPageParamFunction<TPageParam, NoInfer<TQueryFnData>>;
getPreviousPageParam?: GetPreviousPageParamFunction<TPageParam, NoInfer<TQueryFnData>>;
initialData:
| NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>>
| (() => NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>>);
} & RuntimeOption<R>;
} & (
| ({
queryFn: (
context: QueryFunctionContext<TQueryKey, TPageParam>,
) => Effect.Effect<TQueryFnData, TError, R>;
getNextPageParam: GetNextPageParamFunction<TPageParam, NoInfer<TQueryFnData>>;
getPreviousPageParam?: GetPreviousPageParamFunction<TPageParam, NoInfer<TQueryFnData>>;
} & RuntimeOption<R>)
| {
queryFn: SkipToken;
runtime?: never;
getNextPageParam?: GetNextPageParamFunction<TPageParam, NoInfer<TQueryFnData>>;
getPreviousPageParam?: GetPreviousPageParamFunction<TPageParam, NoInfer<TQueryFnData>>;
}
Comment thread
antomorel marked this conversation as resolved.
);

/**
* Options for useInfiniteEffectQuery with undefined initial data.
Expand All @@ -299,16 +341,25 @@ export type UndefinedInitialDataInfiniteEffectQueryOptions<
UseInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>,
"queryFn" | "getNextPageParam" | "getPreviousPageParam" | "initialData"
> & {
queryFn: (
context: QueryFunctionContext<TQueryKey, TPageParam>,
) => Effect.Effect<TQueryFnData, TError, R>;
getNextPageParam: GetNextPageParamFunction<TPageParam, NoInfer<TQueryFnData>>;
getPreviousPageParam?: GetPreviousPageParamFunction<TPageParam, NoInfer<TQueryFnData>>;
initialData?:
| undefined
| InitialDataFunction<NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>>>
| NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>>;
} & RuntimeOption<R>;
} & (
| ({
queryFn: (
context: QueryFunctionContext<TQueryKey, TPageParam>,
) => Effect.Effect<TQueryFnData, TError, R>;
getNextPageParam: GetNextPageParamFunction<TPageParam, NoInfer<TQueryFnData>>;
getPreviousPageParam?: GetPreviousPageParamFunction<TPageParam, NoInfer<TQueryFnData>>;
} & RuntimeOption<R>)
| {
queryFn: SkipToken;
runtime?: never;
getNextPageParam?: GetNextPageParamFunction<TPageParam, NoInfer<TQueryFnData>>;
getPreviousPageParam?: GetPreviousPageParamFunction<TPageParam, NoInfer<TQueryFnData>>;
}
Comment thread
antomorel marked this conversation as resolved.
);

/**
* The result of useInfiniteEffectQuery hook.
Expand Down Expand Up @@ -462,23 +513,34 @@ export type UseEffectQueryOptionsForUseQueries<
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
"queryFn" | "placeholderData" | "subscribed"
> & {
/**
* The query function that returns an Effect.
*/
queryFn: (context: QueryFunctionContext<TQueryKey>) => Effect.Effect<TQueryFnData, TError, R>;
/**
* Placeholder data for this query.
*/
placeholderData?: TQueryFnData | QueriesPlaceholderDataFunction<TQueryFnData>;
} & RuntimeOption<R>;
} & (
| ({
/**
* The query function that returns an Effect.
*/
queryFn: (
context: QueryFunctionContext<TQueryKey>,
) => Effect.Effect<TQueryFnData, TError, R>;
} & RuntimeOption<R>)
| {
queryFn: SkipToken;
runtime?: never;
}
);
Comment thread
antomorel marked this conversation as resolved.

/**
* Maps an array of Effect query options to an array of UseQueryResult.
* Preserves tuple structure for proper type inference.
* 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>) | SkipToken;
}>,
> = {
-readonly [K in keyof T]: T[K] extends {
queryFn: (...args: any) => Effect.Effect<infer TData, infer TError, any>;
Expand Down
29 changes: 22 additions & 7 deletions src/useEffectQueries.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { QueryKey } from "@tanstack/react-query";
import { useQueries } from "@tanstack/react-query";
import type { QueryKey, SkipToken } from "@tanstack/react-query";
import { skipToken, useQueries } from "@tanstack/react-query";
import type { Effect, ManagedRuntime, Runtime } from "effect";
import { createEffectQueryFn } from "./internal/createEffectQueryFn";
import type { EffectQueriesResults } from "./types";
Expand All @@ -11,8 +11,11 @@ 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;
queryFn: ((...args: any[]) => Effect.Effect<any, any, any>) | SkipToken;
runtime?:
| Runtime.Runtime<any>
| ManagedRuntime.ManagedRuntime<any, any>
| undefined;
Comment thread
antomorel marked this conversation as resolved.
};
Comment on lines 12 to 19

/**
Expand Down Expand Up @@ -69,16 +72,28 @@ export function useEffectQueries<
combine?: (result: EffectQueriesResults<T>) => TCombinedResult;
}): TCombinedResult {
const transformedQueries = options.queries.map((query) => {
const { queryFn, runtime, ...rest } = query as EffectQueryOptionsBase & Record<string, unknown>;
const { queryFn, runtime, ...rest } = query as EffectQueryOptionsBase &
Record<string, unknown>;

if (queryFn === skipToken) {
return {
...rest,
queryFn: skipToken,
};
}

return {
...rest,
queryFn: createEffectQueryFn(queryFn, runtime, (context) => context.signal),
queryFn: createEffectQueryFn(
queryFn,
runtime,
(context) => context.signal,
),
};
});

const result = useQueries({
queries: transformedQueries,
queries: transformedQueries as any,
combine: options.combine as (result: Array<any>) => TCombinedResult,
});
Comment on lines 95 to 98

Expand Down
Loading
Loading