@effectify/react-query 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -0
- package/dist/package.json +39 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +5 -0
- package/dist/src/lib/internal/make-use-effect-mutation.d.ts +3 -0
- package/dist/src/lib/internal/make-use-effect-mutation.js +13 -0
- package/dist/src/lib/internal/make-use-effect-query.d.ts +3 -0
- package/dist/src/lib/internal/make-use-effect-query.js +22 -0
- package/dist/src/lib/internal/make-use-rx-subsciption-ref.d.ts +20 -0
- package/dist/src/lib/internal/make-use-rx-subsciption-ref.js +25 -0
- package/dist/src/lib/internal/make-use-rx-subscribe.d.ts +5 -0
- package/dist/src/lib/internal/make-use-rx-subscribe.js +40 -0
- package/dist/src/lib/internal/query-data-helpers.d.ts +75 -0
- package/dist/src/lib/internal/query-data-helpers.js +107 -0
- package/dist/src/lib/tanstack-query-effect.d.ts +28 -0
- package/dist/src/lib/tanstack-query-effect.js +51 -0
- package/dist/src/lib/types.d.ts +25 -0
- package/dist/src/lib/types.js +1 -0
- package/package.json +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# @effectify/react-query
|
|
2
|
+
|
|
3
|
+
Integration of [Effect](https://effect.website/) with [TanStack Query](https://tanstack.com/query/latest) for [React](https://react.dev/).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# npm
|
|
9
|
+
npm install @effectify/react-query
|
|
10
|
+
|
|
11
|
+
# yarn
|
|
12
|
+
yarn add @effectify/react-query
|
|
13
|
+
|
|
14
|
+
# pnpm
|
|
15
|
+
pnpm add @effectify/react-query
|
|
16
|
+
|
|
17
|
+
# bun
|
|
18
|
+
bun add @effectify/react-query
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Basic Usage
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import * as Layer from "effect/Layer"
|
|
25
|
+
import * as Effect from "effect/Effect"
|
|
26
|
+
import { QueryClient } from "@tanstack/react-query"
|
|
27
|
+
import { tanstackQueryEffect } from "@effectify/react-query"
|
|
28
|
+
|
|
29
|
+
// Create an Effect layer
|
|
30
|
+
const AppLayer = Layer.succeed("AppConfig", { apiUrl: "https://api.example.com" })
|
|
31
|
+
|
|
32
|
+
// Create a QueryClient instance
|
|
33
|
+
const queryClient = new QueryClient()
|
|
34
|
+
|
|
35
|
+
// Initialize the TanStack Query integration
|
|
36
|
+
const {
|
|
37
|
+
RuntimeProvider,
|
|
38
|
+
useRuntime,
|
|
39
|
+
useEffectQuery,
|
|
40
|
+
useEffectMutation,
|
|
41
|
+
useRxSubscribe,
|
|
42
|
+
useRxSubscriptionRef,
|
|
43
|
+
} = tanstackQueryEffect({ layer: AppLayer, queryClient })
|
|
44
|
+
|
|
45
|
+
// Wrap your application with the provider
|
|
46
|
+
function App() {
|
|
47
|
+
return (
|
|
48
|
+
<RuntimeProvider>
|
|
49
|
+
<YourApp />
|
|
50
|
+
</RuntimeProvider>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Use in components
|
|
55
|
+
function YourComponent() {
|
|
56
|
+
const query = useEffectQuery({
|
|
57
|
+
queryKey: ["data"],
|
|
58
|
+
queryFn: () => Effect.succeed(["item1", "item2"]),
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div>
|
|
63
|
+
{query.isPending ? <p>Loading...</p> : query.isError ? <p>Error: {query.error.message}</p> : (
|
|
64
|
+
<ul>
|
|
65
|
+
{query.data.map((item) => <li>{item}</li>)}
|
|
66
|
+
</ul>
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## API
|
|
74
|
+
|
|
75
|
+
### `tanstackQueryEffect({ layer, queryClient })`
|
|
76
|
+
|
|
77
|
+
Creates an instance of the TanStack Query integration.
|
|
78
|
+
|
|
79
|
+
#### Parameters
|
|
80
|
+
|
|
81
|
+
- `layer`: An Effect layer that provides the necessary dependencies.
|
|
82
|
+
- `queryClient`: A TanStack Query QueryClient instance.
|
|
83
|
+
|
|
84
|
+
#### Returns
|
|
85
|
+
|
|
86
|
+
An object with the following properties:
|
|
87
|
+
|
|
88
|
+
- `RuntimeProvider`: Provider component that should wrap your application.
|
|
89
|
+
- `useRuntime`: Hook to access the Effect runtime.
|
|
90
|
+
- `useEffectQuery`: Hook to perform queries with Effect.
|
|
91
|
+
- `useEffectMutation`: Hook to perform mutations with Effect.
|
|
92
|
+
- `useRxSubscribe`: Hook to subscribe to Effect streams.
|
|
93
|
+
- `useRxSubscriptionRef`: Hook to maintain a reference to a subscription.
|
|
94
|
+
|
|
95
|
+
### Helpers
|
|
96
|
+
|
|
97
|
+
- `createQueryDataHelpers`: Utility to create query data manipulation helpers.
|
|
98
|
+
- `createQueryKey`: Utility to create typed query keys.
|
|
99
|
+
|
|
100
|
+
## Complete Example
|
|
101
|
+
|
|
102
|
+
Check out the example application in [apps/tanstack-react-app](../../apps/tanstack-react-app) to see a complete use case.
|
|
103
|
+
|
|
104
|
+
## Credits & Inspiration
|
|
105
|
+
|
|
106
|
+
This package was inspired by the excellent educational content from [Lucas Barake](https://www.youtube.com/@lucas-barake), particularly his [video on Effect and TanStack Query](https://www.youtube.com/watch?v=zl4w3BQAoJM&t=1011s) which provides great insights into these technologies.
|
|
107
|
+
|
|
108
|
+
## License
|
|
109
|
+
|
|
110
|
+
MIT
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@effectify/react-query",
|
|
3
|
+
"version": "0.0.3",
|
|
4
|
+
"description": "Integration of Effect with TanStack Query for React",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/src/index.js",
|
|
7
|
+
"module": "./dist/src/index.js",
|
|
8
|
+
"types": "./dist/src/index.d.ts",
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"@effectify/source": "./src/index.ts",
|
|
15
|
+
"types": "./dist/src/index.d.ts",
|
|
16
|
+
"import": "./dist/src/index.js",
|
|
17
|
+
"default": "./dist/src/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"!**/*.tsbuildinfo"
|
|
23
|
+
],
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"tslib": "catalog:",
|
|
26
|
+
"@tanstack/react-query": "5.90.10",
|
|
27
|
+
"@tanstack/query-core": "5.90.10",
|
|
28
|
+
"effect": "catalog:",
|
|
29
|
+
"react": "catalog:"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"typescript": "catalog:",
|
|
33
|
+
"@types/react": "catalog:"
|
|
34
|
+
},
|
|
35
|
+
"optionalDependencies": {},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"effect": "^3.19.16 || ^4.0.0-beta"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type UseMutationResult } from "@tanstack/react-query";
|
|
2
|
+
import type { EffectfulError, EffectfulMutationOptions, Runner } from "../types.js";
|
|
3
|
+
export declare const makeUseEffectMutation: <R>(createRunner: Runner<R>) => <TData, TError extends EffectfulError, TVariables>(options: EffectfulMutationOptions<TData, TError, TVariables, R>) => UseMutationResult<TData, Error, TVariables>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useMutation } from "@tanstack/react-query";
|
|
2
|
+
export const makeUseEffectMutation = (createRunner) => (options) => {
|
|
3
|
+
const effectRunner = createRunner();
|
|
4
|
+
const [spanName] = options.mutationKey;
|
|
5
|
+
const mutationFn = (variables) => {
|
|
6
|
+
const effect = options.mutationFn(variables);
|
|
7
|
+
return effectRunner(spanName)(effect);
|
|
8
|
+
};
|
|
9
|
+
return useMutation({
|
|
10
|
+
...options,
|
|
11
|
+
mutationFn,
|
|
12
|
+
});
|
|
13
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type UseQueryResult } from "@tanstack/react-query";
|
|
2
|
+
import type { EffectfulError, EffectfulQueryOptions, QueryKey, Runner } from "../types.js";
|
|
3
|
+
export declare const makeUseEffectQuery: <R>(createRunner: Runner<R>) => <TData, TError extends EffectfulError, TQueryKey extends QueryKey = QueryKey>({ gcTime, staleTime, ...options }: EffectfulQueryOptions<TData, TError, R, TQueryKey>) => UseQueryResult<TData, Error>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { skipToken, useQuery } from "@tanstack/react-query";
|
|
2
|
+
import * as Duration from "effect/Duration";
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
export const makeUseEffectQuery = (createRunner) => ({ gcTime, staleTime, ...options }) => {
|
|
5
|
+
const effectRunner = createRunner();
|
|
6
|
+
const [spanName] = options.queryKey;
|
|
7
|
+
const queryFn = useMemo(() => (context) => {
|
|
8
|
+
const effect = options.queryFn(context);
|
|
9
|
+
return effectRunner(spanName)(effect);
|
|
10
|
+
}, [effectRunner, spanName, options.queryFn]);
|
|
11
|
+
return useQuery({
|
|
12
|
+
...options,
|
|
13
|
+
queryKey: options.queryKey,
|
|
14
|
+
queryFn: options.queryFn === skipToken ? skipToken : queryFn,
|
|
15
|
+
...(staleTime !== undefined && {
|
|
16
|
+
staleTime: Duration.toMillis(Duration.millis(staleTime)),
|
|
17
|
+
}),
|
|
18
|
+
...(gcTime !== undefined && {
|
|
19
|
+
gcTime: Duration.toMillis(Duration.millis(gcTime)),
|
|
20
|
+
}),
|
|
21
|
+
});
|
|
22
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type Context } from "react";
|
|
2
|
+
import type * as ManagedRuntime from "effect/ManagedRuntime";
|
|
3
|
+
import type { Subscribable, SubscriptionOptions } from "../types.js";
|
|
4
|
+
import type * as Effect from "effect/Effect";
|
|
5
|
+
/**
|
|
6
|
+
* ⚠️ TEMPORARILY DISABLED - Effect v4 Migration
|
|
7
|
+
*
|
|
8
|
+
* This hook is temporarily disabled due to significant API changes in Effect v4:
|
|
9
|
+
* - SubscriptionRef.SubscriptionRefTypeId was removed
|
|
10
|
+
* - Stream APIs reorganized under effect/unstable/*
|
|
11
|
+
* - Migration documentation is incomplete (see Effect-TS/effect-smol#1378)
|
|
12
|
+
*
|
|
13
|
+
* The core functionality (useEffectQuery, useEffectMutation) works with v4.
|
|
14
|
+
* This advanced subscription feature will be revisited when v4 documentation
|
|
15
|
+
* is complete or when the beta stabilizes.
|
|
16
|
+
*
|
|
17
|
+
* TODO: Re-enable after Effect v4 stable release and documentation update
|
|
18
|
+
* @deprecated Temporarily disabled during Effect v4 beta migration
|
|
19
|
+
*/
|
|
20
|
+
export declare const makeUseRxSubscriptionRef: <R, E>(RuntimeContext: Context<ManagedRuntime.ManagedRuntime<R, E> | null>) => <A, E2>(_subscribable: Subscribable<A, E2> | Effect.Effect<Subscribable<A, E2>, never, R> | Effect.Effect<unknown, never, R>, _onNext: (value: A) => void, _opts?: SubscriptionOptions) => A;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useContext } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* ⚠️ TEMPORARILY DISABLED - Effect v4 Migration
|
|
4
|
+
*
|
|
5
|
+
* This hook is temporarily disabled due to significant API changes in Effect v4:
|
|
6
|
+
* - SubscriptionRef.SubscriptionRefTypeId was removed
|
|
7
|
+
* - Stream APIs reorganized under effect/unstable/*
|
|
8
|
+
* - Migration documentation is incomplete (see Effect-TS/effect-smol#1378)
|
|
9
|
+
*
|
|
10
|
+
* The core functionality (useEffectQuery, useEffectMutation) works with v4.
|
|
11
|
+
* This advanced subscription feature will be revisited when v4 documentation
|
|
12
|
+
* is complete or when the beta stabilizes.
|
|
13
|
+
*
|
|
14
|
+
* TODO: Re-enable after Effect v4 stable release and documentation update
|
|
15
|
+
* @deprecated Temporarily disabled during Effect v4 beta migration
|
|
16
|
+
*/
|
|
17
|
+
export const makeUseRxSubscriptionRef = (RuntimeContext) => (_subscribable, _onNext, _opts) => {
|
|
18
|
+
const runtime = useContext(RuntimeContext);
|
|
19
|
+
if (!runtime) {
|
|
20
|
+
throw new Error("Runtime context not found. Make sure to wrap your app with RuntimeProvider");
|
|
21
|
+
}
|
|
22
|
+
throw new Error("useRxSubscriptionRef is temporarily disabled during Effect v4 beta migration. " +
|
|
23
|
+
"Please use useEffectQuery or useEffectMutation instead, or wait for v4 stable release. " +
|
|
24
|
+
"See: https://github.com/Effect-TS/effect-smol/issues/1378");
|
|
25
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import type * as ManagedRuntime from "effect/ManagedRuntime";
|
|
3
|
+
import * as Stream from "effect/Stream";
|
|
4
|
+
import { type Context } from "react";
|
|
5
|
+
export declare const makeUseRxSubscribe: <R, E>(RuntimeContext: Context<ManagedRuntime.ManagedRuntime<R, E> | null>) => <E2, A>(stream: Stream.Stream<A, E2, R> | Effect.Effect<Stream.Stream<A, E2, R>, E2, R>, initialValue: A, onNext: (value: A) => void, onError?: (error: E2) => void) => A | undefined;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Exit from "effect/Exit";
|
|
3
|
+
import * as Fiber from "effect/Fiber";
|
|
4
|
+
import * as Stream from "effect/Stream";
|
|
5
|
+
import { useContext, useEffect, useRef, useState } from "react";
|
|
6
|
+
export const makeUseRxSubscribe = (RuntimeContext) => {
|
|
7
|
+
return (stream, initialValue, onNext, onError) => {
|
|
8
|
+
const runtime = useContext(RuntimeContext);
|
|
9
|
+
if (!runtime) {
|
|
10
|
+
throw new Error("Runtime context not found. Make sure to wrap your app with RuntimeProvider");
|
|
11
|
+
}
|
|
12
|
+
const [value, setValue] = useState(initialValue);
|
|
13
|
+
const fiberRef = useRef(null);
|
|
14
|
+
const finalStream = Effect.isEffect(stream)
|
|
15
|
+
? Stream.unwrap(stream)
|
|
16
|
+
: stream;
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const subscription = finalStream.pipe(Stream.tap((a) => Effect.sync(() => {
|
|
19
|
+
setValue(a);
|
|
20
|
+
onNext(a);
|
|
21
|
+
})), Stream.catch((e) => Stream.fromEffect(Effect.sync(() => {
|
|
22
|
+
onError?.(e);
|
|
23
|
+
return;
|
|
24
|
+
}))), Stream.runDrain, Effect.forever, Effect.forkDetach);
|
|
25
|
+
runtime.runCallback(subscription, {
|
|
26
|
+
onExit: (exit) => {
|
|
27
|
+
if (Exit.isSuccess(exit)) {
|
|
28
|
+
fiberRef.current = exit.value;
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
return () => {
|
|
33
|
+
if (fiberRef.current !== null) {
|
|
34
|
+
runtime.runCallback(Fiber.interrupt(fiberRef.current));
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}, [finalStream, runtime, onNext, onError]);
|
|
38
|
+
return value;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { QueryClient } from "@tanstack/react-query";
|
|
2
|
+
type DeepMutable<T> = {
|
|
3
|
+
-readonly [P in keyof T]: T[P] extends object ? DeepMutable<T[P]> : T[P];
|
|
4
|
+
};
|
|
5
|
+
export type QueryDataUpdater<TData> = (draft: DeepMutable<TData>) => void;
|
|
6
|
+
type QueryKey<TKey extends string, TVariables = void> = TVariables extends void ? readonly [TKey] : readonly [TKey, TVariables];
|
|
7
|
+
/**
|
|
8
|
+
* Creates a type-safe query key factory that can be used with or without variables
|
|
9
|
+
* @template TKey The string literal type for the query key
|
|
10
|
+
* @template TVariables Optional variables type. If not provided, the factory will not accept variables
|
|
11
|
+
* @param key The query key string
|
|
12
|
+
* @returns A function that creates a query key tuple
|
|
13
|
+
*
|
|
14
|
+
* @example Without variables:
|
|
15
|
+
* ```typescript
|
|
16
|
+
* const userKey = createQueryKey("user");
|
|
17
|
+
* const key = userKey(); // returns ["user"]
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* @example With variables:
|
|
21
|
+
* ```typescript
|
|
22
|
+
* type UserVars = { id: string };
|
|
23
|
+
* const userKey = createQueryKey<"user", UserVars>("user");
|
|
24
|
+
* const key = userKey({ id: "123" }); // returns ["user", { id: "123" }]
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export declare function createQueryKey<TKey extends string, TVariables = void>(key: TKey): TVariables extends void ? () => QueryKey<TKey> : (variables: TVariables) => QueryKey<TKey, TVariables>;
|
|
28
|
+
type QueryDataHelpers<TData, TVariables> = {
|
|
29
|
+
removeQuery: (variables: TVariables) => void;
|
|
30
|
+
removeAllQueries: () => void;
|
|
31
|
+
setData: (variables: TVariables, updater: QueryDataUpdater<TData>) => TData | undefined;
|
|
32
|
+
invalidateQuery: (variables: TVariables) => Promise<void>;
|
|
33
|
+
invalidateAllQueries: () => Promise<void>;
|
|
34
|
+
refetchQuery: (variables: TVariables) => Promise<void>;
|
|
35
|
+
refetchAllQueries: () => Promise<void>;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Creates a set of helpers to manage query data in the cache
|
|
39
|
+
* @template TData The type of data stored in the query
|
|
40
|
+
* @template TVariables Automatically inferred from the queryKey function parameter
|
|
41
|
+
* @param queryKey A function that creates a query key tuple from variables
|
|
42
|
+
* @returns An object with methods to remove, update, invalidate, and invalidate all query data
|
|
43
|
+
*
|
|
44
|
+
* @example Without variables:
|
|
45
|
+
* ```typescript
|
|
46
|
+
* const userKey = createQueryKey("user");
|
|
47
|
+
* type User = { name: string };
|
|
48
|
+
*
|
|
49
|
+
* // Types are inferred from userKey
|
|
50
|
+
* const helpers = createQueryDataHelpers<User>(userKey);
|
|
51
|
+
* helpers.setData(undefined, (draft) => {
|
|
52
|
+
* draft.name = "New Name";
|
|
53
|
+
* });
|
|
54
|
+
* ```
|
|
55
|
+
*
|
|
56
|
+
* @example With variables and explicit types:
|
|
57
|
+
* ```typescript
|
|
58
|
+
* type UserVars = { id: string };
|
|
59
|
+
* type User = { id: string; name: string };
|
|
60
|
+
*
|
|
61
|
+
* const userKey = createQueryKey<"user", UserVars>("user");
|
|
62
|
+
* const helpers = createQueryDataHelpers<User, UserVars>(userKey);
|
|
63
|
+
*
|
|
64
|
+
* helpers.setData({ id: "123" }, (draft) => {
|
|
65
|
+
* draft.name = "New Name";
|
|
66
|
+
* });
|
|
67
|
+
*
|
|
68
|
+
* // Other helper methods
|
|
69
|
+
* await helpers.invalidateQuery({ id: "123" });
|
|
70
|
+
* await helpers.refetchQuery({ id: "123" });
|
|
71
|
+
* helpers.removeQuery({ id: "123" });
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export declare const makeCreateQueryDataHelpers: (queryClient: QueryClient) => <TData, TVariables = void>(queryKey: (variables: TVariables) => readonly [string, ...unknown[]]) => QueryDataHelpers<TData, TVariables>;
|
|
75
|
+
export {};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Simple deep clone function to replace mutative
|
|
2
|
+
function deepClone(obj) {
|
|
3
|
+
if (obj === null || typeof obj !== "object") {
|
|
4
|
+
return obj;
|
|
5
|
+
}
|
|
6
|
+
if (obj instanceof Date) {
|
|
7
|
+
return new Date(obj.getTime());
|
|
8
|
+
}
|
|
9
|
+
if (Array.isArray(obj)) {
|
|
10
|
+
return obj.map((item) => deepClone(item));
|
|
11
|
+
}
|
|
12
|
+
if (typeof obj === "object") {
|
|
13
|
+
const cloned = {};
|
|
14
|
+
for (const key in obj) {
|
|
15
|
+
if (Object.hasOwn(obj, key)) {
|
|
16
|
+
cloned[key] = deepClone(obj[key]);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return cloned;
|
|
20
|
+
}
|
|
21
|
+
return obj;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Creates a type-safe query key factory that can be used with or without variables
|
|
25
|
+
* @template TKey The string literal type for the query key
|
|
26
|
+
* @template TVariables Optional variables type. If not provided, the factory will not accept variables
|
|
27
|
+
* @param key The query key string
|
|
28
|
+
* @returns A function that creates a query key tuple
|
|
29
|
+
*
|
|
30
|
+
* @example Without variables:
|
|
31
|
+
* ```typescript
|
|
32
|
+
* const userKey = createQueryKey("user");
|
|
33
|
+
* const key = userKey(); // returns ["user"]
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* @example With variables:
|
|
37
|
+
* ```typescript
|
|
38
|
+
* type UserVars = { id: string };
|
|
39
|
+
* const userKey = createQueryKey<"user", UserVars>("user");
|
|
40
|
+
* const key = userKey({ id: "123" }); // returns ["user", { id: "123" }]
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export function createQueryKey(key) {
|
|
44
|
+
return ((variables) => variables === undefined ? [key] : [key, variables]);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Creates a set of helpers to manage query data in the cache
|
|
48
|
+
* @template TData The type of data stored in the query
|
|
49
|
+
* @template TVariables Automatically inferred from the queryKey function parameter
|
|
50
|
+
* @param queryKey A function that creates a query key tuple from variables
|
|
51
|
+
* @returns An object with methods to remove, update, invalidate, and invalidate all query data
|
|
52
|
+
*
|
|
53
|
+
* @example Without variables:
|
|
54
|
+
* ```typescript
|
|
55
|
+
* const userKey = createQueryKey("user");
|
|
56
|
+
* type User = { name: string };
|
|
57
|
+
*
|
|
58
|
+
* // Types are inferred from userKey
|
|
59
|
+
* const helpers = createQueryDataHelpers<User>(userKey);
|
|
60
|
+
* helpers.setData(undefined, (draft) => {
|
|
61
|
+
* draft.name = "New Name";
|
|
62
|
+
* });
|
|
63
|
+
* ```
|
|
64
|
+
*
|
|
65
|
+
* @example With variables and explicit types:
|
|
66
|
+
* ```typescript
|
|
67
|
+
* type UserVars = { id: string };
|
|
68
|
+
* type User = { id: string; name: string };
|
|
69
|
+
*
|
|
70
|
+
* const userKey = createQueryKey<"user", UserVars>("user");
|
|
71
|
+
* const helpers = createQueryDataHelpers<User, UserVars>(userKey);
|
|
72
|
+
*
|
|
73
|
+
* helpers.setData({ id: "123" }, (draft) => {
|
|
74
|
+
* draft.name = "New Name";
|
|
75
|
+
* });
|
|
76
|
+
*
|
|
77
|
+
* // Other helper methods
|
|
78
|
+
* await helpers.invalidateQuery({ id: "123" });
|
|
79
|
+
* await helpers.refetchQuery({ id: "123" });
|
|
80
|
+
* helpers.removeQuery({ id: "123" });
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export const makeCreateQueryDataHelpers = (queryClient) => (queryKey) => {
|
|
84
|
+
const [namespaceKey] = queryKey(undefined);
|
|
85
|
+
return {
|
|
86
|
+
removeQuery: (variables) => {
|
|
87
|
+
queryClient.removeQueries({ queryKey: queryKey(variables) });
|
|
88
|
+
},
|
|
89
|
+
removeAllQueries: () => {
|
|
90
|
+
queryClient.removeQueries({ queryKey: [namespaceKey], exact: false });
|
|
91
|
+
},
|
|
92
|
+
setData: (variables, updater) => {
|
|
93
|
+
return queryClient.setQueryData(queryKey(variables), (oldData) => {
|
|
94
|
+
if (oldData === undefined) {
|
|
95
|
+
return oldData;
|
|
96
|
+
}
|
|
97
|
+
const clonedData = deepClone(oldData);
|
|
98
|
+
updater(clonedData);
|
|
99
|
+
return clonedData;
|
|
100
|
+
});
|
|
101
|
+
},
|
|
102
|
+
invalidateQuery: (variables) => queryClient.invalidateQueries({ queryKey: queryKey(variables) }),
|
|
103
|
+
invalidateAllQueries: () => queryClient.invalidateQueries({ queryKey: [namespaceKey], exact: false }),
|
|
104
|
+
refetchQuery: (variables) => queryClient.refetchQueries({ queryKey: queryKey(variables) }),
|
|
105
|
+
refetchAllQueries: () => queryClient.refetchQueries({ queryKey: [namespaceKey], exact: false }),
|
|
106
|
+
};
|
|
107
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type QueryClient } from "@tanstack/react-query";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Layer from "effect/Layer";
|
|
4
|
+
import * as ManagedRuntime from "effect/ManagedRuntime";
|
|
5
|
+
import { type ReactNode } from "react";
|
|
6
|
+
export declare const tanstackQueryEffect: <R, E>({ layer, queryClient, }: {
|
|
7
|
+
layer: Layer.Layer<R, E, never>;
|
|
8
|
+
queryClient: QueryClient;
|
|
9
|
+
}) => {
|
|
10
|
+
useRunner: () => <A, E2>(span: string) => (effect: Effect.Effect<A, E2, R>) => Promise<A>;
|
|
11
|
+
RuntimeProvider: ({ children }: {
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
}) => import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
useRuntime: () => ManagedRuntime.ManagedRuntime<R, E>;
|
|
15
|
+
useEffectQuery: <TData, TError extends import("./types.js").EffectfulError, TQueryKey extends import("./types.js").QueryKey = import("./types.js").QueryKey>({ gcTime, staleTime, ...options }: import("./types.js").EffectfulQueryOptions<TData, TError, R, TQueryKey>) => import("@tanstack/react-query").UseQueryResult<TData, Error>;
|
|
16
|
+
useEffectMutation: <TData, TError_1 extends import("./types.js").EffectfulError, TVariables>(options: import("./types.js").EffectfulMutationOptions<TData, TError_1, TVariables, R>) => import("@tanstack/react-query").UseMutationResult<TData, Error, TVariables>;
|
|
17
|
+
useRxSubscribe: <E2_1, A>(stream: import("effect/Stream").Stream<A, E2_1, R> | Effect.Effect<import("effect/Stream").Stream<A, E2_1, R>, E2_1, R>, initialValue: A, onNext: (value: A) => void, onError?: ((error: E2_1) => void) | undefined) => A | undefined;
|
|
18
|
+
useRxSubscriptionRef: <A_1, E2_1>(_subscribable: import("./types.js").Subscribable<A_1, E2_1> | Effect.Effect<import("./types.js").Subscribable<A_1, E2_1>, never, R> | Effect.Effect<unknown, never, R>, _onNext: (value: A_1) => void, _opts?: import("./types.js").SubscriptionOptions) => A_1;
|
|
19
|
+
createQueryDataHelpers: <TData, TVariables_1 = void>(queryKey: (variables: TVariables_1) => readonly [string, ...unknown[]]) => {
|
|
20
|
+
removeQuery: (variables: TVariables_1) => void;
|
|
21
|
+
removeAllQueries: () => void;
|
|
22
|
+
setData: (variables: TVariables_1, updater: import("./internal/query-data-helpers.js").QueryDataUpdater<TData>) => TData | undefined;
|
|
23
|
+
invalidateQuery: (variables: TVariables_1) => Promise<void>;
|
|
24
|
+
invalidateAllQueries: () => Promise<void>;
|
|
25
|
+
refetchQuery: (variables: TVariables_1) => Promise<void>;
|
|
26
|
+
refetchAllQueries: () => Promise<void>;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { QueryClientProvider } from "@tanstack/react-query";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as ManagedRuntime from "effect/ManagedRuntime";
|
|
5
|
+
import { createContext, useContext, useEffect, useMemo, useRef } from "react";
|
|
6
|
+
import { makeUseEffectMutation } from "./internal/make-use-effect-mutation.js";
|
|
7
|
+
import { makeUseEffectQuery } from "./internal/make-use-effect-query.js";
|
|
8
|
+
import { makeUseRxSubscriptionRef } from "./internal/make-use-rx-subsciption-ref.js";
|
|
9
|
+
import { makeUseRxSubscribe } from "./internal/make-use-rx-subscribe.js";
|
|
10
|
+
import { makeCreateQueryDataHelpers } from "./internal/query-data-helpers.js";
|
|
11
|
+
export const tanstackQueryEffect = ({ layer, queryClient, }) => {
|
|
12
|
+
const RuntimeContext = createContext(null);
|
|
13
|
+
const useRunner = () => {
|
|
14
|
+
const runtime = useContext(RuntimeContext);
|
|
15
|
+
if (!runtime) {
|
|
16
|
+
throw new Error("Runtime context not found. Make sure to wrap your app with RuntimeProvider");
|
|
17
|
+
}
|
|
18
|
+
return useMemo(() => (span) => (effect) => runtime.runPromise(effect.pipe(Effect.withSpan(span))), [runtime]);
|
|
19
|
+
};
|
|
20
|
+
const RuntimeProvider = ({ children }) => {
|
|
21
|
+
const runtimeRef = useRef(null);
|
|
22
|
+
if (!runtimeRef.current) {
|
|
23
|
+
runtimeRef.current = ManagedRuntime.make(layer);
|
|
24
|
+
}
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
return () => {
|
|
27
|
+
if (runtimeRef.current) {
|
|
28
|
+
runtimeRef.current.dispose();
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}, []);
|
|
32
|
+
return (_jsx(RuntimeContext.Provider, { value: runtimeRef.current, children: _jsx(QueryClientProvider, { client: queryClient, children: children }) }));
|
|
33
|
+
};
|
|
34
|
+
const useRuntime = () => {
|
|
35
|
+
const runtime = useContext(RuntimeContext);
|
|
36
|
+
if (!runtime) {
|
|
37
|
+
throw new Error("Runtime context not found. Make sure to wrap your app with RuntimeProvider");
|
|
38
|
+
}
|
|
39
|
+
return runtime;
|
|
40
|
+
};
|
|
41
|
+
return {
|
|
42
|
+
useRunner,
|
|
43
|
+
RuntimeProvider,
|
|
44
|
+
useRuntime,
|
|
45
|
+
useEffectQuery: makeUseEffectQuery(useRunner),
|
|
46
|
+
useEffectMutation: makeUseEffectMutation(useRunner),
|
|
47
|
+
useRxSubscribe: makeUseRxSubscribe(RuntimeContext),
|
|
48
|
+
useRxSubscriptionRef: makeUseRxSubscriptionRef(RuntimeContext),
|
|
49
|
+
createQueryDataHelpers: makeCreateQueryDataHelpers(queryClient),
|
|
50
|
+
};
|
|
51
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { QueryFunctionContext, skipToken, UseMutationOptions, UseQueryOptions } from "@tanstack/react-query";
|
|
2
|
+
import type * as Effect from "effect/Effect";
|
|
3
|
+
export type QueryKey = readonly [string, Record<string, unknown>?];
|
|
4
|
+
export type EffectfulError = {
|
|
5
|
+
_tag: string;
|
|
6
|
+
};
|
|
7
|
+
export type Runner<R> = () => <A, E>(span: string) => (effect: Effect.Effect<A, E, R>) => Promise<A>;
|
|
8
|
+
export type EffectfulMutationOptions<TData, TError extends EffectfulError, TVariables, R> = Omit<UseMutationOptions<TData, Error, TVariables>, "mutationFn" | "onSuccess" | "onError" | "onSettled" | "onMutate" | "retry" | "retryDelay"> & {
|
|
9
|
+
mutationKey: QueryKey;
|
|
10
|
+
mutationFn: (variables: TVariables) => Effect.Effect<TData, TError, R>;
|
|
11
|
+
};
|
|
12
|
+
export type EffectfulQueryFunction<TData, TError, TQueryKey extends QueryKey = QueryKey, R = never, TPageParam = never> = (context: QueryFunctionContext<TQueryKey, TPageParam>) => Effect.Effect<TData, TError, R>;
|
|
13
|
+
export type EffectfulQueryOptions<TData, TError, R, TQueryKey extends QueryKey = QueryKey, TPageParam = never> = Omit<UseQueryOptions<TData, Error, TData, TQueryKey>, "queryKey" | "queryFn" | "retry" | "retryDelay" | "staleTime" | "gcTime"> & {
|
|
14
|
+
queryKey: TQueryKey;
|
|
15
|
+
queryFn: EffectfulQueryFunction<TData, TError, TQueryKey, R, TPageParam> | typeof skipToken;
|
|
16
|
+
staleTime?: number;
|
|
17
|
+
gcTime?: number;
|
|
18
|
+
};
|
|
19
|
+
export interface Subscribable<A, E = never> {
|
|
20
|
+
readonly changes: unknown;
|
|
21
|
+
readonly get: () => A;
|
|
22
|
+
}
|
|
23
|
+
export interface SubscriptionOptions {
|
|
24
|
+
readonly skipInitial?: boolean;
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@effectify/react-query",
|
|
3
|
+
"version": "0.0.3",
|
|
4
|
+
"description": "Integration of Effect with TanStack Query for React",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/src/index.js",
|
|
7
|
+
"module": "./dist/src/index.js",
|
|
8
|
+
"types": "./dist/src/index.d.ts",
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"@effectify/source": "./src/index.ts",
|
|
15
|
+
"types": "./dist/src/index.d.ts",
|
|
16
|
+
"import": "./dist/src/index.js",
|
|
17
|
+
"default": "./dist/src/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"!**/*.tsbuildinfo"
|
|
23
|
+
],
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"tslib": "2.8.1",
|
|
26
|
+
"@tanstack/react-query": "5.90.10",
|
|
27
|
+
"@tanstack/query-core": "5.90.10",
|
|
28
|
+
"effect": "4.0.0-beta.31",
|
|
29
|
+
"react": "19.2.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"typescript": "5.9.3",
|
|
33
|
+
"@types/react": "19.2.7"
|
|
34
|
+
},
|
|
35
|
+
"optionalDependencies": {},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"effect": "^3.19.16 || ^4.0.0-beta"
|
|
38
|
+
}
|
|
39
|
+
}
|