@void-snippets/react 0.2.1 → 0.3.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @void-snippets/react
2
2
 
3
- TanStack Query v5 hook factory for React. Generates a fully-typed set of hooks (`useList`, `useGet`, `useMutations`, `useInfinite`) from a `ResourceService` instance — with zero manual type annotations required.
3
+ TanStack Query v5 resource hooks factory + general-purpose React hooks. All fully typed, zero boilerplate.
4
4
 
5
5
  ## Installation
6
6
 
@@ -10,119 +10,87 @@ npm install @void-snippets/react @void-snippets/client @tanstack/react-query axi
10
10
 
11
11
  ---
12
12
 
13
- ## Quick Start
13
+ ## Setup
14
14
 
15
- ### 1. Configure axios once at your app entry point
15
+ ### Configure axios once
16
16
 
17
17
  ```ts
18
- // src/main.ts
19
18
  import axios from 'axios';
20
19
  import { configure } from '@void-snippets/client';
21
-
22
- const axiosInstance = axios.create({ baseURL: 'https://api.example.com' });
23
- configure(axiosInstance);
20
+ configure(axios.create({ baseURL: 'https://api.example.com' }));
24
21
  ```
25
22
 
26
- ### 2. Set up TanStack Query
23
+ ### Wrap your app with QueryClientProvider
27
24
 
28
25
  ```tsx
29
- // src/main.tsx
30
26
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
31
-
32
27
  const queryClient = new QueryClient();
33
-
34
28
  function App() {
35
- return (
36
- <QueryClientProvider client={queryClient}>
37
- <YourApp />
38
- </QueryClientProvider>
39
- );
29
+ return <QueryClientProvider client={queryClient}><YourApp /></QueryClientProvider>;
40
30
  }
41
31
  ```
42
32
 
43
- ### 3. Define a resource service
33
+ ---
44
34
 
45
- ```ts
46
- // contacts/contacts.api.ts
47
- import { ResourceService } from '@void-snippets/client';
48
- import type { Contact } from './contacts.types';
35
+ ## Resource Hooks — `createResourceHooks`
49
36
 
37
+ Define a service once, get all hooks free — fully typed, no generics needed.
38
+
39
+ ```ts
40
+ // contacts.api.ts
50
41
  export class ContactsApiService extends ResourceService<
51
- Contact.Id,
52
- Contact.Base,
53
- Contact.WithCreatedBy,
54
- Contact.Apis.CreatePayload,
55
- Contact.Apis.UpdatePayload
42
+ Contact.Id, Contact.Base, Contact.WithCreatedBy,
43
+ Contact.Apis.CreatePayload, Contact.Apis.UpdatePayload
56
44
  > {
57
- constructor() {
58
- super('/contacts');
59
- }
45
+ constructor() { super('/contacts'); }
60
46
  }
61
-
62
47
  export const ContactsApis = new ContactsApiService();
63
- ```
64
48
 
65
- ### 4. Create hooks
66
-
67
- ```ts
68
- // contacts/contacts.hooks.ts
69
- import { createResourceHooks } from '@void-snippets/react';
70
- import { ContactsApis } from './contacts.api';
71
-
72
- // All types are inferred from ContactsApis — no generics needed
49
+ // contacts.hooks.ts
73
50
  export const contactHooks = createResourceHooks('contacts', ContactsApis);
74
51
  ```
75
52
 
76
- ### 5. Use in components
53
+ ### `useList(params?)`
77
54
 
78
55
  ```tsx
