@modelence/react-query 1.0.0
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 +214 -0
- package/dist/index.d.ts +97 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/examples/usage.tsx +249 -0
- package/package.json +35 -0
- package/src/index.ts +116 -0
- package/tsconfig.json +19 -0
- package/tsup.config.ts +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# @modelence/react-query
|
|
2
|
+
|
|
3
|
+
React Query utilities for Modelence method calls.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm i @modelence/react-query @tanstack/react-query
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
This package provides `getQueryOptions` and `getMutationOptions` factory functions that can be used with TanStack Query's native `useQuery` and `useMutation` hooks. This approach, recommended by TanStack, gives you direct access to TanStack Query's full API while providing Modelence-specific query configurations.
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
### Basic Query
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
import { useQuery } from '@tanstack/react-query';
|
|
21
|
+
import { getQueryOptions } from '@modelence/react-query';
|
|
22
|
+
|
|
23
|
+
function TodoList() {
|
|
24
|
+
const { data, isPending, error } = useQuery(
|
|
25
|
+
getQueryOptions('todo.getAll', { limit: 10 })
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
if (isPending) return <div>Loading...</div>;
|
|
29
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div>
|
|
33
|
+
{data?.map(todo => (
|
|
34
|
+
<div key={todo.id}>{todo.title}</div>
|
|
35
|
+
))}
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Basic Mutation
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
45
|
+
import { getMutationOptions } from '@modelence/react-query';
|
|
46
|
+
|
|
47
|
+
function CreateTodo() {
|
|
48
|
+
const queryClient = useQueryClient();
|
|
49
|
+
|
|
50
|
+
const { mutate: createTodo, isPending } = useMutation({
|
|
51
|
+
...getMutationOptions('todo.create'),
|
|
52
|
+
onSuccess: () => {
|
|
53
|
+
// Invalidate and refetch todos
|
|
54
|
+
queryClient.invalidateQueries({ queryKey: ['todo.getAll'] });
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<button
|
|
60
|
+
onClick={() => createTodo({ title: 'New Todo', completed: false })}
|
|
61
|
+
disabled={isPending}
|
|
62
|
+
>
|
|
63
|
+
{isPending ? 'Creating...' : 'Create Todo'}
|
|
64
|
+
</button>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Advanced Usage
|
|
70
|
+
|
|
71
|
+
#### Query with Additional Options
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
import { useQuery } from '@tanstack/react-query';
|
|
75
|
+
import { getQueryOptions } from '@modelence/react-query';
|
|
76
|
+
|
|
77
|
+
function TodoDetail({ id }: { id: string }) {
|
|
78
|
+
const { data: todo } = useQuery({
|
|
79
|
+
...getQueryOptions('todo.getById', { id }),
|
|
80
|
+
enabled: !!id, // Only run query if id exists
|
|
81
|
+
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
82
|
+
refetchOnWindowFocus: false,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return <div>{todo?.title}</div>;
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
#### Mutation with Default Args
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
import { useMutation } from '@tanstack/react-query';
|
|
93
|
+
import { getMutationOptions } from '@modelence/react-query';
|
|
94
|
+
|
|
95
|
+
function UpdateTodo({ todoId }: { todoId: string }) {
|
|
96
|
+
const { mutate: updateTodo } = useMutation({
|
|
97
|
+
...getMutationOptions('todo.update', { id: todoId }), // Default args
|
|
98
|
+
onSuccess: (data) => {
|
|
99
|
+
console.log('Todo updated:', data);
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<button onClick={() => updateTodo({ title: 'Updated Title' })}>
|
|
105
|
+
Update Todo
|
|
106
|
+
</button>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
#### Manual Cache Operations
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
115
|
+
import { createQueryKey, getQueryOptions } from '@modelence/react-query';
|
|
116
|
+
|
|
117
|
+
function TodoActions() {
|
|
118
|
+
const queryClient = useQueryClient();
|
|
119
|
+
|
|
120
|
+
const refreshTodos = () => {
|
|
121
|
+
queryClient.invalidateQueries({
|
|
122
|
+
queryKey: createQueryKey('todo.getAll', { limit: 10 })
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const prefetchTodo = (id: string) => {
|
|
127
|
+
queryClient.prefetchQuery({
|
|
128
|
+
...getQueryOptions('todo.getById', { id }),
|
|
129
|
+
staleTime: 10 * 60 * 1000, // 10 minutes
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<div>
|
|
135
|
+
<button onClick={refreshTodos}>Refresh Todos</button>
|
|
136
|
+
<button onClick={() => prefetchTodo('123')}>Prefetch Todo</button>
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## API Reference
|
|
143
|
+
|
|
144
|
+
### `getQueryOptions<T>(methodName, args?)`
|
|
145
|
+
|
|
146
|
+
Creates a query configuration object for use with TanStack Query's `useQuery`.
|
|
147
|
+
|
|
148
|
+
**Parameters:**
|
|
149
|
+
- `methodName` (string): The Modelence method name (e.g., 'todo.getAll')
|
|
150
|
+
- `args` (object, optional): Arguments to pass to the method
|
|
151
|
+
|
|
152
|
+
**Returns:** Query configuration object with `queryKey` and `queryFn`
|
|
153
|
+
|
|
154
|
+
### `getMutationOptions<T, TVariables>(methodName, defaultArgs?)`
|
|
155
|
+
|
|
156
|
+
Creates a mutation configuration object for use with TanStack Query's `useMutation`.
|
|
157
|
+
|
|
158
|
+
**Parameters:**
|
|
159
|
+
- `methodName` (string): The Modelence method name (e.g., 'todo.create')
|
|
160
|
+
- `defaultArgs` (object, optional): Default arguments merged with mutation variables
|
|
161
|
+
|
|
162
|
+
**Returns:** Mutation configuration object with `mutationFn`
|
|
163
|
+
|
|
164
|
+
### `createQueryKey<T, U>(methodName, args?)`
|
|
165
|
+
|
|
166
|
+
Utility function to create typed query keys for manual cache operations.
|
|
167
|
+
|
|
168
|
+
**Parameters:**
|
|
169
|
+
- `methodName` (T): The method name
|
|
170
|
+
- `args` (U, optional): The arguments
|
|
171
|
+
|
|
172
|
+
**Returns:** Typed query key array
|
|
173
|
+
|
|
174
|
+
## Migration from Modelence's useQuery/useMutation
|
|
175
|
+
|
|
176
|
+
### Before
|
|
177
|
+
|
|
178
|
+
```tsx
|
|
179
|
+
import { useQuery, useMutation } from 'modelence/client';
|
|
180
|
+
|
|
181
|
+
function TodoComponent() {
|
|
182
|
+
const { data, isFetching, error } = useQuery('todo.getAll');
|
|
183
|
+
const { mutate: createTodo } = useMutation('todo.create');
|
|
184
|
+
|
|
185
|
+
// ...
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### After
|
|
190
|
+
|
|
191
|
+
```tsx
|
|
192
|
+
import { useQuery, useMutation } from '@tanstack/react-query';
|
|
193
|
+
import { getQueryOptions, getMutationOptions } from '@modelence/react-query';
|
|
194
|
+
|
|
195
|
+
function TodoComponent() {
|
|
196
|
+
const { data, isPending: isFetching, error } = useQuery(
|
|
197
|
+
getQueryOptions('todo.getAll')
|
|
198
|
+
);
|
|
199
|
+
const { mutate: createTodo } = useMutation(
|
|
200
|
+
getMutationOptions('todo.create')
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// ...
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Benefits
|
|
208
|
+
|
|
209
|
+
1. **Full TanStack Query API**: Access to all TanStack Query features and options
|
|
210
|
+
2. **Simple and Explicit**: Clear separation between Modelence configuration and TanStack Query options
|
|
211
|
+
3. **Better TypeScript Support**: Improved type inference and safety
|
|
212
|
+
4. **Familiar API**: Standard TanStack Query patterns that developers already know
|
|
213
|
+
5. **Future-Proof**: Easy to adopt new TanStack Query features as they're released
|
|
214
|
+
6. **Composability**: Easy to combine with other TanStack Query utilities
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
type Args = Record<string, unknown>;
|
|
2
|
+
/**
|
|
3
|
+
* Creates query options for use with TanStack Query's useQuery hook.
|
|
4
|
+
*
|
|
5
|
+
* @typeParam T - The expected return type of the query
|
|
6
|
+
* @param methodName - The name of the method to query
|
|
7
|
+
* @param args - Optional arguments to pass to the method
|
|
8
|
+
* @returns Query options object for TanStack Query's useQuery
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* import { useQuery } from '@tanstack/react-query';
|
|
13
|
+
* import { modelenceQuery } from '@modelence/react-query';
|
|
14
|
+
*
|
|
15
|
+
* function MyComponent() {
|
|
16
|
+
* // Basic usage
|
|
17
|
+
* const { data } = useQuery(modelenceQuery('todo.getAll'));
|
|
18
|
+
*
|
|
19
|
+
* // With additional options
|
|
20
|
+
* const { data: todo } = useQuery({
|
|
21
|
+
* ...modelenceQuery('todo.getById', { id: '123' }),
|
|
22
|
+
* enabled: !!id,
|
|
23
|
+
* staleTime: 5 * 60 * 1000,
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* return <div>{data?.name}</div>;
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
declare function modelenceQuery<T = unknown>(methodName: string, args?: Args): {
|
|
31
|
+
queryKey: (string | Args)[];
|
|
32
|
+
queryFn: () => Promise<T>;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Creates mutation options for use with TanStack Query's useMutation hook.
|
|
36
|
+
*
|
|
37
|
+
* @typeParam T - The expected return type of the mutation
|
|
38
|
+
* @param methodName - The name of the method to mutate
|
|
39
|
+
* @param defaultArgs - Optional default arguments to merge with mutation variables
|
|
40
|
+
* @returns Mutation options object for TanStack Query's useMutation
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```tsx
|
|
44
|
+
* import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
45
|
+
* import { modelenceMutation } from '@modelence/react-query';
|
|
46
|
+
*
|
|
47
|
+
* function MyComponent() {
|
|
48
|
+
* const queryClient = useQueryClient();
|
|
49
|
+
*
|
|
50
|
+
* // Basic usage
|
|
51
|
+
* const { mutate } = useMutation(modelenceMutation('todos.create'));
|
|
52
|
+
*
|
|
53
|
+
* // With additional options
|
|
54
|
+
* const { mutate: updateTodo } = useMutation({
|
|
55
|
+
* ...modelenceMutation('todos.update'),
|
|
56
|
+
* onSuccess: () => {
|
|
57
|
+
* queryClient.invalidateQueries({ queryKey: ['todos.getAll'] });
|
|
58
|
+
* },
|
|
59
|
+
* });
|
|
60
|
+
*
|
|
61
|
+
* return <button onClick={() => mutate({ title: 'New Todo' })}>Create</button>;
|
|
62
|
+
* }
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
declare function modelenceMutation<T = unknown>(methodName: string, defaultArgs?: Args): {
|
|
66
|
+
mutationFn: (variables?: Args) => Promise<T>;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Type helper for creating properly typed query keys
|
|
70
|
+
*/
|
|
71
|
+
type ModelenceQueryKey<T extends string, U extends Args = Args> = readonly [T, U];
|
|
72
|
+
/**
|
|
73
|
+
* Utility function to create query keys for manual cache operations
|
|
74
|
+
*
|
|
75
|
+
* @param methodName - The method name
|
|
76
|
+
* @param args - The arguments
|
|
77
|
+
* @returns Typed query key
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```tsx
|
|
81
|
+
* import { useQueryClient } from '@tanstack/react-query';
|
|
82
|
+
* import { createQueryKey } from '@modelence/react-query';
|
|
83
|
+
*
|
|
84
|
+
* function TodoActions() {
|
|
85
|
+
* const queryClient = useQueryClient();
|
|
86
|
+
*
|
|
87
|
+
* const refreshTodos = () => {
|
|
88
|
+
* queryClient.invalidateQueries({
|
|
89
|
+
* queryKey: createQueryKey('todo.getAll', { limit: 10 })
|
|
90
|
+
* });
|
|
91
|
+
* };
|
|
92
|
+
* }
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
declare function createQueryKey<T extends string, U extends Args = Args>(methodName: T, args?: U): ModelenceQueryKey<T, U>;
|
|
96
|
+
|
|
97
|
+
export { type ModelenceQueryKey, createQueryKey, modelenceMutation, modelenceQuery };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import {callMethod}from'modelence/client';function s(e,n={}){return {queryKey:[e,n],queryFn:()=>callMethod(e,n)}}function u(e,n={}){return {mutationFn:(t={})=>callMethod(e,{...n,...t})}}function y(e,n={}){return [e,n]}export{y as createQueryKey,u as modelenceMutation,s as modelenceQuery};//# sourceMappingURL=index.js.map
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["modelenceQuery","methodName","args","callMethod","modelenceMutation","defaultArgs","variables","createQueryKey"],"mappings":"0CAgCO,SAASA,CACdC,CAAAA,CAAAA,CACAC,CAAa,CAAA,EACb,CAAA,CACA,OAAO,CACL,QAAU,CAAA,CAACD,CAAYC,CAAAA,CAAI,CAC3B,CAAA,OAAA,CAAS,IAAMC,UAAAA,CAAcF,CAAYC,CAAAA,CAAI,CAC/C,CACF,CAiCO,SAASE,CACdH,CAAAA,CAAAA,CACAI,CAAoB,CAAA,EACpB,CAAA,CACA,OAAO,CACL,UAAY,CAAA,CAACC,CAAkB,CAAA,EAAOH,GAAAA,UAAAA,CAAcF,CAAY,CAAA,CAAE,GAAGI,CAAAA,CAAa,GAAGC,CAAU,CAAC,CAClG,CACF,CA8BO,SAASC,CAAAA,CACdN,CACAC,CAAAA,CAAAA,CAAU,EAAC,CACc,CACzB,OAAO,CAACD,CAAAA,CAAYC,CAAI,CAC1B","file":"index.js","sourcesContent":["import { callMethod } from 'modelence/client';\n\ntype Args = Record<string, unknown>;\n\n/**\n * Creates query options for use with TanStack Query's useQuery hook.\n * \n * @typeParam T - The expected return type of the query\n * @param methodName - The name of the method to query\n * @param args - Optional arguments to pass to the method\n * @returns Query options object for TanStack Query's useQuery\n * \n * @example\n * ```tsx\n * import { useQuery } from '@tanstack/react-query';\n * import { modelenceQuery } from '@modelence/react-query';\n * \n * function MyComponent() {\n * // Basic usage\n * const { data } = useQuery(modelenceQuery('todo.getAll'));\n * \n * // With additional options\n * const { data: todo } = useQuery({\n * ...modelenceQuery('todo.getById', { id: '123' }),\n * enabled: !!id,\n * staleTime: 5 * 60 * 1000,\n * });\n * \n * return <div>{data?.name}</div>;\n * }\n * ```\n */\nexport function modelenceQuery<T = unknown>(\n methodName: string, \n args: Args = {}\n) {\n return {\n queryKey: [methodName, args],\n queryFn: () => callMethod<T>(methodName, args),\n };\n}\n\n/**\n * Creates mutation options for use with TanStack Query's useMutation hook.\n * \n * @typeParam T - The expected return type of the mutation\n * @param methodName - The name of the method to mutate\n * @param defaultArgs - Optional default arguments to merge with mutation variables\n * @returns Mutation options object for TanStack Query's useMutation\n * \n * @example\n * ```tsx\n * import { useMutation, useQueryClient } from '@tanstack/react-query';\n * import { modelenceMutation } from '@modelence/react-query';\n * \n * function MyComponent() {\n * const queryClient = useQueryClient();\n * \n * // Basic usage\n * const { mutate } = useMutation(modelenceMutation('todos.create'));\n * \n * // With additional options\n * const { mutate: updateTodo } = useMutation({\n * ...modelenceMutation('todos.update'),\n * onSuccess: () => {\n * queryClient.invalidateQueries({ queryKey: ['todos.getAll'] });\n * },\n * });\n * \n * return <button onClick={() => mutate({ title: 'New Todo' })}>Create</button>;\n * }\n * ```\n */\nexport function modelenceMutation<T = unknown>(\n methodName: string, \n defaultArgs: Args = {}\n) {\n return {\n mutationFn: (variables: Args = {}) => callMethod<T>(methodName, { ...defaultArgs, ...variables }),\n };\n}\n\n/**\n * Type helper for creating properly typed query keys\n */\nexport type ModelenceQueryKey<T extends string, U extends Args = Args> = readonly [T, U];\n\n/**\n * Utility function to create query keys for manual cache operations\n * \n * @param methodName - The method name\n * @param args - The arguments\n * @returns Typed query key\n * \n * @example\n * ```tsx\n * import { useQueryClient } from '@tanstack/react-query';\n * import { createQueryKey } from '@modelence/react-query';\n * \n * function TodoActions() {\n * const queryClient = useQueryClient();\n * \n * const refreshTodos = () => {\n * queryClient.invalidateQueries({ \n * queryKey: createQueryKey('todo.getAll', { limit: 10 }) \n * });\n * };\n * }\n * ```\n */\nexport function createQueryKey<T extends string, U extends Args = Args>(\n methodName: T,\n args: U = {} as U\n): ModelenceQueryKey<T, U> {\n return [methodName, args] as const;\n}\n"]}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
3
|
+
import { getQueryOptions, getMutationOptions, createQueryKey } from '@modelence/react-query';
|
|
4
|
+
|
|
5
|
+
interface Todo {
|
|
6
|
+
id: string;
|
|
7
|
+
title: string;
|
|
8
|
+
completed: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Example 1: Basic query usage
|
|
12
|
+
function TodoList() {
|
|
13
|
+
const { data: todos, isPending, error } = useQuery<Todo[]>(
|
|
14
|
+
getQueryOptions('todo.getAll', { limit: 10 })
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
if (isPending) return <div>Loading todos...</div>;
|
|
18
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div>
|
|
22
|
+
<h2>Todos</h2>
|
|
23
|
+
{todos?.map((todo) => (
|
|
24
|
+
<div key={todo.id}>
|
|
25
|
+
<h3>{todo.title}</h3>
|
|
26
|
+
<p>{todo.completed ? '✅' : '⏳'}</p>
|
|
27
|
+
</div>
|
|
28
|
+
))}
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Example 2: Query with options and enabled condition
|
|
34
|
+
function TodoDetail({ todoId }: { todoId: string | null }) {
|
|
35
|
+
const { data: todo, isPending } = useQuery<Todo>({
|
|
36
|
+
...getQueryOptions('todo.getById', { id: todoId }),
|
|
37
|
+
enabled: !!todoId, // Only run when todoId exists
|
|
38
|
+
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
39
|
+
retry: 3,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (!todoId) return <div>Select a todo</div>;
|
|
43
|
+
if (isPending) return <div>Loading todo...</div>;
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div>
|
|
47
|
+
<h3>{todo?.title}</h3>
|
|
48
|
+
<p>Status: {todo?.completed ? 'Completed' : 'Pending'}</p>
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Example 3: Basic mutation
|
|
54
|
+
function CreateTodo() {
|
|
55
|
+
const queryClient = useQueryClient();
|
|
56
|
+
|
|
57
|
+
const { mutate: createTodo, isPending, error } = useMutation<Todo, Error, { title: string; completed: boolean }>({
|
|
58
|
+
...getMutationOptions('todo.create'),
|
|
59
|
+
onSuccess: () => {
|
|
60
|
+
// Invalidate and refetch all todo queries
|
|
61
|
+
queryClient.invalidateQueries({ queryKey: ['todo.getAll'] });
|
|
62
|
+
},
|
|
63
|
+
onError: (error) => {
|
|
64
|
+
console.error('Failed to create todo:', error);
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
|
69
|
+
e.preventDefault();
|
|
70
|
+
const formData = new FormData(e.currentTarget);
|
|
71
|
+
const title = formData.get('title') as string;
|
|
72
|
+
|
|
73
|
+
createTodo({ title, completed: false });
|
|
74
|
+
e.currentTarget.reset();
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<form onSubmit={handleSubmit}>
|
|
79
|
+
<input
|
|
80
|
+
name="title"
|
|
81
|
+
placeholder="Enter todo title"
|
|
82
|
+
required
|
|
83
|
+
disabled={isPending}
|
|
84
|
+
/>
|
|
85
|
+
<button type="submit" disabled={isPending}>
|
|
86
|
+
{isPending ? 'Creating...' : 'Create Todo'}
|
|
87
|
+
</button>
|
|
88
|
+
{error && <p style={{ color: 'red' }}>Error: {error.message}</p>}
|
|
89
|
+
</form>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Example 4: Mutation with default args
|
|
94
|
+
function UpdateTodo({ todoId }: { todoId: string }) {
|
|
95
|
+
const queryClient = useQueryClient();
|
|
96
|
+
|
|
97
|
+
const { mutate: updateTodo, isPending } = useMutation<Todo, Error, { completed: boolean }>({
|
|
98
|
+
...getMutationOptions('todo.update', { id: todoId }), // Default id
|
|
99
|
+
onSuccess: () => {
|
|
100
|
+
// Invalidate specific todo and list
|
|
101
|
+
queryClient.invalidateQueries({ queryKey: ['todo.getById'] });
|
|
102
|
+
queryClient.invalidateQueries({ queryKey: ['todo.getAll'] });
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const toggleComplete = () => {
|
|
107
|
+
// The id is already provided in defaultArgs, so we only need the fields to update
|
|
108
|
+
updateTodo({ completed: true });
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<button onClick={toggleComplete} disabled={isPending}>
|
|
113
|
+
{isPending ? 'Updating...' : 'Mark Complete'}
|
|
114
|
+
</button>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Example 5: Manual cache operations
|
|
119
|
+
function TodoActions() {
|
|
120
|
+
const queryClient = useQueryClient();
|
|
121
|
+
|
|
122
|
+
const refreshTodos = () => {
|
|
123
|
+
queryClient.invalidateQueries({
|
|
124
|
+
queryKey: createQueryKey('todo.getAll', { limit: 10 })
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const prefetchTodo = (id: string) => {
|
|
129
|
+
queryClient.prefetchQuery<Todo>({
|
|
130
|
+
...getQueryOptions('todo.getById', { id }),
|
|
131
|
+
staleTime: 10 * 60 * 1000, // 10 minutes
|
|
132
|
+
});
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const setTodoData = (id: string, todo: Todo) => {
|
|
136
|
+
queryClient.setQueryData(
|
|
137
|
+
createQueryKey('todo.getById', { id }),
|
|
138
|
+
todo
|
|
139
|
+
);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<div>
|
|
144
|
+
<button onClick={refreshTodos}>Refresh Todos</button>
|
|
145
|
+
<button onClick={() => prefetchTodo('123')}>Prefetch Todo 123</button>
|
|
146
|
+
<button onClick={() => setTodoData('123', { id: '123', title: 'Test', completed: false })}>
|
|
147
|
+
Set Todo Data
|
|
148
|
+
</button>
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Example 6: Advanced usage with optimistic updates
|
|
154
|
+
function OptimisticTodo({ todoId }: { todoId: string }) {
|
|
155
|
+
const queryClient = useQueryClient();
|
|
156
|
+
|
|
157
|
+
const { mutate: updateTodo } = useMutation<
|
|
158
|
+
Todo,
|
|
159
|
+
Error,
|
|
160
|
+
{ id: string; completed: boolean },
|
|
161
|
+
{ previousTodo: Todo | undefined }
|
|
162
|
+
>({
|
|
163
|
+
...getMutationOptions('todo.update'),
|
|
164
|
+
onMutate: async (variables) => {
|
|
165
|
+
// Cancel outgoing refetches (so they don't overwrite our optimistic update)
|
|
166
|
+
await queryClient.cancelQueries({
|
|
167
|
+
queryKey: createQueryKey('todo.getById', { id: todoId })
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Snapshot the previous value
|
|
171
|
+
const previousTodo = queryClient.getQueryData<Todo>(
|
|
172
|
+
createQueryKey('todo.getById', { id: todoId })
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// Optimistically update to the new value
|
|
176
|
+
queryClient.setQueryData(
|
|
177
|
+
createQueryKey('todo.getById', { id: todoId }),
|
|
178
|
+
(old: Todo | undefined) => old ? { ...old, ...variables } : undefined
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// Return a context object with the snapshotted value
|
|
182
|
+
return { previousTodo };
|
|
183
|
+
},
|
|
184
|
+
onError: (_err, _variables, context) => {
|
|
185
|
+
// If the mutation fails, use the context to roll back
|
|
186
|
+
if (context?.previousTodo) {
|
|
187
|
+
queryClient.setQueryData(
|
|
188
|
+
createQueryKey('todo.getById', { id: todoId }),
|
|
189
|
+
context.previousTodo
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
onSettled: () => {
|
|
194
|
+
// Always refetch after error or success
|
|
195
|
+
queryClient.invalidateQueries({
|
|
196
|
+
queryKey: createQueryKey('todo.getById', { id: todoId })
|
|
197
|
+
});
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<button onClick={() => updateTodo({ id: todoId, completed: true })}>
|
|
203
|
+
Update with Optimistic UI
|
|
204
|
+
</button>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Main app component showcasing all examples
|
|
209
|
+
export default function App() {
|
|
210
|
+
const [selectedTodoId, setSelectedTodoId] = React.useState<string | null>(null);
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
<div style={{ padding: '20px', maxWidth: '800px', margin: '0 auto' }}>
|
|
214
|
+
<h1>@modelence/react-query Examples</h1>
|
|
215
|
+
|
|
216
|
+
<section>
|
|
217
|
+
<h2>Create Todo</h2>
|
|
218
|
+
<CreateTodo />
|
|
219
|
+
</section>
|
|
220
|
+
|
|
221
|
+
<section>
|
|
222
|
+
<h2>Todo List</h2>
|
|
223
|
+
<TodoList />
|
|
224
|
+
</section>
|
|
225
|
+
|
|
226
|
+
<section>
|
|
227
|
+
<h2>Todo Detail</h2>
|
|
228
|
+
<input
|
|
229
|
+
placeholder="Enter todo ID"
|
|
230
|
+
onChange={(e) => setSelectedTodoId(e.target.value || null)}
|
|
231
|
+
/>
|
|
232
|
+
<TodoDetail todoId={selectedTodoId} />
|
|
233
|
+
</section>
|
|
234
|
+
|
|
235
|
+
{selectedTodoId && (
|
|
236
|
+
<section>
|
|
237
|
+
<h2>Todo Actions</h2>
|
|
238
|
+
<UpdateTodo todoId={selectedTodoId} />
|
|
239
|
+
<OptimisticTodo todoId={selectedTodoId} />
|
|
240
|
+
</section>
|
|
241
|
+
)}
|
|
242
|
+
|
|
243
|
+
<section>
|
|
244
|
+
<h2>Cache Actions</h2>
|
|
245
|
+
<TodoActions />
|
|
246
|
+
</section>
|
|
247
|
+
</div>
|
|
248
|
+
);
|
|
249
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "module",
|
|
3
|
+
"name": "@modelence/react-query",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "React Query utilities for Modelence",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"import": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"main": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsup",
|
|
16
|
+
"dev": "tsup --watch",
|
|
17
|
+
"prepublishOnly": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"author": "Modelence",
|
|
20
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"@tanstack/react-query": ">=5.0.0",
|
|
23
|
+
"react": ">=18.0.0"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"modelence": "^0.5.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@tanstack/react-query": "^5.76.2",
|
|
30
|
+
"@types/react": "^19.0.0",
|
|
31
|
+
"react": "^19.0.0",
|
|
32
|
+
"tsup": "^8.3.6",
|
|
33
|
+
"typescript": "^5.7.2"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { callMethod } from 'modelence/client';
|
|
2
|
+
|
|
3
|
+
type Args = Record<string, unknown>;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates query options for use with TanStack Query's useQuery hook.
|
|
7
|
+
*
|
|
8
|
+
* @typeParam T - The expected return type of the query
|
|
9
|
+
* @param methodName - The name of the method to query
|
|
10
|
+
* @param args - Optional arguments to pass to the method
|
|
11
|
+
* @returns Query options object for TanStack Query's useQuery
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* import { useQuery } from '@tanstack/react-query';
|
|
16
|
+
* import { modelenceQuery } from '@modelence/react-query';
|
|
17
|
+
*
|
|
18
|
+
* function MyComponent() {
|
|
19
|
+
* // Basic usage
|
|
20
|
+
* const { data } = useQuery(modelenceQuery('todo.getAll'));
|
|
21
|
+
*
|
|
22
|
+
* // With additional options
|
|
23
|
+
* const { data: todo } = useQuery({
|
|
24
|
+
* ...modelenceQuery('todo.getById', { id: '123' }),
|
|
25
|
+
* enabled: !!id,
|
|
26
|
+
* staleTime: 5 * 60 * 1000,
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* return <div>{data?.name}</div>;
|
|
30
|
+
* }
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export function modelenceQuery<T = unknown>(
|
|
34
|
+
methodName: string,
|
|
35
|
+
args: Args = {}
|
|
36
|
+
) {
|
|
37
|
+
return {
|
|
38
|
+
queryKey: [methodName, args],
|
|
39
|
+
queryFn: () => callMethod<T>(methodName, args),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Creates mutation options for use with TanStack Query's useMutation hook.
|
|
45
|
+
*
|
|
46
|
+
* @typeParam T - The expected return type of the mutation
|
|
47
|
+
* @param methodName - The name of the method to mutate
|
|
48
|
+
* @param defaultArgs - Optional default arguments to merge with mutation variables
|
|
49
|
+
* @returns Mutation options object for TanStack Query's useMutation
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```tsx
|
|
53
|
+
* import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
54
|
+
* import { modelenceMutation } from '@modelence/react-query';
|
|
55
|
+
*
|
|
56
|
+
* function MyComponent() {
|
|
57
|
+
* const queryClient = useQueryClient();
|
|
58
|
+
*
|
|
59
|
+
* // Basic usage
|
|
60
|
+
* const { mutate } = useMutation(modelenceMutation('todos.create'));
|
|
61
|
+
*
|
|
62
|
+
* // With additional options
|
|
63
|
+
* const { mutate: updateTodo } = useMutation({
|
|
64
|
+
* ...modelenceMutation('todos.update'),
|
|
65
|
+
* onSuccess: () => {
|
|
66
|
+
* queryClient.invalidateQueries({ queryKey: ['todos.getAll'] });
|
|
67
|
+
* },
|
|
68
|
+
* });
|
|
69
|
+
*
|
|
70
|
+
* return <button onClick={() => mutate({ title: 'New Todo' })}>Create</button>;
|
|
71
|
+
* }
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export function modelenceMutation<T = unknown>(
|
|
75
|
+
methodName: string,
|
|
76
|
+
defaultArgs: Args = {}
|
|
77
|
+
) {
|
|
78
|
+
return {
|
|
79
|
+
mutationFn: (variables: Args = {}) => callMethod<T>(methodName, { ...defaultArgs, ...variables }),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Type helper for creating properly typed query keys
|
|
85
|
+
*/
|
|
86
|
+
export type ModelenceQueryKey<T extends string, U extends Args = Args> = readonly [T, U];
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Utility function to create query keys for manual cache operations
|
|
90
|
+
*
|
|
91
|
+
* @param methodName - The method name
|
|
92
|
+
* @param args - The arguments
|
|
93
|
+
* @returns Typed query key
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* ```tsx
|
|
97
|
+
* import { useQueryClient } from '@tanstack/react-query';
|
|
98
|
+
* import { createQueryKey } from '@modelence/react-query';
|
|
99
|
+
*
|
|
100
|
+
* function TodoActions() {
|
|
101
|
+
* const queryClient = useQueryClient();
|
|
102
|
+
*
|
|
103
|
+
* const refreshTodos = () => {
|
|
104
|
+
* queryClient.invalidateQueries({
|
|
105
|
+
* queryKey: createQueryKey('todo.getAll', { limit: 10 })
|
|
106
|
+
* });
|
|
107
|
+
* };
|
|
108
|
+
* }
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
export function createQueryKey<T extends string, U extends Args = Args>(
|
|
112
|
+
methodName: T,
|
|
113
|
+
args: U = {} as U
|
|
114
|
+
): ModelenceQueryKey<T, U> {
|
|
115
|
+
return [methodName, args] as const;
|
|
116
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"lib": ["ES2020", "DOM"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "Bundler",
|
|
7
|
+
"allowSyntheticDefaultImports": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"outDir": "dist",
|
|
15
|
+
"jsx": "react-jsx"
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*"],
|
|
18
|
+
"exclude": ["node_modules", "dist"]
|
|
19
|
+
}
|
package/tsup.config.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { defineConfig } from 'tsup'
|
|
2
|
+
|
|
3
|
+
export default defineConfig((options) => ({
|
|
4
|
+
entry: ['src/index.ts'],
|
|
5
|
+
format: ['esm'],
|
|
6
|
+
dts: {
|
|
7
|
+
resolve: true,
|
|
8
|
+
entry: {
|
|
9
|
+
index: 'src/index.ts'
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
splitting: true,
|
|
13
|
+
clean: true,
|
|
14
|
+
outDir: 'dist',
|
|
15
|
+
sourcemap: true,
|
|
16
|
+
minify: !options.watch,
|
|
17
|
+
treeshake: !options.watch,
|
|
18
|
+
jsx: true,
|
|
19
|
+
esbuildOptions: (options) => {
|
|
20
|
+
options.resolveExtensions = ['.ts', '.js', '.tsx', '.jsx']
|
|
21
|
+
return options
|
|
22
|
+
},
|
|
23
|
+
external: [
|
|
24
|
+
'react',
|
|
25
|
+
'@tanstack/react-query',
|
|
26
|
+
'modelence'
|
|
27
|
+
]
|
|
28
|
+
}));
|