react-fresh-key 0.1.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.
@@ -0,0 +1,390 @@
1
+ import { Dispatch, SetStateAction, ReactNode, ReactElement, ComponentType, LazyExoticComponent, MemoExoticComponent, ComponentPropsWithRef, JSX, ComponentProps, ForwardRefExoticComponent } from 'react';
2
+
3
+ /**
4
+ * Watched values used by remount and state-reset APIs.
5
+ *
6
+ * List length and corresponding entries are compared. Entries use `Object.is`,
7
+ * except that plain objects and ordinary arrays compare one level deep using
8
+ * their own enumerable string and symbol properties; arrays also compare
9
+ * length. Equal content preserves state even when object identities differ.
10
+ * Nested objects, functions, Maps, Sets, Dates, and class instances (including
11
+ * array subclasses) compare by identity.
12
+ *
13
+ * Treat the list and its values as immutable. A new list is fine when its
14
+ * entries compare equal; values created anew on every render that cannot
15
+ * compare equal can cause hooks to hit React's re-render limit.
16
+ */
17
+ type DepsList = readonly unknown[];
18
+
19
+ /**
20
+ * Returns a numeric key that changes whenever any value in `deps` changes.
21
+ * Values are compared with `Object.is` — like React's own dependency
22
+ * comparison — except that plain objects and ordinary arrays are compared
23
+ * one level deep. Distinct objects with equal content preserve the key;
24
+ * `[{ id }]` written inline therefore converges instead of looping.
25
+ *
26
+ * A hook cannot remount the component it runs in, so apply the key to an
27
+ * inner subtree:
28
+ *
29
+ * ```tsx
30
+ * function Profile({ userId }: { userId: string }) {
31
+ * const key = useRemountKey([userId])
32
+ * return <ProfileForm key={key} userId={userId} />
33
+ * }
34
+ * ```
35
+ *
36
+ * The key is derived purely during render (the setState-in-render
37
+ * "derived state" pattern), so it is SSR-deterministic and StrictMode-safe.
38
+ * In the render where deps change, the *new* key is returned immediately.
39
+ *
40
+ * **Stability contract** — put primitives, references you already hold
41
+ * (props, state, memoized values), or flat plain objects/arrays in `deps`.
42
+ * A value that is a *new, non-equal* reference on every render — a nested
43
+ * object literal, `new Map()`, `new Date()`, a class instance built inline —
44
+ * can never compare equal and will trip React's re-render limit. Hold such
45
+ * values in state or `useMemo` first.
46
+ *
47
+ * @param deps Watched values, compared using {@link DepsList} rules. Treat the
48
+ * list and its values as immutable; changing their contents in place is not
49
+ * detected reliably. An empty list keeps the initial key.
50
+ * @returns A key local to this hook instance, starting at `0` and increasing
51
+ * when dependencies change. Apply it to a descendant element's `key` prop.
52
+ */
53
+ declare function useRemountKey(deps: DepsList): number;
54
+ /**
55
+ * Predicate flavor of {@link useRemountKey}: tracks an arbitrary `value` and
56
+ * bumps the key when `when(prev, next)` returns `true` for a changed value.
57
+ *
58
+ * `prev` is the last recorded value. Whenever a changed value is observed,
59
+ * the snapshot advances even if the predicate returns `false`. Equal values
60
+ * retain the existing snapshot and skip the predicate. Equality uses
61
+ * `Object.is`, or a one-level shallow comparison for plain objects and ordinary
62
+ * arrays, so a flat inline object (`useRemountKeyWhen({ id, tab }, …)`) converges.
63
+ *
64
+ * ```tsx
65
+ * const key = useRemountKeyWhen(user, (prev, next) => prev.id !== next.id)
66
+ * ```
67
+ *
68
+ * **Stability contract**: the same as {@link useRemountKey} — a value that is
69
+ * a new, non-equal reference on every render (nested literals, Map, Date,
70
+ * class instances built inline) cannot converge; hold it in state or `useMemo`.
71
+ *
72
+ * @param value The immutable value to track. Replace changed objects instead
73
+ * of mutating them in place.
74
+ * @param when A pure render-time predicate comparing the last recorded value
75
+ * with the new value. It is skipped on initial render and for equal values;
76
+ * changing only the predicate does not trigger a comparison.
77
+ * @returns A key starting at `0`, increased when a changed value satisfies
78
+ * `when`. Apply it to a descendant element; it cannot remount this hook's owner.
79
+ */
80
+ declare function useRemountKeyWhen<T>(value: T, when: (prev: T, next: T) => boolean): number;
81
+
82
+ /**
83
+ * Like `useState`, but the state snaps back to `initial` whenever any value
84
+ * in `deps` changes.
85
+ *
86
+ * This is the cheap alternative to a full remount: only this piece of state
87
+ * resets — DOM state (focus, scroll, uncontrolled inputs), refs, and effects
88
+ * are untouched. Reach for this first; remount only when you need everything
89
+ * gone.
90
+ *
91
+ * ```tsx
92
+ * function Comment({ postId }: { postId: string }) {
93
+ * const [draft, setDraft] = useResettableState('', [postId])
94
+ * // draft clears when the user navigates to another post
95
+ * }
96
+ * ```
97
+ *
98
+ * Reset happens purely during render (no effect, no flash of stale state),
99
+ * and the render in which deps change already sees the fresh value — so
100
+ * code after the hook never observes state that belongs to the old deps.
101
+ *
102
+ * `deps` follow {@link DepsList} comparison rules: `Object.is`, with plain
103
+ * objects and ordinary arrays compared one level deep. Dependencies must be
104
+ * able to compare equal across rerenders: nested literals or new class
105
+ * instances created on every render can cause a re-render loop. Keep those
106
+ * values in state or memoize them, and replace changed values rather than
107
+ * mutating them in place.
108
+ *
109
+ * @param initial The value, or a pure lazy initializer, used on mount and on
110
+ * each dependency change. Resets use the current render's `initial`; changing
111
+ * only this argument does not reset existing state. Wrap a function-valued
112
+ * initial state in an initializer, as with React's `useState`.
113
+ * @param deps Immutable watched values that determine when to reset. An empty
114
+ * list disables dependency-driven resets.
115
+ * @returns The current state and a stable setter accepting either the next
116
+ * value or a pure updater function. A dependency change returns fresh state
117
+ * in the same render, without remounting the component.
118
+ */
119
+ declare function useResettableState<T>(initial: T | (() => T), deps: DepsList): [T, Dispatch<SetStateAction<T>>];
120
+
121
+ /** Dependency-list mode for {@link Remount}; do not combine with `watch` or `when`. */
122
+ interface RemountDepsProps {
123
+ /**
124
+ * Remount children when a dependency changes. Values use `Object.is`, except
125
+ * plain objects and ordinary arrays are compared one level deep.
126
+ */
127
+ deps: DepsList;
128
+ watch?: never;
129
+ when?: never;
130
+ /** Children that remount together; the component containing this boundary stays mounted. */
131
+ children?: ReactNode;
132
+ }
133
+ /** Predicate mode for {@link Remount}; provide both `watch` and `when`, without `deps`. */
134
+ interface RemountWhenProps<T> {
135
+ deps?: never;
136
+ /**
137
+ * Value compared across renders before consulting `when`. Uses `Object.is`,
138
+ * except plain objects and ordinary arrays are compared one level deep.
139
+ */
140
+ watch: T;
141
+ /**
142
+ * Pure predicate called during render when `watch` changes. Return `true` to
143
+ * remount children. The tracked snapshot advances even when this returns `false`.
144
+ * React may evaluate the predicate more than once; avoid side effects.
145
+ *
146
+ * @param prev Previous tracked value, not necessarily the value at the last remount.
147
+ * @param next Incoming `watch` value.
148
+ */
149
+ when: (prev: T, next: T) => boolean;
150
+ /** Children that remount together; the component containing this boundary stays mounted. */
151
+ children?: ReactNode;
152
+ }
153
+ /** Choose `deps` or `watch` + `when`, and keep that mode for the boundary's lifetime. */
154
+ type RemountProps<T = unknown> = RemountDepsProps | RemountWhenProps<T>;
155
+ /**
156
+ * Declarative remount boundary. Everything inside is unmounted and mounted
157
+ * fresh (state, refs, effects, DOM) when the watched values change.
158
+ *
159
+ * Used by a parent it is nicer `key` syntax; used *inside* a component around
160
+ * its own subtree, it lets the component own its reset policy:
161
+ *
162
+ * ```tsx
163
+ * <Remount deps={[userId]}>
164
+ * <ProfileForm userId={userId} />
165
+ * </Remount>
166
+ *
167
+ * <Remount watch={user} when={(prev, next) => prev.id !== next.id}>
168
+ * <ProfileForm user={user} />
169
+ * </Remount>
170
+ * ```
171
+ *
172
+ * Pick one mode (`deps` or `watch`+`when`) and stick to it for the lifetime
173
+ * of the element.
174
+ *
175
+ * @param props Watched inputs, reset policy, and children to remount together.
176
+ * @returns A keyed fragment; no wrapper DOM element is added.
177
+ */
178
+ declare function Remount<T = unknown>(props: RemountProps<T>): ReactElement;
179
+
180
+ /**
181
+ * What to watch for remounting:
182
+ *
183
+ * - `['userId', 'mode']` — prop names; remount when any of them changes.
184
+ * - `(props) => props.user.id` — selector; remount when the returned value
185
+ * (or any element, if it returns an array) changes. Same as `{ select }`.
186
+ * - `{ select: (props) => value | deps[] }` — explicit selector form.
187
+ * - `{ when: (prev, next) => boolean }` — predicate over previous/next props;
188
+ * remount when it returns `true`.
189
+ *
190
+ * Predicates are always explicit (`{ when }`). A bare function is always a
191
+ * selector — there is no arity-based guessing.
192
+ *
193
+ * Selected values use `Object.is`, except plain objects and ordinary arrays
194
+ * are compared one level deep. Selectors and predicates run during render:
195
+ * keep them pure, because React may evaluate them more than once.
196
+ *
197
+ * Declared `defaultProps` are applied before callbacks, including through
198
+ * `memo`. Inner defaults behind `lazy` and JavaScript parameter defaults
199
+ * cannot be resolved by the wrapper; handle any optional props accordingly.
200
+ */
201
+ type WatchSpec<P extends object> = ReadonlyArray<keyof P> | ((props: P) => unknown) | {
202
+ /**
203
+ * Pure selector of the incoming props. Return one watched value or an
204
+ * array of dependencies; remount when any selected dependency changes.
205
+ *
206
+ * @param props Incoming props with available `defaultProps` applied.
207
+ */
208
+ select: (props: P) => unknown;
209
+ } | {
210
+ /**
211
+ * Pure predicate called when props differ shallowly (`Object.is` per
212
+ * prop). Return `true` to remount. The snapshot advances on a detected
213
+ * change even when this returns `false`.
214
+ *
215
+ * @param prev Previous tracked props, not necessarily those at the last remount.
216
+ * @param next Incoming props. Available `defaultProps` are applied to both arguments.
217
+ */
218
+ when: (prev: P, next: P) => boolean;
219
+ };
220
+ /**
221
+ * `true` when `C` is a `lazy` component, possibly wrapped in any number of
222
+ * `memo` layers. Mirrors the runtime `unwrapMemo`, which also looks through
223
+ * every memo layer and then stops at a lazy object it cannot open.
224
+ */
225
+ type ContainsLazy<C> = C extends LazyExoticComponent<infer _Component> ? true : C extends MemoExoticComponent<infer T> ? ContainsLazy<T> : false;
226
+ /**
227
+ * The props the wrapper accepts: whatever React's JSX checker would accept
228
+ * for the original component (so props covered by `defaultProps` stay
229
+ * optional — through `memo` too), with the ref type inferred from the
230
+ * original.
231
+ *
232
+ * `lazy` is the one exception, at any depth of `memo` nesting: its inner
233
+ * component's defaults are unknowable until the module loads, so the wrapper
234
+ * requires those props as declared. That keeps the types truthful about what
235
+ * selectors and predicates receive.
236
+ */
237
+ type RemountedProps<C extends ComponentType<any>> = ContainsLazy<C> extends true ? ComponentPropsWithRef<C> : JSX.LibraryManagedAttributes<C, ComponentPropsWithRef<C>>;
238
+ /**
239
+ * Wrap a component with a remount policy declared *at the definition site*,
240
+ * so call sites don't need to know which props warrant a fresh mount:
241
+ *
242
+ * ```tsx
243
+ * // Profile.tsx
244
+ * function Profile({ userId }: ProfileProps) { ... }
245
+ * export default withRemount(Profile, ['userId'])
246
+ *
247
+ * // Anywhere else — no key juggling required:
248
+ * <Profile userId={id} />
249
+ * ```
250
+ *
251
+ * Parents can still override identity the normal way with their own `key`.
252
+ * Refs are forwarded, and the ref type is inferred from the wrapped
253
+ * component (a `forwardRef<HTMLInputElement, …>` component yields a wrapper
254
+ * that accepts `Ref<HTMLInputElement>` and nothing else). Props covered by
255
+ * `defaultProps` stay optional, except behind `lazy`; see {@link RemountedProps}.
256
+ *
257
+ * Call this once outside rendering. Creating a wrapper during render creates
258
+ * a new component type each time and discards its state. Selectors and
259
+ * predicates must be pure; they run during render and may be evaluated again.
260
+ *
261
+ * @param Component Component whose entire subtree remounts when the rule matches.
262
+ * @param watch Prop names, a selector, `{ select }`, or `{ when }`; see {@link WatchSpec}.
263
+ * @returns A component accepting the original props and supported ref type.
264
+ * @throws If the watch specification is invalid; pass predicates as `{ when }`.
265
+ */
266
+ declare function withRemount<C extends ComponentType<any>>(Component: C, watch: WatchSpec<ComponentProps<C>>): ForwardRefExoticComponent<RemountedProps<C>>;
267
+
268
+ /** Props for the shared, zero-argument {@link ResetBoundary}. */
269
+ interface ResetBoundaryProps {
270
+ /** Subtree to remount on reset. Its local state, refs, effects, and DOM are recreated. */
271
+ children?: ReactNode;
272
+ /**
273
+ * Observes each reset request synchronously, with no arguments (for example, analytics).
274
+ * This is not a mount lifecycle callback; initialize focus in the remounted UI itself.
275
+ * Use {@link createResetBoundary} to receive typed request metadata.
276
+ */
277
+ onReset?: () => void;
278
+ }
279
+ /**
280
+ * A typed reset request. Check `key` to narrow `payload` to that action's type.
281
+ * `Events` maps registration keys to the payloads declared by the component author.
282
+ */
283
+ type ResetEvent<Events extends object> = {
284
+ [Key in keyof Events]-?: {
285
+ /** Registration key selected by the descendant's hook. */
286
+ key: Key;
287
+ /** Original argument passed to reset, or undefined for a call without a payload. */
288
+ payload: Events[Key];
289
+ };
290
+ }[keyof Events];
291
+ /** Props for a boundary returned by {@link createResetBoundary}. */
292
+ interface TypedResetBoundaryProps<Events extends object> {
293
+ /** Subtree to remount when a descendant requests a reset through this factory's hook. */
294
+ children?: ReactNode;
295
+ /**
296
+ * Observes each reset request synchronously. Does not signal that the new subtree has mounted.
297
+ * @param event - Registration key and unchanged payload. Narrow on `event.key` before reading action-specific fields.
298
+ */
299
+ onReset?: (event: ResetEvent<Events>) => void;
300
+ }
301
+ type ResetArgs<Payload> = undefined extends Payload ? [payload?: Payload] : [payload: Payload];
302
+ type RegistrationPayload<Events extends object, Key extends keyof Events> = {
303
+ [K in Key]-?: (payload: Events[K]) => void;
304
+ }[Key] extends (payload: infer Payload) => void ? Payload : never;
305
+ type RegisteredReset<Events extends object, Key extends keyof Events> = {
306
+ /**
307
+ * Requests a remount from the matching boundary and reports this registration's key and payload.
308
+ * Supply a payload unless the action's declared type accepts undefined.
309
+ * @param payload - Data for the selected action. May be omitted only if its type accepts undefined.
310
+ */
311
+ (...args: ResetArgs<RegistrationPayload<Events, Key>>): void;
312
+ };
313
+ /**
314
+ * Imperative reset from below: any descendant can call the function returned
315
+ * by {@link useResetBoundary} to unmount and freshly mount everything inside
316
+ * the nearest boundary — state, refs, effects, DOM.
317
+ *
318
+ * Handy for "start over" buttons, clearing a wizard after submit, or error
319
+ * recovery, without the parent knowing anything about it:
320
+ *
321
+ * ```tsx
322
+ * <ResetBoundary>
323
+ * <CheckoutWizard />
324
+ * </ResetBoundary>
325
+ *
326
+ * // deep inside CheckoutWizard:
327
+ * const reset = useResetBoundary();
328
+ * <button onClick={reset}>Start over</button>
329
+ * ```
330
+ *
331
+ * Boundaries nest; `useResetBoundary` targets the nearest one.
332
+ * @param props - Subtree and optional synchronous reset observer.
333
+ * @returns A resettable subtree accessible through {@link useResetBoundary}.
334
+ */
335
+ declare function ResetBoundary({ children, onReset }: ResetBoundaryProps): ReactElement;
336
+ /**
337
+ * Returns the nearest {@link ResetBoundary}'s reset function.
338
+ * @returns A stable, zero-argument function that remounts the boundary's entire subtree.
339
+ * @throws If no shared {@link ResetBoundary} is above the caller. Factory boundaries use their own hook.
340
+ */
341
+ declare function useResetBoundary(): () => void;
342
+ /**
343
+ * Creates a boundary and registration hook that share a typed reset contract.
344
+ * Create it once at module scope; aliases and destructured exports are safe.
345
+ *
346
+ * ```tsx
347
+ * const checkoutReset = createResetBoundary<{
348
+ * restart: { step: number }
349
+ * completed: { orderId: string }
350
+ * dismissed: void
351
+ * }>()
352
+ * export const ClientResetBoundary = checkoutReset.ResetBoundary
353
+ * export const useResetClientRegistration = checkoutReset.useResetBoundary
354
+ *
355
+ * // Inside a descendant of ClientResetBoundary:
356
+ * const reset = useResetClientRegistration('restart')
357
+ * // In an event handler:
358
+ * reset({ step: 2 })
359
+ * ```
360
+ *
361
+ * The hook targets the nearest boundary created by this factory call. It
362
+ * captures a registration key without subscriptions or registration effects.
363
+ * The reset function is stable while that key and provider remain unchanged.
364
+ * Payloads are forwarded unchanged to `onReset({ key, payload })`.
365
+ * @returns A matching boundary and hook. Export either under an application-specific name without binding.
366
+ */
367
+ declare function createResetBoundary<Events extends object>(): {
368
+ /** Boundary providing this factory's reset scope and typed request observer. */
369
+ ResetBoundary: {
370
+ /**
371
+ * Provides this factory's reset scope. Nested instances use the nearest matching provider.
372
+ * @param props - Subtree to remount and optional `onReset` observer of typed requests.
373
+ * @returns A resettable subtree accessible through this factory's hook.
374
+ */
375
+ (props: TypedResetBoundaryProps<Events>): ReactElement;
376
+ };
377
+ /** Registration hook for the nearest boundary from this factory; safe to alias or destructure. */
378
+ useResetBoundary: {
379
+ /**
380
+ * Selects a reset action from the nearest boundary created by this factory.
381
+ * Safe to alias, for example `export const useResetClientRegistration = scope.useResetBoundary`.
382
+ * @param key - An action key from the shared event map; determines the reset payload's type.
383
+ * @returns A reset function stable while key and provider are unchanged. Pass the action's payload when calling it; use void for payload-free actions.
384
+ * @throws If no boundary from this factory call is above the caller.
385
+ */
386
+ <Key extends keyof Events>(key: Key): RegisteredReset<Events, Key>;
387
+ };
388
+ };
389
+
390
+ export { type DepsList, Remount, type RemountDepsProps, type RemountProps, type RemountWhenProps, type RemountedProps, ResetBoundary, type ResetBoundaryProps, type ResetEvent, type TypedResetBoundaryProps, type WatchSpec, createResetBoundary, useRemountKey, useRemountKeyWhen, useResetBoundary, useResettableState, withRemount };
package/dist/index.js ADDED
@@ -0,0 +1,286 @@
1
+ // scripts/react-jsx-runtime.mjs
2
+ import { createElement, Fragment } from "react";
3
+
4
+ // src/useRemountKey.ts
5
+ import { useState } from "react";
6
+
7
+ // src/utils.ts
8
+ function isPlainObject(value) {
9
+ if (value === null || typeof value !== "object") return false;
10
+ const proto = Object.getPrototypeOf(value);
11
+ return proto === Object.prototype || proto === null;
12
+ }
13
+ function isOrdinaryArray(value) {
14
+ return Array.isArray(value) && Object.getPrototypeOf(value) === Array.prototype;
15
+ }
16
+ function ownEnumerableKeys(value) {
17
+ const keys = Object.keys(value);
18
+ for (const sym of Object.getOwnPropertySymbols(value)) {
19
+ if (Object.prototype.propertyIsEnumerable.call(value, sym)) keys.push(sym);
20
+ }
21
+ return keys;
22
+ }
23
+ function shallowEqual(a, b) {
24
+ if (Object.is(a, b)) return true;
25
+ const keysA = ownEnumerableKeys(a);
26
+ const keysB = ownEnumerableKeys(b);
27
+ if (keysA.length !== keysB.length) return false;
28
+ for (const key of keysA) {
29
+ if (!Object.prototype.propertyIsEnumerable.call(b, key) || !Object.is(a[key], b[key])) {
30
+ return false;
31
+ }
32
+ }
33
+ return true;
34
+ }
35
+ function isEqualDep(a, b) {
36
+ if (Object.is(a, b)) return true;
37
+ if (isOrdinaryArray(a) && isOrdinaryArray(b)) return a.length === b.length && shallowEqual(a, b);
38
+ if (isPlainObject(a) && isPlainObject(b)) return shallowEqual(a, b);
39
+ return false;
40
+ }
41
+ function areDepsEqual(a, b) {
42
+ if (a === b) return true;
43
+ if (a.length !== b.length) return false;
44
+ for (let i = 0; i < a.length; i++) {
45
+ if (!isEqualDep(a[i], b[i])) return false;
46
+ }
47
+ return true;
48
+ }
49
+ function toDeps(value) {
50
+ return Array.isArray(value) ? value : [value];
51
+ }
52
+
53
+ // src/useRemountKey.ts
54
+ function useRemountKey(deps) {
55
+ const [state, setState] = useState({
56
+ deps,
57
+ key: 0
58
+ });
59
+ if (!areDepsEqual(state.deps, deps)) {
60
+ const key = state.key + 1;
61
+ setState({ deps, key });
62
+ return key;
63
+ }
64
+ return state.key;
65
+ }
66
+ function useRemountKeyWhen(value, when) {
67
+ const [state, setState] = useState({
68
+ value,
69
+ key: 0
70
+ });
71
+ if (!isEqualDep(state.value, value)) {
72
+ const key = when(state.value, value) ? state.key + 1 : state.key;
73
+ setState({ value, key });
74
+ return key;
75
+ }
76
+ return state.key;
77
+ }
78
+
79
+ // src/useResettableState.ts
80
+ import { useCallback, useState as useState2 } from "react";
81
+ function resolveInitial(initial) {
82
+ return typeof initial === "function" ? initial() : initial;
83
+ }
84
+ function useResettableState(initial, deps) {
85
+ const [state, setState] = useState2(() => ({
86
+ deps,
87
+ value: resolveInitial(initial)
88
+ }));
89
+ const setValue = useCallback((action) => {
90
+ setState((s) => ({
91
+ deps: s.deps,
92
+ value: typeof action === "function" ? action(s.value) : action
93
+ }));
94
+ }, []);
95
+ if (!areDepsEqual(state.deps, deps)) {
96
+ const value = resolveInitial(initial);
97
+ setState({ deps, value });
98
+ return [value, setValue];
99
+ }
100
+ return [state.value, setValue];
101
+ }
102
+
103
+ // src/Remount.tsx
104
+ import { Fragment as Fragment2 } from "react";
105
+ var alwaysFalse = () => false;
106
+ function Remount(props) {
107
+ var _a;
108
+ const usingWhen = typeof props.when === "function";
109
+ const depsKey = useRemountKey(usingWhen ? [] : (_a = props.deps) != null ? _a : []);
110
+ const whenKey = useRemountKeyWhen(
111
+ usingWhen ? props.watch : void 0,
112
+ usingWhen ? props.when : alwaysFalse
113
+ );
114
+ const key = usingWhen ? whenKey : depsKey;
115
+ return /* @__PURE__ */ createElement(Fragment2, { key: `remount-${key}` }, props.children);
116
+ }
117
+
118
+ // src/withRemount.tsx
119
+ import {
120
+ forwardRef,
121
+ useState as useState3
122
+ } from "react";
123
+ function normalizeWatch(watch) {
124
+ if (Array.isArray(watch)) {
125
+ const names = watch;
126
+ return { mode: "select", select: (props) => names.map((n) => props[n]) };
127
+ }
128
+ if (typeof watch === "function") {
129
+ if (watch.length >= 2) {
130
+ throw new Error(
131
+ "react-fresh-key: a bare function passed to withRemount is a selector `(props) => value`. For a predicate `(prev, next) => boolean`, pass `{ when: fn }` instead."
132
+ );
133
+ }
134
+ const select = watch;
135
+ return { mode: "select", select: (props) => toDeps(select(props)) };
136
+ }
137
+ if (watch && typeof watch === "object") {
138
+ if ("select" in watch && typeof watch.select === "function") {
139
+ const select = watch.select;
140
+ return { mode: "select", select: (props) => toDeps(select(props)) };
141
+ }
142
+ if ("when" in watch && typeof watch.when === "function") {
143
+ return { mode: "when", when: watch.when };
144
+ }
145
+ }
146
+ throw new Error(
147
+ "react-fresh-key: invalid watch spec. Expected a prop-name array, a selector function, { select }, or { when }."
148
+ );
149
+ }
150
+ var REACT_MEMO_TYPE = /* @__PURE__ */ Symbol.for("react.memo");
151
+ function unwrapMemo(Component) {
152
+ let current = Component;
153
+ while (current !== null && typeof current === "object" && current.$$typeof === REACT_MEMO_TYPE) {
154
+ current = current.type;
155
+ }
156
+ return current;
157
+ }
158
+ function makeDefaultsResolver(Component) {
159
+ var _a;
160
+ const defaults = (_a = unwrapMemo(Component)) == null ? void 0 : _a.defaultProps;
161
+ if (!defaults) return (props) => props;
162
+ const keys = Object.keys(defaults);
163
+ return (props) => {
164
+ let resolved = null;
165
+ for (const key of keys) {
166
+ if (props[key] === void 0) {
167
+ resolved != null ? resolved : resolved = { ...props };
168
+ resolved[key] = defaults[key];
169
+ }
170
+ }
171
+ return resolved != null ? resolved : props;
172
+ };
173
+ }
174
+ function useWatchKey(props, spec, resolve) {
175
+ const [state, setState] = useState3(() => ({
176
+ props,
177
+ deps: spec.mode === "select" ? spec.select(resolve(props)) : null,
178
+ key: 0
179
+ }));
180
+ if (state.props !== props) {
181
+ if (spec.mode === "select") {
182
+ const deps = spec.select(resolve(props));
183
+ if (!areDepsEqual(state.deps, deps)) {
184
+ const key = state.key + 1;
185
+ setState({ props, deps, key });
186
+ return key;
187
+ }
188
+ } else if (!shallowEqual(state.props, props)) {
189
+ const key = spec.when(resolve(state.props), resolve(props)) ? state.key + 1 : state.key;
190
+ setState({ props, deps: null, key });
191
+ return key;
192
+ }
193
+ }
194
+ return state.key;
195
+ }
196
+ function withRemount(Component, watch) {
197
+ var _a, _b;
198
+ const spec = normalizeWatch(watch);
199
+ const resolve = makeDefaultsResolver(Component);
200
+ const Inner = Component;
201
+ const Wrapped = forwardRef((props, ref) => {
202
+ const key = useWatchKey(props, spec, resolve);
203
+ return /* @__PURE__ */ createElement(Inner, { key, ref, ...props });
204
+ });
205
+ const name = (_b = (_a = Component.displayName) != null ? _a : Component.name) != null ? _b : "Component";
206
+ Wrapped.displayName = `withRemount(${name})`;
207
+ return Wrapped;
208
+ }
209
+
210
+ // src/ResetBoundary.tsx
211
+ import {
212
+ Fragment as Fragment3,
213
+ createContext,
214
+ useCallback as useCallback2,
215
+ useContext,
216
+ useEffect,
217
+ useRef,
218
+ useState as useState4
219
+ } from "react";
220
+ var ResetContext = createContext(null);
221
+ function useResetRequest(onReset) {
222
+ const [generation, setGeneration] = useState4(0);
223
+ const onResetRef = useRef(onReset);
224
+ useEffect(() => {
225
+ onResetRef.current = onReset;
226
+ });
227
+ const request = useCallback2((...args) => {
228
+ var _a;
229
+ setGeneration((g) => g + 1);
230
+ (_a = onResetRef.current) == null ? void 0 : _a.call(onResetRef, ...args);
231
+ }, []);
232
+ return { generation, request };
233
+ }
234
+ function ResetBoundary({ children, onReset }) {
235
+ const { generation, request } = useResetRequest(onReset);
236
+ const reset = useCallback2(() => request(), [request]);
237
+ return /* @__PURE__ */ createElement(ResetContext.Provider, { value: reset }, /* @__PURE__ */ createElement(Fragment3, { key: `reset-${generation}` }, children));
238
+ }
239
+ function useResetBoundary() {
240
+ const reset = useContext(ResetContext);
241
+ if (reset === null) {
242
+ throw new Error("react-fresh-key: useResetBoundary must be used inside a <ResetBoundary>.");
243
+ }
244
+ return reset;
245
+ }
246
+ function createResetBoundary() {
247
+ const Context = createContext(null);
248
+ function TypedResetBoundary({
249
+ children,
250
+ onReset
251
+ }) {
252
+ const { generation, request } = useResetRequest(onReset);
253
+ return /* @__PURE__ */ createElement(Context.Provider, { value: request }, /* @__PURE__ */ createElement(Fragment3, { key: `reset-${generation}` }, children));
254
+ }
255
+ function useRegisteredReset(key) {
256
+ const request = useContext(Context);
257
+ const reset = useCallback2(
258
+ /**
259
+ * Requests a remount and forwards this registration's key and payload to `onReset`.
260
+ * @param payload - Request metadata forwarded unchanged to the matching boundary.
261
+ */
262
+ (payload) => {
263
+ request == null ? void 0 : request({ key, payload });
264
+ },
265
+ [request, key]
266
+ );
267
+ if (request === null) {
268
+ throw new Error(
269
+ "react-fresh-key: useResetBoundary must be used inside a boundary from the same createResetBoundary() call."
270
+ );
271
+ }
272
+ return reset;
273
+ }
274
+ return { ResetBoundary: TypedResetBoundary, useResetBoundary: useRegisteredReset };
275
+ }
276
+ export {
277
+ Remount,
278
+ ResetBoundary,
279
+ createResetBoundary,
280
+ useRemountKey,
281
+ useRemountKeyWhen,
282
+ useResetBoundary,
283
+ useResettableState,
284
+ withRemount
285
+ };
286
+ //# sourceMappingURL=index.js.map