79
- // List with pagination
80
- function ContactsList() {
81
- const { contacts, isContactsLoading, pagination } = contactHooks.useList({
82
- page: 1,
83
- limit: 20,
84
- });
85
-
86
- if (isContactsLoading) return <Spinner />;
87
- return contacts.map(c => <ContactCard key={c._id} contact={c} />);
88
- }
89
-
90
- // Single item
91
- function ContactDetail({ id }: { id: Contact.Id }) {
92
- const { data, isLoading } = contactHooks.useGet(id);
93
- // data is typed as Contact.WithCreatedBy ✅
94
- }
56
+ const { list, isLoading, pagination, error, invalidate } =
57
+ contactHooks.useList({ page: 1, limit: 20 });
58
+ // list is typed as Contact.Base[]
59
+ ```
95
60
 
96
- // Mutations (auto-invalidate the list on success)
97
- function CreateContactForm() {
98
- const { create, update, delete: remove } = contactHooks.useMutations();
61
+ | Key | Type |
62
+ |---|---|
63
+ | `list` | `TBase[]` |
64
+ | `pagination` | `VSPagination` |
65
+ | `isLoading` | `boolean` |
66
+ | `error` | `Error \| null` |
67
+ | `invalidate` | `() => void` |
99
68
 
100
- return (
101
- <button onClick={() => create.mutate({ name: 'John' })}>
102
- Create
103
- </button>
104
- );
105
- }
69
+ ### `useGet(id, staleTime?)`
106
70
 
107
- // Infinite scroll
108
- function InfiniteContactsList() {
109
- const { data, fetchNextPage, hasNextPage } = contactHooks.useInfinite({ limit: 20 });
110
- const all = data?.pages.flatMap(p => p.items) ?? [];
111
- }
71
+ ```tsx
72
+ const { item, isLoading, error, refetch } = contactHooks.useGet(id);
73
+ // item is typed as Contact.WithCreatedBy
112
74
  ```
113
75
 
114
- ---
76
+ ### `useMutations()`
77
+
78
+ ```tsx
79
+ const { create, update, remove } = contactHooks.useMutations();
115
80
 
116
- ## Custom API Response Shapes
81
+ create.mutate({ name: 'John' });
82
+ update.mutate({ _id: id, payload: { name: 'Jane' } });
83
+ remove.mutate(id);
84
+ ```
117
85
 
118
- By default the library expects this shape from your API:
86
+ ### `useInfinite(params?)`
119
87
 
120
- ```json
121
- // List: { "data": { "items": [], "page": 1, "limit": 10, "totalPages": 5, "totalDocuments": 42 } }
122
- // Single: { "data": { "_id": "...", "name": "..." } }
88
+ ```tsx
89
+ const { data, fetchNextPage, hasNextPage } = contactHooks.useInfinite({ limit: 20 });
90
+ const all = data?.pages.flatMap(p => p.items) ?? [];
123
91
  ```
124
92
 
125
- If your API looks different, pass adapters as a third argument:
93
+ ### Custom API shapes
126
94
 
127
95
  ```ts
128
96
  export const contactHooks = createResourceHooks('contacts', ContactsApis, {
@@ -143,25 +111,71 @@ export const contactHooks = createResourceHooks('contacts', ContactsApis, {
143
111
 
144
112
  ---
145
113
 
146
- ## Hook Reference
114
+ ## General-Purpose Hooks
147
115
 
148
- ### `useList(params?)`
149
- | Returned key | Type | Description |
150
- |---|---|---|
151
- | `[prefix]` | `TBase[]` | e.g. `contacts` for prefix `"contacts"` |
152
- | `pagination` | `TPagination` | `{ page, limit, totalPages, totalDocuments }` |
153
- | `is[Prefix]Loading` | `boolean` | e.g. `isContactsLoading` |
154
- | `[prefix]Error` | `Error \| null` | e.g. `contactsError` |
155
- | `invalidate[Prefix]` | `() => void` | e.g. `invalidateContacts` |
116
+ ### `useAlertMessage(autoHideDuration?)`
156
117
 
157
- ### `useGet(id, staleTime?)`
158
- Returns `{ data, isLoading, error, refetch }`. Skips the query if `id` is empty.
118
+ ```tsx
119
+ const { alert, showAlert, hideAlert } = useAlertMessage(3000);
159
120
 
