@octanejs/inertia 0.0.1

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/src/useHttp.ts ADDED
@@ -0,0 +1,457 @@
1
+ import {
2
+ CancelToken,
3
+ Errors,
4
+ ErrorValue,
5
+ FormDataConvertible,
6
+ FormDataErrors,
7
+ FormDataKeys,
8
+ FormDataType,
9
+ FormDataValues,
10
+ hasFiles,
11
+ http,
12
+ HttpCancelledError,
13
+ HttpProgressEvent,
14
+ HttpResponseError,
15
+ mergeDataIntoQueryString,
16
+ Method,
17
+ objectToFormData,
18
+ Progress,
19
+ UrlMethodPair,
20
+ UseFormArguments,
21
+ UseFormTransformCallback,
22
+ UseFormUtils,
23
+ UseFormWithPrecognitionArguments,
24
+ UseHttpSubmitArguments,
25
+ UseHttpSubmitOptions,
26
+ } from '@inertiajs/core';
27
+ import { cloneDeep } from 'es-toolkit';
28
+ import {
29
+ NamedInputEvent,
30
+ toSimpleValidationErrors,
31
+ ValidationConfig,
32
+ Validator,
33
+ } from 'laravel-precognition';
34
+ import { useCallback, useMemo, useRef, useState } from 'octane';
35
+ import useFormState, { SetDataAction } from './useFormState';
36
+ import useRemember from './useRemember';
37
+
38
+ const slots = Object.fromEntries(
39
+ [
40
+ 'abortController',
41
+ 'excludeKeys',
42
+ 'pendingOptimistic',
43
+ 'response',
44
+ 'submit',
45
+ 'cancel',
46
+ 'submitWithArgs',
47
+ 'submitMethods',
48
+ ].map((name) => [name, Symbol(`Inertia.useHttp.${name}`)]),
49
+ ) as Record<string, symbol>;
50
+
51
+ type PrecognitionValidationConfig<TKeys> = ValidationConfig & {
52
+ only?: TKeys[] | Iterable<TKeys> | ArrayLike<TKeys>;
53
+ };
54
+
55
+ export interface UseHttpProps<TForm extends object, TResponse = unknown> {
56
+ data: TForm;
57
+ isDirty: boolean;
58
+ errors: FormDataErrors<TForm>;
59
+ hasErrors: boolean;
60
+ processing: boolean;
61
+ progress: Progress | null;
62
+ wasSuccessful: boolean;
63
+ recentlySuccessful: boolean;
64
+ response: TResponse | null;
65
+ setData: SetDataAction<TForm>;
66
+ transform: (callback: UseFormTransformCallback<TForm>) => void;
67
+ setDefaults: {
68
+ (): void;
69
+ <T extends FormDataKeys<TForm>>(field: T, value: FormDataValues<TForm, T>): void;
70
+ (fields: Partial<TForm>): void;
71
+ };
72
+ reset: <K extends FormDataKeys<TForm>>(...fields: K[]) => void;
73
+ clearErrors: <K extends FormDataKeys<TForm>>(...fields: K[]) => void;
74
+ resetAndClearErrors: <K extends FormDataKeys<TForm>>(...fields: K[]) => void;
75
+ setError: {
76
+ <K extends FormDataKeys<TForm>>(field: K, value: ErrorValue): void;
77
+ (errors: FormDataErrors<TForm>): void;
78
+ };
79
+ submit: (...args: UseHttpSubmitArguments<TResponse, TForm>) => Promise<TResponse>;
80
+ get: (url: string, options?: UseHttpSubmitOptions<TResponse, TForm>) => Promise<TResponse>;
81
+ post: (url: string, options?: UseHttpSubmitOptions<TResponse, TForm>) => Promise<TResponse>;
82
+ put: (url: string, options?: UseHttpSubmitOptions<TResponse, TForm>) => Promise<TResponse>;
83
+ patch: (url: string, options?: UseHttpSubmitOptions<TResponse, TForm>) => Promise<TResponse>;
84
+ delete: (url: string, options?: UseHttpSubmitOptions<TResponse, TForm>) => Promise<TResponse>;
85
+ cancel: () => void;
86
+ dontRemember: <K extends FormDataKeys<TForm>>(...fields: K[]) => UseHttpProps<TForm, TResponse>;
87
+ optimistic: (callback: (currentData: TForm) => Partial<TForm>) => UseHttpProps<TForm, TResponse>;
88
+ withAllErrors: () => UseHttpProps<TForm, TResponse>;
89
+ withPrecognition: (
90
+ ...args: UseFormWithPrecognitionArguments
91
+ ) => UseHttpPrecognitiveProps<TForm, TResponse>;
92
+ }
93
+
94
+ export interface UseHttpValidationProps<TForm extends object, TResponse = unknown> {
95
+ invalid: <K extends FormDataKeys<TForm>>(field: K) => boolean;
96
+ setValidationTimeout: (duration: number) => UseHttpPrecognitiveProps<TForm, TResponse>;
97
+ touch: <K extends FormDataKeys<TForm>>(
98
+ field: K | NamedInputEvent | Array<K>,
99
+ ...fields: K[]
100
+ ) => UseHttpPrecognitiveProps<TForm, TResponse>;
101
+ touched: <K extends FormDataKeys<TForm>>(field?: K) => boolean;
102
+ valid: <K extends FormDataKeys<TForm>>(field: K) => boolean;
103
+ validate: <K extends FormDataKeys<TForm>>(
104
+ field?: K | NamedInputEvent | PrecognitionValidationConfig<K>,
105
+ config?: PrecognitionValidationConfig<K>,
106
+ ) => UseHttpPrecognitiveProps<TForm, TResponse>;
107
+ validateFiles: () => UseHttpPrecognitiveProps<TForm, TResponse>;
108
+ validating: boolean;
109
+ validator: () => Validator;
110
+ withAllErrors: () => UseHttpPrecognitiveProps<TForm, TResponse>;
111
+ withoutFileValidation: () => UseHttpPrecognitiveProps<TForm, TResponse>;
112
+ setErrors: (errors: FormDataErrors<TForm>) => UseHttpPrecognitiveProps<TForm, TResponse>;
113
+ forgetError: <K extends FormDataKeys<TForm> | NamedInputEvent>(
114
+ field: K,
115
+ ) => UseHttpPrecognitiveProps<TForm, TResponse>;
116
+ }
117
+
118
+ export type UseHttp<TForm extends object, TResponse = unknown> = UseHttpProps<TForm, TResponse>;
119
+ export type UseHttpPrecognitiveProps<TForm extends object, TResponse = unknown> = UseHttpProps<
120
+ TForm,
121
+ TResponse
122
+ > &
123
+ UseHttpValidationProps<TForm, TResponse>;
124
+
125
+ export default function useHttp<TForm extends FormDataType<TForm>, TResponse = unknown>(
126
+ method: Method | (() => Method),
127
+ url: string | (() => string),
128
+ data: TForm | (() => TForm),
129
+ ): UseHttpPrecognitiveProps<TForm, TResponse>;
130
+ export default function useHttp<TForm extends FormDataType<TForm>, TResponse = unknown>(
131
+ urlMethodPair: UrlMethodPair | (() => UrlMethodPair),
132
+ data: TForm | (() => TForm),
133
+ ): UseHttpPrecognitiveProps<TForm, TResponse>;
134
+ export default function useHttp<TForm extends FormDataType<TForm>, TResponse = unknown>(
135
+ rememberKey: string,
136
+ data: TForm | (() => TForm),
137
+ ): UseHttp<TForm, TResponse>;
138
+ export default function useHttp<TForm extends FormDataType<TForm>, TResponse = unknown>(
139
+ data: TForm | (() => TForm),
140
+ ): UseHttp<TForm, TResponse>;
141
+ export default function useHttp<TForm extends FormDataType<TForm>, TResponse = unknown>(): UseHttp<
142
+ TForm,
143
+ TResponse
144
+ >;
145
+ export default function useHttp<TForm extends FormDataType<TForm>, TResponse = unknown>(
146
+ ...args: UseFormArguments<TForm>
147
+ ): UseHttp<TForm, TResponse> | UseHttpPrecognitiveProps<TForm, TResponse> {
148
+ const publicArgs =
149
+ typeof args[args.length - 1] === 'symbol'
150
+ ? (args.slice(0, -1) as UseFormArguments<TForm>)
151
+ : args;
152
+ const { rememberKey, data, precognitionEndpoint } = UseFormUtils.parseUseFormArguments<TForm>(
153
+ ...publicArgs,
154
+ );
155
+
156
+ // Resolve initial data for remember functionality hooks
157
+ const initialDefaults = typeof data === 'function' ? cloneDeep(data()) : cloneDeep(data);
158
+
159
+ const abortController = useRef<AbortController | null>(null, slots.abortController);
160
+ const excludeKeysRef = useRef<FormDataKeys<TForm>[]>([], slots.excludeKeys);
161
+ const pendingOptimisticRef = useRef<((currentData: TForm) => Partial<TForm>) | null>(
162
+ null,
163
+ slots.pendingOptimistic,
164
+ );
165
+ const [response, setResponse] = useState<TResponse | null>(null, slots.response);
166
+
167
+ // For remember functionality, we need custom state hooks
168
+ const useDataState = rememberKey
169
+ ? () => useRemember<TForm>(initialDefaults, `${rememberKey}:data`, excludeKeysRef)
170
+ : undefined;
171
+
172
+ const useErrorsState = rememberKey
173
+ ? () => useRemember<FormDataErrors<TForm>>({} as FormDataErrors<TForm>, `${rememberKey}:errors`)
174
+ : undefined;
175
+
176
+ const {
177
+ form: baseForm,
178
+ setDefaultsState,
179
+ transformRef,
180
+ dataRef,
181
+ precognitionEndpointRef,
182
+ isMounted,
183
+ setProcessing,
184
+ setProgress,
185
+ markAsSuccessful,
186
+ clearErrors,
187
+ setError,
188
+ defaultsCalledInOnSuccessRef,
189
+ resetBeforeSubmit,
190
+ finishProcessing,
191
+ withAllErrors,
192
+ } = useFormState<TForm>({
193
+ data,
194
+ precognitionEndpoint,
195
+ useDataState,
196
+ useErrorsState,
197
+ });
198
+
199
+ const submit = useCallback(
200
+ async (
201
+ method: Method,
202
+ url: string,
203
+ options: UseHttpSubmitOptions<TResponse, TForm>,
204
+ ): Promise<TResponse> => {
205
+ const onBefore = options.onBefore?.();
206
+
207
+ if (onBefore === false) {
208
+ return Promise.reject(new Error('Request cancelled by onBefore'));
209
+ }
210
+
211
+ defaultsCalledInOnSuccessRef.current = false;
212
+
213
+ if (isMounted.current) {
214
+ resetBeforeSubmit();
215
+ }
216
+
217
+ abortController.current = new AbortController();
218
+
219
+ const cancelToken: CancelToken = {
220
+ cancel: () => abortController.current?.abort(),
221
+ };
222
+
223
+ options.onCancelToken?.(cancelToken);
224
+
225
+ options.optimistic = options.optimistic ?? pendingOptimisticRef.current ?? undefined;
226
+ pendingOptimisticRef.current = null;
227
+
228
+ let snapshot: TForm | undefined;
229
+
230
+ if (options.optimistic) {
231
+ snapshot = cloneDeep(dataRef.current);
232
+ const optimisticData = options.optimistic(cloneDeep(snapshot));
233
+
234
+ baseForm.setData((current: TForm) => ({ ...current, ...optimisticData }));
235
+ }
236
+
237
+ if (isMounted.current) {
238
+ setProcessing(true);
239
+ }
240
+
241
+ options.onStart?.();
242
+
243
+ const transformedData = transformRef.current(dataRef.current);
244
+ const useFormData = hasFiles(transformedData as Record<string, FormDataConvertible>);
245
+
246
+ let requestUrl = url;
247
+ let requestData: FormData | string | undefined;
248
+ let contentType: string | undefined;
249
+
250
+ if (method === 'get') {
251
+ const [urlWithParams] = mergeDataIntoQueryString(
252
+ method,
253
+ url,
254
+ transformedData as Record<string, FormDataConvertible>,
255
+ );
256
+ requestUrl = urlWithParams;
257
+ } else {
258
+ if (useFormData) {
259
+ requestData = objectToFormData(transformedData as Record<string, FormDataConvertible>);
260
+ } else {
261
+ requestData = JSON.stringify(transformedData);
262
+ contentType = 'application/json';
263
+ }
264
+ }
265
+
266
+ try {
267
+ const httpResponse = await http.getClient().request({
268
+ method,
269
+ url: requestUrl,
270
+ data: requestData,
271
+ headers: {
272
+ Accept: 'application/json',
273
+ ...(contentType ? { 'Content-Type': contentType } : {}),
274
+ ...options.headers,
275
+ },
276
+ signal: abortController.current.signal,
277
+ onUploadProgress: (event: HttpProgressEvent) => {
278
+ if (isMounted.current) {
279
+ setProgress(event);
280
+ }
281
+
282
+ options.onProgress?.(event);
283
+ },
284
+ });
285
+
286
+ const responseData = (
287
+ httpResponse.data ? JSON.parse(httpResponse.data) : null
288
+ ) as TResponse;
289
+
290
+ if (httpResponse.status >= 200 && httpResponse.status < 300) {
291
+ if (isMounted.current) {
292
+ markAsSuccessful();
293
+ setResponse(responseData);
294
+ }
295
+
296
+ options.onSuccess?.(responseData, httpResponse);
297
+
298
+ if (isMounted.current && !defaultsCalledInOnSuccessRef.current) {
299
+ baseForm.setData((data: TForm) => {
300
+ setDefaultsState(cloneDeep(data));
301
+ return data;
302
+ });
303
+ }
304
+
305
+ return responseData;
306
+ }
307
+
308
+ throw new HttpResponseError(
309
+ `Request failed with status ${httpResponse.status}`,
310
+ httpResponse,
311
+ );
312
+ } catch (error: unknown) {
313
+ if (snapshot && isMounted.current) {
314
+ baseForm.setData(snapshot);
315
+ }
316
+
317
+ if (error instanceof HttpResponseError) {
318
+ if (error.response.status === 422) {
319
+ const responseData = JSON.parse(error.response.data);
320
+ const validationErrors = responseData.errors || {};
321
+ const processedErrors = (
322
+ withAllErrors.enabled()
323
+ ? validationErrors
324
+ : toSimpleValidationErrors(validationErrors)
325
+ ) as FormDataErrors<TForm>;
326
+
327
+ if (isMounted.current) {
328
+ clearErrors();
329
+ setError(processedErrors);
330
+ }
331
+
332
+ options.onError?.(processedErrors as Errors);
333
+
334
+ return undefined as TResponse;
335
+ }
336
+
337
+ options.onHttpException?.(error.response);
338
+
339
+ throw error;
340
+ }
341
+
342
+ if (
343
+ error instanceof HttpCancelledError ||
344
+ (error instanceof Error && error.name === 'AbortError')
345
+ ) {
346
+ options.onCancel?.();
347
+ throw new HttpCancelledError('Request was cancelled', url);
348
+ }
349
+
350
+ options.onNetworkError?.(error instanceof Error ? error : new Error('Unknown error'));
351
+
352
+ throw error;
353
+ } finally {
354
+ if (isMounted.current) {
355
+ finishProcessing();
356
+ }
357
+
358
+ abortController.current = null;
359
+ options.onFinish?.();
360
+ }
361
+ },
362
+ [clearErrors, setError],
363
+ slots.submit,
364
+ );
365
+
366
+ const cancel = useCallback(
367
+ () => {
368
+ if (abortController.current) {
369
+ abortController.current.abort();
370
+ }
371
+ },
372
+ [],
373
+ slots.cancel,
374
+ );
375
+
376
+ const submitWithArgs = useCallback(
377
+ (...args: UseHttpSubmitArguments<TResponse, TForm>): Promise<TResponse> => {
378
+ const parsed = UseFormUtils.parseSubmitArguments(
379
+ args as any,
380
+ precognitionEndpointRef.current,
381
+ );
382
+
383
+ return submit(
384
+ parsed.method,
385
+ parsed.url,
386
+ parsed.options as UseHttpSubmitOptions<TResponse, TForm>,
387
+ );
388
+ },
389
+ [submit],
390
+ slots.submitWithArgs,
391
+ );
392
+
393
+ const submitMethods = useMemo(
394
+ () => ({
395
+ get: async (
396
+ url: string,
397
+ options: UseHttpSubmitOptions<TResponse, TForm> = {},
398
+ ): Promise<TResponse> => submit('get', url, options),
399
+ post: async (
400
+ url: string,
401
+ options: UseHttpSubmitOptions<TResponse, TForm> = {},
402
+ ): Promise<TResponse> => submit('post', url, options),
403
+ put: async (
404
+ url: string,
405
+ options: UseHttpSubmitOptions<TResponse, TForm> = {},
406
+ ): Promise<TResponse> => submit('put', url, options),
407
+ patch: async (
408
+ url: string,
409
+ options: UseHttpSubmitOptions<TResponse, TForm> = {},
410
+ ): Promise<TResponse> => submit('patch', url, options),
411
+ delete: async (
412
+ url: string,
413
+ options: UseHttpSubmitOptions<TResponse, TForm> = {},
414
+ ): Promise<TResponse> => submit('delete', url, options),
415
+ }),
416
+ [submit],
417
+ slots.submitMethods,
418
+ );
419
+
420
+ // Add useHttp-specific methods to the form object
421
+ Object.assign(baseForm, {
422
+ response,
423
+ submit: submitWithArgs,
424
+ ...submitMethods,
425
+ cancel,
426
+ dontRemember: <K extends FormDataKeys<TForm>>(...keys: K[]) => {
427
+ excludeKeysRef.current = keys;
428
+ return form;
429
+ },
430
+
431
+ optimistic: (callback: (currentData: TForm) => Partial<TForm>) => {
432
+ pendingOptimisticRef.current = callback;
433
+ return form;
434
+ },
435
+
436
+ withAllErrors: () => {
437
+ withAllErrors.enable();
438
+ return form;
439
+ },
440
+ });
441
+
442
+ // Cast to the full form type (baseForm now has HTTP methods)
443
+ const form = baseForm as unknown as UseHttpProps<TForm, TResponse>;
444
+
445
+ // Wrap withPrecognition to return the correct type with HTTP methods
446
+ const originalWithPrecognition = baseForm.withPrecognition;
447
+ form.withPrecognition = (
448
+ ...args: UseFormWithPrecognitionArguments
449
+ ): UseHttpPrecognitiveProps<TForm, TResponse> => {
450
+ originalWithPrecognition(...args);
451
+ return form as UseHttpPrecognitiveProps<TForm, TResponse>;
452
+ };
453
+
454
+ return precognitionEndpointRef.current
455
+ ? (form as UseHttpPrecognitiveProps<TForm, TResponse>)
456
+ : form;
457
+ }
package/src/usePage.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { Page, PageProps, SharedPageProps } from '@inertiajs/core';
2
+ import { useContext } from 'octane';
3
+ import PageContext from './PageContext';
4
+
5
+ export default function usePage<TPageProps extends PageProps = PageProps>(): Page<
6
+ TPageProps & SharedPageProps
7
+ > {
8
+ const page = useContext(PageContext);
9
+
10
+ if (!page) {
11
+ throw new Error('usePage must be used within the Inertia component');
12
+ }
13
+
14
+ return page as Page<TPageProps & SharedPageProps>;
15
+ }
package/src/usePoll.ts ADDED
@@ -0,0 +1,49 @@
1
+ import { PollOptions, ReloadOptions, router } from '@inertiajs/core';
2
+ import { useEffect, useRef } from 'octane';
3
+
4
+ const latestSlot = Symbol('Inertia.usePoll.latest');
5
+ const pollSlot = Symbol('Inertia.usePoll.poll');
6
+ const effectSlot = Symbol('Inertia.usePoll.effect');
7
+
8
+ export default function usePoll(
9
+ interval: number,
10
+ requestOptions: ReloadOptions | (() => ReloadOptions) | symbol = {},
11
+ options: PollOptions | symbol = {
12
+ keepAlive: false,
13
+ autoStart: true,
14
+ },
15
+ ) {
16
+ requestOptions = typeof requestOptions === 'symbol' ? {} : requestOptions;
17
+ options =
18
+ typeof options === 'symbol'
19
+ ? {
20
+ keepAlive: false,
21
+ autoStart: true,
22
+ }
23
+ : options;
24
+ const latest = useRef(requestOptions, latestSlot);
25
+ latest.current = requestOptions;
26
+
27
+ const pollRef = useRef<ReturnType<typeof router.poll> | null>(null, pollSlot);
28
+
29
+ useEffect(
30
+ () => {
31
+ pollRef.current = router.poll(
32
+ interval,
33
+ typeof requestOptions === 'function'
34
+ ? () => (latest.current as () => ReloadOptions)()
35
+ : requestOptions,
36
+ { ...options, autoStart: options.autoStart ?? true },
37
+ );
38
+
39
+ return () => pollRef.current?.destroy();
40
+ },
41
+ [],
42
+ effectSlot,
43
+ );
44
+
45
+ return {
46
+ stop: () => pollRef.current?.stop(),
47
+ start: () => pollRef.current?.start(),
48
+ };
49
+ }
@@ -0,0 +1,59 @@
1
+ import { router, VisitOptions } from '@inertiajs/core';
2
+ import { useEffect, useState } from 'octane';
3
+
4
+ const lastUpdatedAtSlot = Symbol('Inertia.usePrefetch.lastUpdatedAt');
5
+ const isPrefetchingSlot = Symbol('Inertia.usePrefetch.isPrefetching');
6
+ const isPrefetchedSlot = Symbol('Inertia.usePrefetch.isPrefetched');
7
+ const effectSlot = Symbol('Inertia.usePrefetch.effect');
8
+
9
+ export default function usePrefetch(options: VisitOptions | symbol = {}): {
10
+ lastUpdatedAt: number | null;
11
+ isPrefetching: boolean;
12
+ isPrefetched: boolean;
13
+ flush: () => void;
14
+ } {
15
+ options = typeof options === 'symbol' ? {} : options;
16
+ const cached =
17
+ typeof window === 'undefined' ? null : router.getCached(window.location.pathname, options);
18
+ const inFlight =
19
+ typeof window === 'undefined' ? null : router.getPrefetching(window.location.pathname, options);
20
+
21
+ const [lastUpdatedAt, setLastUpdatedAt] = useState<number | null>(
22
+ cached?.staleTimestamp || null,
23
+ lastUpdatedAtSlot,
24
+ );
25
+ const [isPrefetching, setIsPrefetching] = useState(inFlight !== null, isPrefetchingSlot);
26
+ const [isPrefetched, setIsPrefetched] = useState(cached !== null, isPrefetchedSlot);
27
+
28
+ useEffect(
29
+ () => {
30
+ const onPrefetchingListener = router.on('prefetching', (e) => {
31
+ if (e.detail.visit.url.pathname === window.location.pathname) {
32
+ setIsPrefetching(true);
33
+ }
34
+ });
35
+
36
+ const onPrefetchedListener = router.on('prefetched', (e) => {
37
+ if (e.detail.visit.url.pathname === window.location.pathname) {
38
+ setIsPrefetching(false);
39
+ setIsPrefetched(true);
40
+ setLastUpdatedAt(e.detail.fetchedAt);
41
+ }
42
+ });
43
+
44
+ return () => {
45
+ onPrefetchedListener();
46
+ onPrefetchingListener();
47
+ };
48
+ },
49
+ [],
50
+ effectSlot,
51
+ );
52
+
53
+ return {
54
+ lastUpdatedAt,
55
+ isPrefetching,
56
+ isPrefetched,
57
+ flush: () => router.flush(window.location.pathname, options),
58
+ };
59
+ }
@@ -0,0 +1,58 @@
1
+ import { router } from '@inertiajs/core';
2
+ import { useEffect, useState } from 'octane';
3
+
4
+ type SetStateAction<State> = State | ((previousState: State) => State);
5
+ type Dispatch<Action> = (action: Action) => void;
6
+ type MutableRefObject<Value> = { current: Value };
7
+
8
+ const stateSlotCache = new Map<string, symbol>();
9
+ const effectSlotCache = new Map<string, symbol>();
10
+
11
+ function slotForRememberKey(
12
+ cache: Map<string, symbol>,
13
+ label: string,
14
+ key: string | undefined,
15
+ ): symbol {
16
+ const cacheKey = key ?? '';
17
+ let slot = cache.get(cacheKey);
18
+ if (!slot) {
19
+ slot = Symbol(`Inertia.useRemember.${label}:${cacheKey}`);
20
+ cache.set(cacheKey, slot);
21
+ }
22
+ return slot;
23
+ }
24
+
25
+ export default function useRemember<State>(
26
+ initialState: State,
27
+ key?: string | symbol,
28
+ excludeKeysRef?: MutableRefObject<string[]> | symbol,
29
+ ): [State, Dispatch<SetStateAction<State>>] {
30
+ key = typeof key === 'symbol' ? undefined : key;
31
+ excludeKeysRef = typeof excludeKeysRef === 'symbol' ? undefined : excludeKeysRef;
32
+ const stateSlot = slotForRememberKey(stateSlotCache, 'state', key);
33
+ const effectSlot = slotForRememberKey(effectSlotCache, 'effect', key);
34
+ const [state, setState] = useState(function () {
35
+ const restored = router.restore(key) as State;
36
+
37
+ return restored !== undefined ? restored : initialState;
38
+ }, stateSlot);
39
+
40
+ useEffect(
41
+ function () {
42
+ const keys = excludeKeysRef?.current;
43
+ if (keys && keys.length > 0 && typeof state === 'object' && state !== null) {
44
+ const filtered = { ...state } as Record<string, unknown>;
45
+ keys.forEach(function (k) {
46
+ delete filtered[k];
47
+ });
48
+ router.remember(filtered, key);
49
+ } else {
50
+ router.remember(state, key);
51
+ }
52
+ },
53
+ [state, key],
54
+ effectSlot,
55
+ );
56
+
57
+ return [state, setState];
58
+ }