160
- ### `useMutations()`
161
- Returns `{ create, update, delete }` — each is a TanStack `UseMutationResult`. All auto-invalidate the list cache on success.
121
+ showAlert('Saved!', 'success');
122
+ showAlert('Something went wrong', 'error');
123
+ showAlert(<b>Custom JSX</b>, 'info');
124
+ // alert.isVisible, alert.message, alert.type
125
+ ```
162
126
 
163
- ### `useInfinite(params?)`
164
- Returns the standard TanStack `useInfiniteQuery` result with `items` and `pagination` per page.
127
+ Variants: `"success" | "info" | "error"`
128
+
129
+ ---
130
+
131
+ ### `useAsyncState<T>(initialData?)`
132
+
133
+ ```tsx
134
+ const { data, isLoading, isError, execute } = useAsyncState<User>();
135
+
136
+ const [err, user] = await execute(() => fetchUser(id), {
137
+ onSuccess: (u) => showAlert(`Welcome ${u.name}`, 'success'),
138
+ onError: (e) => showAlert(e.message, 'error'),
139
+ });
140
+ ```
141
+
142
+ Status values: `"idle" | "pending" | "success" | "error"`
143
+
144
+ ---
145
+
146
+ ### `useCallTimer(startedAt?)`
147
+
148
+ ```tsx
149
+ const duration = useCallTimer(call.startedAt); // "02:45"
150
+ const duration = useCallTimer(null); // "00:00"
151
+ ```
152
+
153
+ ---
154
+
155
+ ### `useModal<T>()`
156
+
157
+ ```tsx
158
+ const modal = useModal<Contact.Base>();
159
+
160
+ modal.openCreateModal(); // data → null
161
+ modal.openEditModal(contact); // data → contact
162
+
163
+ if (modal.data) { /* edit mode */ } else { /* create mode */ }
164
+ ```
165
+
166
+ ---
167
+
168
+ ### `usePagination(initialPage?, initialLimit?)`
169
+
170
+ ```tsx
171
+ const { queryParams, onPaginationChange } = usePagination(1, 20);
172
+
173
+ // Wire directly to useList
174
+ const { list } = contactHooks.useList(queryParams);
175
+
176
+ // Wire to your pagination UI
177
+ <Pagination onChange={onPaginationChange} total={pagination.totalDocuments} />
178
+ ```
165
179
 
166
180
  ---
167
181
 
package/dist/index.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as _tanstack_react_query from '@tanstack/react-query';
2
- import { ResourceAdapters, TQueryParams, TPagination, ResourceListResult } from '@void-snippets/core';
2
+ import { VSAdapters, VSQueryParams, VSPagination, VSListResult } from '@void-snippets/core';
3
+ import { ReactNode } from 'react';
3
4
 
4
- type CapitalizeStr<S extends string> = S extends `${infer F}${infer Rest}` ? `${Uppercase<F>}${Rest}` : S;
5
5
  interface WithResourceTypes {
6
6
  readonly __types: {
7
7
  id: unknown;
@@ -20,21 +20,23 @@ type Create<S extends WithResourceTypes> = S["__types"]["create"];
20
20
  type Update<S extends WithResourceTypes> = S["__types"]["update"];
21
21
  type ListRaw<S extends WithResourceTypes> = S["__types"]["listRaw"];
22
22
  type SingleRaw<S extends WithResourceTypes> = S["__types"]["singleRaw"];
23
- type UseListReturn<K extends string, TBase> = {
24
- [P in K]: TBase[];
25
- } & {
26
- pagination: TPagination;
27
- } & {
28
- [P in `is${CapitalizeStr<K>}Loading`]: boolean;
29
- } & {
30
- [P in `${K}Error`]: Error | null;
31
- } & {
32
- [P in `invalidate${CapitalizeStr<K>}`]: () => void;
33
- };
34
- interface CreateResourceHooksOptions<TListRaw, TBase, TSingleRaw, TDetail> {
23
+ interface VSUseListReturn<TBase> {
24
+ list: TBase[];
25
+ pagination: VSPagination;
26
+ isLoading: boolean;
27
+ error: Error | null;
28
+ invalidate: () => void;
29
+ }
30
+ interface VSUseGetReturn<TDetail> {
31
+ item: TDetail | undefined;
32
+ isLoading: boolean;
33
+ error: Error | null;
34
+ refetch: () => void;
35
+ }
36
+ interface VSResourceHooksOptions<TListRaw, TBase, TSingleRaw, TDetail> {
35
37
  /**
36
- * Adapters map your API's raw response shapes to the library's internal
37
- * format. Omit this entirely if your API matches the default shape:
38
+ * Adapters map your API's raw response to the library's internal format.
39
+ * Omit if your API matches the default shape:
38
40
  * List: { data: { items, page, limit, totalPages, totalDocuments } }
39
41
  * Single: { data: <item> }
40
42
  *
@@ -54,54 +56,186 @@ interface CreateResourceHooksOptions<TListRaw, TBase, TSingleRaw, TDetail> {
54
56
  * },
55
57
  * })
56
58
  */
57
- adapters?: ResourceAdapters<TListRaw, TBase, TSingleRaw, TDetail>;
59
+ adapters?: VSAdapters<TListRaw, TBase, TSingleRaw, TDetail>;
58
60
  /**
59
61
  * Default params passed to useList and useInfinite when none are provided.
60
62
  * @default { page: 1, limit: 10 }
61
63
  */
62
- defaultParams?: TQueryParams;
64
+ defaultParams?: VSQueryParams;
63
65
  }
64
66
  /**
65
67
  * Creates a set of TanStack Query hooks for a resource.
66
- * All types are fully inferred from the `apiService` instance — no generics
67
- * need to be passed manually.
68
+ * All types are fully inferred from the `apiService` instance — no generics needed.
68
69
  *
69
- * @param queryKeyPrefix - TanStack Query cache key prefix and the base name
70
- * for the returned hook properties.
71
- * e.g. "contacts" → { contacts, isContactsLoading, ... }
70
+ * @param queryKeyPrefix - TanStack Query cache key. Used to scope the cache
71
+ * and for auto-invalidation. e.g. "contacts"
72
72
  * @param apiService - An instance of ResourceService (or a subclass).
73
73
  * @param options - Optional adapters and default params.
74
74
  *
75
75
  * @example
76
- * // contacts.hooks.ts
77
- * import { createResourceHooks } from '@void-snippets/react';
78
- * import { ContactsApis } from './contacts.api';
79
- *
80
- * // No generics needed — all types are inferred from ContactsApis
81
76
  * export const contactHooks = createResourceHooks('contacts', ContactsApis);
82
77
  *
83
- * // In a component:
84
- * const { contacts, isContactsLoading } = contactHooks.useList();
85
- * const { data } = contactHooks.useGet(id); // data: Contact.WithCreatedBy
86
- * const { create, update, delete: remove } = contactHooks.useMutations();
78
+ * // useList generic fixed shape
79
+ * const { list, isLoading, pagination, error, invalidate } = contactHooks.useList();
80
+ * // list is typed as Contact.Base[]
81
+ *
82
+ * // useGet
83
+ * const { item, isLoading, error, refetch } = contactHooks.useGet(id);
84
+ * // item is typed as Contact.WithCreatedBy ✅
85
+ *
86
+ * // useMutations
87
+ * const { create, update, remove } = contactHooks.useMutations();
87
88
  */
88
- declare function createResourceHooks<K extends string, S extends WithResourceTypes>(queryKeyPrefix: K, apiService: S, options?: CreateResourceHooksOptions<ListRaw<S>, Base<S>, SingleRaw<S>, Detail<S>>): {
89
- useList: (params?: TQueryParams) => UseListReturn<K, Base<S>>;
90
- useGet: (id: Id<S>, staleTime?: number) => {
91
- data: _tanstack_react_query.NoInfer<Detail<S>> | undefined;
92
- isLoading: boolean;
93
- error: Error | null;
94
- refetch: (options?: _tanstack_react_query.RefetchOptions) => Promise<_tanstack_react_query.QueryObserverResult<_tanstack_react_query.NoInfer<Detail<S>>, Error>>;
95
- };
89
+ declare function createResourceHooks<K extends string, S extends WithResourceTypes>(queryKeyPrefix: K, apiService: S, options?: VSResourceHooksOptions<ListRaw<S>, Base<S>, SingleRaw<S>, Detail<S>>): {
90
+ useList: (params?: VSQueryParams) => VSUseListReturn<Base<S>>;
91
+ useGet: (id: Id<S>, staleTime?: number) => VSUseGetReturn<Detail<S>>;
96
92
  useMutations: () => {
97
93
  create: _tanstack_react_query.UseMutationResult<Detail<S>, Error, Create<S>, unknown>;
98
94
  update: _tanstack_react_query.UseMutationResult<Detail<S>, Error, {
99
95
  _id: Id<S>;
100
96
  payload: Update<S>;
101
97
  }, unknown>;
102
- delete: _tanstack_react_query.UseMutationResult<Detail<S>, Error, Id<S>, unknown>;
98
+ remove: _tanstack_react_query.UseMutationResult<Detail<S>, Error, Id<S>, unknown>;
103
99
  };
104
- useInfinite: (params?: TQueryParams) => _tanstack_react_query.UseInfiniteQueryResult<_tanstack_react_query.InfiniteData<ResourceListResult<Base<S>>, unknown>, Error>;
100
+ useInfinite: (params?: VSQueryParams) => _tanstack_react_query.UseInfiniteQueryResult<_tanstack_react_query.InfiniteData<VSListResult<Base<S>>, unknown>, Error>;
105
101
  };
106
102
 
107
- export { type CreateResourceHooksOptions, type UseListReturn, createResourceHooks };
103
+ type VSAlertVariant = "success" | "info" | "error";
104
+ interface VSAlertState {
105
+ message: ReactNode | string;
106
+ type: VSAlertVariant;
107
+ isVisible: boolean;
108
+ }
109
+ /**
110
+ * Manages alert/toast message state with optional auto-hide.
111
+ *
112
+ * @param autoHideDuration - ms before alert hides automatically. Pass 0 to disable. Default: 3000
113
+ *
114
+ * @example
115
+ * const { alert, showAlert, hideAlert } = useAlertMessage();
116
+ * showAlert('Saved successfully!', 'success');
117
+ * showAlert(<b>Something went wrong</b>, 'error');
118
+ */
119
+ declare function useAlertMessage(autoHideDuration?: number): {
120
+ alert: VSAlertState;
121
+ showAlert: (message: ReactNode | string, type?: VSAlertVariant) => void;
122
+ hideAlert: () => void;
123
+ };
124
+
125
+ type VSAsyncStatus = "idle" | "pending" | "success" | "error";
126
+ interface VSAsyncState<T> {
127
+ data: T | null;
128
+ status: VSAsyncStatus;
129
+ error: Error | null;
130
+ }
131
+ interface VSUseAsyncStateReturn<T> extends VSAsyncState<T> {
132
+ isLoading: boolean;
133
+ isSuccess: boolean;
134
+ isError: boolean;
135
+ setData: (data: T | null) => void;
136
+ setError: (error: Error | null) => void;
137
+ reset: () => void;
138
+ /**
139
+ * Executes an async function, updates state, and returns a [err, data] tuple.
140
+ * Allows immediate result handling without try/catch.
141
+ *
142
+ * @example
143
+ * const [err, data] = await execute(() => ContactsApis.create(payload));
144
+ * if (err) return showAlert(err.message, 'error');
145
+ * showAlert('Created!', 'success');
146
+ */
147
+ execute: (asyncFn: () => Promise<T>, options?: {
148
+ onSuccess?: (data: T) => void;
149
+ onError?: (error: Error) => void;
150
+ }) => Promise<[Error, null] | [null, T]>;
151
+ }
152
+ /**
153
+ * Generic async state machine — tracks data, status, and error for any async operation.
154
+ * Pair with any async function: API calls, file reads, timers, etc.
155
+ *
156
+ * @param initialData - Optional initial data value. Default: null
157
+ *
158
+ * @example
159
+ * const { data, isLoading, isError, execute } = useAsyncState<User>();
160
+ *
161
+ * const handleSubmit = async () => {
162
+ * const [err, user] = await execute(() => fetchUser(id));
163
+ * if (err) return;
164
+ * console.log(user.name);
165
+ * };
166
+ */
167
+ declare function useAsyncState<T>(initialData?: T | null): VSUseAsyncStateReturn<T>;
168
+
169
+ /**
170
+ * Tracks elapsed time from a given start timestamp — useful for call durations,
171
+ * countdowns, or any elapsed-time display.
172
+ *
173
+ * @param startedAt - Unix timestamp in ms (e.g. Date.now()). Pass null/undefined to reset.
174
+ * @returns Formatted duration string "MM:SS"
175
+ *
176
+ * @example
177
+ * const duration = useCallTimer(call.startedAt);
178
+ * // duration → "02:45"
179
+ *
180
+ * // Reset when no active call
181
+ * const duration = useCallTimer(activeCall ? activeCall.startedAt : null);
182
+ */
183
+ declare function useCallTimer(startedAt?: number | null): string;
184
+
185
+ interface VSModalReturn<T> {
186
+ isOpen: boolean;
187
+ data: T | null;
188
+ isLoading: boolean;
189
+ openCreateModal: () => void;
190
+ openEditModal: (editData: T) => void;
191
+ setLoading: (loading: boolean) => void;
192
+ closeModal: () => void;
193
+ setModal: (open: boolean, editData?: T | null) => void;
194
+ }
195
+ /**
196
+ * Manages modal open/close state with optional data payload and loading state.
197
+ * Works for both create and edit modals — pass data to distinguish the mode.
198
+ *
199
+ * @typeParam T - The type of data the modal operates on (e.g. a Contact, User, etc.)
200
+ *
201
+ * @example
202
+ * const modal = useModal<Contact.Base>();
203
+ *
204
+ * modal.openCreateModal(); // data → null (create mode)
205
+ * modal.openEditModal(contact); // data → contact (edit mode)
206
+ *
207
+ * if (modal.data) {
208
+ * // Edit mode — modal.data is Contact.Base
209
+ * } else {
210
+ * // Create mode
211
+ * }
212
+ */
213
+ declare function useModal<T = unknown>(): VSModalReturn<T>;
214
+
215
+ interface VSPaginationReturn {
216
+ page: number;
217
+ limit: number;
218
+ onPaginationChange: (newPage: number, newLimit: number) => void;
219
+ resetPagination: () => void;
220
+ setPage: (page: number) => void;
221
+ setLimit: (limit: number) => void;
222
+ /** Ready-to-use query params object — pass directly to useList() */
223
+ queryParams: VSQueryParams;
224
+ }
225
+ /**
226
+ * Manages pagination state and produces a ready-to-use queryParams object
227
+ * compatible with createResourceHooks' useList() and useInfinite().
228
+ *
229
+ * @param initialPage - Starting page. Default: 1
230
+ * @param initialLimit - Items per page. Default: 10
231
+ *
232
+ * @example
233
+ * const { queryParams, onPaginationChange } = usePagination(1, 20);
234
+ *
235
+ * const { list, isLoading } = contactHooks.useList(queryParams);
236
+ *
237
+ * <Pagination onChange={onPaginationChange} total={pagination.totalDocuments} />
238
+ */
239
+ declare function usePagination(initialPage?: number, initialLimit?: number): VSPaginationReturn;
240
+
241
+ export { type VSAlertState, type VSAlertVariant, type VSAsyncStatus, type VSModalReturn, type VSPaginationReturn, type VSResourceHooksOptions, type VSUseAsyncStateReturn, type VSUseGetReturn, type VSUseListReturn, createResourceHooks, useAlertMessage, useAsyncState, useCallTimer, useModal, usePagination };