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,95 @@
1
+ // react-fresh-key v0.1.0 — vendored copy (JavaScript).
2
+ // This file is yours now: edit it freely. Docs: https://www.npmjs.com/package/react-fresh-key
3
+ /*!
4
+ MIT License
5
+
6
+ Copyright (c) 2026 Rafael Buzatto de Campos
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ SOFTWARE.
25
+ */
26
+ function isPlainObject(value) {
27
+ if (value === null || typeof value !== 'object')
28
+ return false;
29
+ const proto = Object.getPrototypeOf(value);
30
+ return proto === Object.prototype || proto === null;
31
+ }
32
+ /** An `Array` proper — not a subclass, which is treated as a class instance. */
33
+ function isOrdinaryArray(value) {
34
+ return Array.isArray(value) && Object.getPrototypeOf(value) === Array.prototype;
35
+ }
36
+ /** Own enumerable keys, including symbol keys (which `Object.keys` omits). */
37
+ function ownEnumerableKeys(value) {
38
+ const keys = Object.keys(value);
39
+ for (const sym of Object.getOwnPropertySymbols(value)) {
40
+ if (Object.prototype.propertyIsEnumerable.call(value, sym))
41
+ keys.push(sym);
42
+ }
43
+ return keys;
44
+ }
45
+ /** Shallow-compare two objects: same own enumerable keys, `Object.is` per value. */
46
+ export function shallowEqual(a, b) {
47
+ if (Object.is(a, b))
48
+ return true;
49
+ const keysA = ownEnumerableKeys(a);
50
+ const keysB = ownEnumerableKeys(b);
51
+ if (keysA.length !== keysB.length)
52
+ return false;
53
+ for (const key of keysA) {
54
+ if (!Object.prototype.propertyIsEnumerable.call(b, key) ||
55
+ !Object.is(a[key], b[key])) {
56
+ return false;
57
+ }
58
+ }
59
+ return true;
60
+ }
61
+ /**
62
+ * Equality for a single watched value: `Object.is`, except that two *plain*
63
+ * objects (`{}` / `Object.create(null)`) or two ordinary arrays are compared
64
+ * one level deep — every own enumerable property, symbol keys included, plus
65
+ * array length. This lets the common inline-literal shapes converge —
66
+ * `useRemountKey([{ id }])`, `useRemountKeyWhen({ id, tab }, …)` — without
67
+ * hiding real changes. Anything else (class instances, including `Array`
68
+ * subclasses; Map/Set; Date; functions; nested objects' inner values) is
69
+ * compared by identity.
70
+ */
71
+ export function isEqualDep(a, b) {
72
+ if (Object.is(a, b))
73
+ return true;
74
+ if (isOrdinaryArray(a) && isOrdinaryArray(b))
75
+ return a.length === b.length && shallowEqual(a, b);
76
+ if (isPlainObject(a) && isPlainObject(b))
77
+ return shallowEqual(a, b);
78
+ return false;
79
+ }
80
+ /** Compare two dependency lists element-wise with {@link isEqualDep}. */
81
+ export function areDepsEqual(a, b) {
82
+ if (a === b)
83
+ return true;
84
+ if (a.length !== b.length)
85
+ return false;
86
+ for (let i = 0; i < a.length; i++) {
87
+ if (!isEqualDep(a[i], b[i]))
88
+ return false;
89
+ }
90
+ return true;
91
+ }
92
+ /** Normalize a selector result into a deps list. */
93
+ export function toDeps(value) {
94
+ return Array.isArray(value) ? value : [value];
95
+ }
@@ -0,0 +1,162 @@
1
+ // react-fresh-key v0.1.0 — vendored copy (JavaScript).
2
+ // This file is yours now: edit it freely. Docs: https://www.npmjs.com/package/react-fresh-key
3
+ /*!
4
+ MIT License
5
+
6
+ Copyright (c) 2026 Rafael Buzatto de Campos
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ SOFTWARE.
25
+ */
26
+ import { forwardRef, useState, } from 'react';
27
+ import { areDepsEqual, shallowEqual, toDeps } from './utils';
28
+ function normalizeWatch(watch) {
29
+ if (Array.isArray(watch)) {
30
+ const names = watch;
31
+ return { mode: 'select', select: (props) => names.map((n) => props[n]) };
32
+ }
33
+ if (typeof watch === 'function') {
34
+ if (watch.length >= 2) {
35
+ throw new Error('react-fresh-key: a bare function passed to withRemount is a selector `(props) => value`. ' +
36
+ 'For a predicate `(prev, next) => boolean`, pass `{ when: fn }` instead.');
37
+ }
38
+ const select = watch;
39
+ return { mode: 'select', select: (props) => toDeps(select(props)) };
40
+ }
41
+ if (watch && typeof watch === 'object') {
42
+ if ('select' in watch && typeof watch.select === 'function') {
43
+ const select = watch.select;
44
+ return { mode: 'select', select: (props) => toDeps(select(props)) };
45
+ }
46
+ if ('when' in watch && typeof watch.when === 'function') {
47
+ return { mode: 'when', when: watch.when };
48
+ }
49
+ }
50
+ throw new Error('react-fresh-key: invalid watch spec. Expected a prop-name array, a selector function, { select }, or { when }.');
51
+ }
52
+ const REACT_MEMO_TYPE = Symbol.for('react.memo');
53
+ /**
54
+ * `memo(C)` is an object `{ $$typeof, type: C }`; React applies the *inner*
55
+ * component's `defaultProps` when it renders, and `JSX.LibraryManagedAttributes`
56
+ * unwraps `memo` the same way. Mirror both so types and runtime agree.
57
+ * (`lazy` cannot be unwrapped before it loads; see {@link RemountedProps}.)
58
+ */
59
+ function unwrapMemo(Component) {
60
+ let current = Component;
61
+ while (current !== null &&
62
+ typeof current === 'object' &&
63
+ current.$$typeof === REACT_MEMO_TYPE) {
64
+ current = current.type;
65
+ }
66
+ return current;
67
+ }
68
+ /**
69
+ * Apply `defaultProps` the way React does (a default fills in only when the
70
+ * prop is `undefined`). Returns the same object when nothing needs filling.
71
+ */
72
+ function makeDefaultsResolver(Component) {
73
+ const defaults = unwrapMemo(Component)
74
+ ?.defaultProps;
75
+ if (!defaults)
76
+ return (props) => props;
77
+ const keys = Object.keys(defaults);
78
+ return (props) => {
79
+ let resolved = null;
80
+ for (const key of keys) {
81
+ if (props[key] === undefined) {
82
+ resolved ?? (resolved = { ...props });
83
+ resolved[key] = defaults[key];
84
+ }
85
+ }
86
+ return (resolved ?? props);
87
+ };
88
+ }
89
+ /**
90
+ * Single-useState implementation so the hook order is identical for both
91
+ * modes, with the raw `props` object as the convergence anchor: after a
92
+ * render-phase setState, React re-invokes the component with the *same*
93
+ * props object, so the comparison branch is skipped on that second pass.
94
+ * That makes the derivation converge even when a selector returns a fresh
95
+ * object on every call (it remounts once per props change instead of looping).
96
+ */
97
+ function useWatchKey(props, spec, resolve) {
98
+ const [state, setState] = useState(() => ({
99
+ props,
100
+ deps: spec.mode === 'select' ? spec.select(resolve(props)) : null,
101
+ key: 0,
102
+ }));
103
+ if (state.props !== props) {
104
+ if (spec.mode === 'select') {
105
+ const deps = spec.select(resolve(props));
106
+ if (!areDepsEqual(state.deps, deps)) {
107
+ const key = state.key + 1;
108
+ setState({ props, deps, key });
109
+ return key;
110
+ }
111
+ // Unchanged: leave the snapshot alone (no extra render pass).
112
+ }
113
+ else if (!shallowEqual(state.props, props)) {
114
+ // Only consult the predicate when props actually changed (shallow),
115
+ // so parent re-renders with identical props stay single-pass.
116
+ const key = spec.when(resolve(state.props), resolve(props)) ? state.key + 1 : state.key;
117
+ setState({ props, deps: null, key });
118
+ return key;
119
+ }
120
+ }
121
+ return state.key;
122
+ }
123
+ /**
124
+ * Wrap a component with a remount policy declared *at the definition site*,
125
+ * so call sites don't need to know which props warrant a fresh mount:
126
+ *
127
+ * ```tsx
128
+ * // Profile.tsx
129
+ * function Profile({ userId }: ProfileProps) { ... }
130
+ * export default withRemount(Profile, ['userId'])
131
+ *
132
+ * // Anywhere else — no key juggling required:
133
+ * <Profile userId={id} />
134
+ * ```
135
+ *
136
+ * Parents can still override identity the normal way with their own `key`.
137
+ * Refs are forwarded, and the ref type is inferred from the wrapped
138
+ * component (a `forwardRef<HTMLInputElement, …>` component yields a wrapper
139
+ * that accepts `Ref<HTMLInputElement>` and nothing else). Props covered by
140
+ * `defaultProps` stay optional, except behind `lazy`; see {@link RemountedProps}.
141
+ *
142
+ * Call this once outside rendering. Creating a wrapper during render creates
143
+ * a new component type each time and discards its state. Selectors and
144
+ * predicates must be pure; they run during render and may be evaluated again.
145
+ *
146
+ * @param Component Component whose entire subtree remounts when the rule matches.
147
+ * @param watch Prop names, a selector, `{ select }`, or `{ when }`; see {@link WatchSpec}.
148
+ * @returns A component accepting the original props and supported ref type.
149
+ * @throws If the watch specification is invalid; pass predicates as `{ when }`.
150
+ */
151
+ export function withRemount(Component, watch) {
152
+ const spec = normalizeWatch(watch);
153
+ const resolve = makeDefaultsResolver(Component);
154
+ const Inner = Component;
155
+ const Wrapped = forwardRef((props, ref) => {
156
+ const key = useWatchKey(props, spec, resolve);
157
+ return <Inner key={key} ref={ref} {...props}/>;
158
+ });
159
+ const name = Component.displayName ?? Component.name ?? 'Component';
160
+ Wrapped.displayName = `withRemount(${name})`;
161
+ return Wrapped;
162
+ }
@@ -0,0 +1,103 @@
1
+ // react-fresh-key v0.1.0 — vendored copy (TypeScript).
2
+ // This file is yours now: edit it freely. Docs: https://www.npmjs.com/package/react-fresh-key
3
+ /*!
4
+ MIT License
5
+
6
+ Copyright (c) 2026 Rafael Buzatto de Campos
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ SOFTWARE.
25
+ */
26
+ import { Fragment, type ReactElement, type ReactNode } from 'react'
27
+ import { useRemountKey, useRemountKeyWhen } from './useRemountKey'
28
+ import type { DepsList } from './utils'
29
+
30
+ /** Dependency-list mode for {@link Remount}; do not combine with `watch` or `when`. */
31
+ export interface RemountDepsProps {
32
+ /**
33
+ * Remount children when a dependency changes. Values use `Object.is`, except
34
+ * plain objects and ordinary arrays are compared one level deep.
35
+ */
36
+ deps: DepsList
37
+ watch?: never
38
+ when?: never
39
+ /** Children that remount together; the component containing this boundary stays mounted. */
40
+ children?: ReactNode
41
+ }
42
+
43
+ /** Predicate mode for {@link Remount}; provide both `watch` and `when`, without `deps`. */
44
+ export interface RemountWhenProps<T> {
45
+ deps?: never
46
+ /**
47
+ * Value compared across renders before consulting `when`. Uses `Object.is`,
48
+ * except plain objects and ordinary arrays are compared one level deep.
49
+ */
50
+ watch: T
51
+ /**
52
+ * Pure predicate called during render when `watch` changes. Return `true` to
53
+ * remount children. The tracked snapshot advances even when this returns `false`.
54
+ * React may evaluate the predicate more than once; avoid side effects.
55
+ *
56
+ * @param prev Previous tracked value, not necessarily the value at the last remount.
57
+ * @param next Incoming `watch` value.
58
+ */
59
+ when: (prev: T, next: T) => boolean
60
+ /** Children that remount together; the component containing this boundary stays mounted. */
61
+ children?: ReactNode
62
+ }
63
+
64
+ /** Choose `deps` or `watch` + `when`, and keep that mode for the boundary's lifetime. */
65
+ export type RemountProps<T = unknown> = RemountDepsProps | RemountWhenProps<T>
66
+
67
+ const alwaysFalse = () => false
68
+
69
+ /**
70
+ * Declarative remount boundary. Everything inside is unmounted and mounted
71
+ * fresh (state, refs, effects, DOM) when the watched values change.
72
+ *
73
+ * Used by a parent it is nicer `key` syntax; used *inside* a component around
74
+ * its own subtree, it lets the component own its reset policy:
75
+ *
76
+ * ```tsx
77
+ * <Remount deps={[userId]}>
78
+ * <ProfileForm userId={userId} />
79
+ * </Remount>
80
+ *
81
+ * <Remount watch={user} when={(prev, next) => prev.id !== next.id}>
82
+ * <ProfileForm user={user} />
83
+ * </Remount>
84
+ * ```
85
+ *
86
+ * Pick one mode (`deps` or `watch`+`when`) and stick to it for the lifetime
87
+ * of the element.
88
+ *
89
+ * @param props Watched inputs, reset policy, and children to remount together.
90
+ * @returns A keyed fragment; no wrapper DOM element is added.
91
+ */
92
+ export function Remount<T = unknown>(props: RemountProps<T>): ReactElement {
93
+ const usingWhen = typeof props.when === 'function'
94
+ // Both hooks run unconditionally so the hook order is stable even if a
95
+ // caller (incorrectly) switches modes between renders.
96
+ const depsKey = useRemountKey(usingWhen ? [] : (props.deps ?? []))
97
+ const whenKey = useRemountKeyWhen(
98
+ usingWhen ? (props.watch as T) : (undefined as T),
99
+ usingWhen ? (props.when as (prev: T, next: T) => boolean) : alwaysFalse
100
+ )
101
+ const key = usingWhen ? whenKey : depsKey
102
+ return <Fragment key={`remount-${key}`}>{props.children}</Fragment>
103
+ }
@@ -0,0 +1,256 @@
1
+ // react-fresh-key v0.1.0 — vendored copy (TypeScript).
2
+ // This file is yours now: edit it freely. Docs: https://www.npmjs.com/package/react-fresh-key
3
+ /*!
4
+ MIT License
5
+
6
+ Copyright (c) 2026 Rafael Buzatto de Campos
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ SOFTWARE.
25
+ */
26
+ import {
27
+ Fragment,
28
+ createContext,
29
+ useCallback,
30
+ useContext,
31
+ useEffect,
32
+ useRef,
33
+ useState,
34
+ type ReactElement,
35
+ type ReactNode,
36
+ } from 'react'
37
+
38
+ const ResetContext = createContext<(() => void) | null>(null)
39
+
40
+ /** Props for the shared, zero-argument {@link ResetBoundary}. */
41
+ export interface ResetBoundaryProps {
42
+ /** Subtree to remount on reset. Its local state, refs, effects, and DOM are recreated. */
43
+ children?: ReactNode
44
+ /**
45
+ * Observes each reset request synchronously, with no arguments (for example, analytics).
46
+ * This is not a mount lifecycle callback; initialize focus in the remounted UI itself.
47
+ * Use {@link createResetBoundary} to receive typed request metadata.
48
+ */
49
+ onReset?: () => void
50
+ }
51
+
52
+ /**
53
+ * A typed reset request. Check `key` to narrow `payload` to that action's type.
54
+ * `Events` maps registration keys to the payloads declared by the component author.
55
+ */
56
+ export type ResetEvent<Events extends object> = {
57
+ [Key in keyof Events]-?: {
58
+ /** Registration key selected by the descendant's hook. */
59
+ key: Key
60
+ /** Original argument passed to reset, or undefined for a call without a payload. */
61
+ payload: Events[Key]
62
+ }
63
+ }[keyof Events]
64
+
65
+ /** Props for a boundary returned by {@link createResetBoundary}. */
66
+ export interface TypedResetBoundaryProps<Events extends object> {
67
+ /** Subtree to remount when a descendant requests a reset through this factory's hook. */
68
+ children?: ReactNode
69
+ /**
70
+ * Observes each reset request synchronously. Does not signal that the new subtree has mounted.
71
+ * @param event - Registration key and unchanged payload. Narrow on `event.key` before reading action-specific fields.
72
+ */
73
+ onReset?: (event: ResetEvent<Events>) => void
74
+ }
75
+
76
+ type ResetArgs<Payload> = undefined extends Payload ? [payload?: Payload] : [payload: Payload]
77
+
78
+ // Infer the intersection through function parameters: a dynamic key needs a
79
+ // payload valid for every possible action. A union of callable signatures
80
+ // alone would incorrectly allow omitting required data when one key is void.
81
+ type RegistrationPayload<Events extends object, Key extends keyof Events> = {
82
+ [K in Key]-?: (payload: Events[K]) => void
83
+ }[Key] extends (payload: infer Payload) => void
84
+ ? Payload
85
+ : never
86
+
87
+ type RegisteredReset<Events extends object, Key extends keyof Events> = {
88
+ /**
89
+ * Requests a remount from the matching boundary and reports this registration's key and payload.
90
+ * Supply a payload unless the action's declared type accepts undefined.
91
+ * @param payload - Data for the selected action. May be omitted only if its type accepts undefined.
92
+ */
93
+ // biome-ignore lint/style/useShorthandFunctionType: Signature-level JSDoc supplies editor help for the returned reset function.
94
+ (...args: ResetArgs<RegistrationPayload<Events, Key>>): void
95
+ }
96
+
97
+ function useResetRequest<Args extends unknown[]>(onReset: ((...args: Args) => void) | undefined) {
98
+ const [generation, setGeneration] = useState(0)
99
+ const onResetRef = useRef(onReset)
100
+ useEffect(() => {
101
+ onResetRef.current = onReset
102
+ })
103
+
104
+ const request = useCallback((...args: Args) => {
105
+ setGeneration((g) => g + 1)
106
+ onResetRef.current?.(...args)
107
+ }, [])
108
+
109
+ return { generation, request }
110
+ }
111
+
112
+ /**
113
+ * Imperative reset from below: any descendant can call the function returned
114
+ * by {@link useResetBoundary} to unmount and freshly mount everything inside
115
+ * the nearest boundary — state, refs, effects, DOM.
116
+ *
117
+ * Handy for "start over" buttons, clearing a wizard after submit, or error
118
+ * recovery, without the parent knowing anything about it:
119
+ *
120
+ * ```tsx
121
+ * <ResetBoundary>
122
+ * <CheckoutWizard />
123
+ * </ResetBoundary>
124
+ *
125
+ * // deep inside CheckoutWizard:
126
+ * const reset = useResetBoundary();
127
+ * <button onClick={reset}>Start over</button>
128
+ * ```
129
+ *
130
+ * Boundaries nest; `useResetBoundary` targets the nearest one.
131
+ * @param props - Subtree and optional synchronous reset observer.
132
+ * @returns A resettable subtree accessible through {@link useResetBoundary}.
133
+ */
134
+ export function ResetBoundary({ children, onReset }: ResetBoundaryProps): ReactElement {
135
+ const { generation, request } = useResetRequest(onReset)
136
+ // Preserve the zero-argument API, including when used as onClick={reset}.
137
+ const reset = useCallback(() => request(), [request])
138
+
139
+ return (
140
+ <ResetContext.Provider value={reset}>
141
+ <Fragment key={`reset-${generation}`}>{children}</Fragment>
142
+ </ResetContext.Provider>
143
+ )
144
+ }
145
+
146
+ /**
147
+ * Returns the nearest {@link ResetBoundary}'s reset function.
148
+ * @returns A stable, zero-argument function that remounts the boundary's entire subtree.
149
+ * @throws If no shared {@link ResetBoundary} is above the caller. Factory boundaries use their own hook.
150
+ */
151
+ export function useResetBoundary(): () => void {
152
+ const reset = useContext(ResetContext)
153
+ if (reset === null) {
154
+ throw new Error('react-fresh-key: useResetBoundary must be used inside a <ResetBoundary>.')
155
+ }
156
+ return reset
157
+ }
158
+
159
+ /**
160
+ * Creates a boundary and registration hook that share a typed reset contract.
161
+ * Create it once at module scope; aliases and destructured exports are safe.
162
+ *
163
+ * ```tsx
164
+ * const checkoutReset = createResetBoundary<{
165
+ * restart: { step: number }
166
+ * completed: { orderId: string }
167
+ * dismissed: void
168
+ * }>()
169
+ * export const ClientResetBoundary = checkoutReset.ResetBoundary
170
+ * export const useResetClientRegistration = checkoutReset.useResetBoundary
171
+ *
172
+ * // Inside a descendant of ClientResetBoundary:
173
+ * const reset = useResetClientRegistration('restart')
174
+ * // In an event handler:
175
+ * reset({ step: 2 })
176
+ * ```
177
+ *
178
+ * The hook targets the nearest boundary created by this factory call. It
179
+ * captures a registration key without subscriptions or registration effects.
180
+ * The reset function is stable while that key and provider remain unchanged.
181
+ * Payloads are forwarded unchanged to `onReset({ key, payload })`.
182
+ * @returns A matching boundary and hook. Export either under an application-specific name without binding.
183
+ */
184
+ export function createResetBoundary<Events extends object>(): {
185
+ /** Boundary providing this factory's reset scope and typed request observer. */
186
+ ResetBoundary: {
187
+ /**
188
+ * Provides this factory's reset scope. Nested instances use the nearest matching provider.
189
+ * @param props - Subtree to remount and optional `onReset` observer of typed requests.
190
+ * @returns A resettable subtree accessible through this factory's hook.
191
+ */
192
+ // biome-ignore lint/style/useShorthandFunctionType: Signature-level JSDoc preserves editor help when the boundary is aliased.
193
+ (props: TypedResetBoundaryProps<Events>): ReactElement
194
+ }
195
+ /** Registration hook for the nearest boundary from this factory; safe to alias or destructure. */
196
+ useResetBoundary: {
197
+ /**
198
+ * Selects a reset action from the nearest boundary created by this factory.
199
+ * Safe to alias, for example `export const useResetClientRegistration = scope.useResetBoundary`.
200
+ * @param key - An action key from the shared event map; determines the reset payload's type.
201
+ * @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.
202
+ * @throws If no boundary from this factory call is above the caller.
203
+ */
204
+ // biome-ignore lint/style/useShorthandFunctionType: Signature-level JSDoc preserves editor help when the hook is aliased.
205
+ <Key extends keyof Events>(key: Key): RegisteredReset<Events, Key>
206
+ }
207
+ } {
208
+ const Context = createContext<((event: ResetEvent<Events>) => void) | null>(null)
209
+
210
+ /**
211
+ * Provides this factory's reset scope; its subtree remounts when the matching hook requests it.
212
+ * @param props - Children and optional synchronous `onReset` request observer.
213
+ */
214
+ function TypedResetBoundary({
215
+ children,
216
+ onReset,
217
+ }: TypedResetBoundaryProps<Events>): ReactElement {
218
+ const { generation, request } = useResetRequest(onReset)
219
+ return (
220
+ <Context.Provider value={request}>
221
+ <Fragment key={`reset-${generation}`}>{children}</Fragment>
222
+ </Context.Provider>
223
+ )
224
+ }
225
+
226
+ /**
227
+ * Selects a reset action from the nearest boundary created by this factory.
228
+ * @param key - Registration key included in each request's `{ key, payload }` event.
229
+ * @returns A reset callback stable while the key and provider are unchanged.
230
+ * @throws If no boundary from this factory is above the caller.
231
+ */
232
+ function useRegisteredReset<Key extends keyof Events>(key: Key): RegisteredReset<Events, Key> {
233
+ const request = useContext(Context)
234
+ const reset = useCallback(
235
+ /**
236
+ * Requests a remount and forwards this registration's key and payload to `onReset`.
237
+ * @param payload - Request metadata forwarded unchanged to the matching boundary.
238
+ */
239
+ (payload?: Events[Key]) => {
240
+ // The public signature relates each key to its own payload. TypeScript
241
+ // cannot express that correlation when constructing a generic union.
242
+ request?.({ key, payload } as ResetEvent<Events>)
243
+ },
244
+ [request, key]
245
+ )
246
+
247
+ if (request === null) {
248
+ throw new Error(
249
+ 'react-fresh-key: useResetBoundary must be used inside a boundary from the same createResetBoundary() call.'
250
+ )
251
+ }
252
+ return reset as RegisteredReset<Events, Key>
253
+ }
254
+
255
+ return { ResetBoundary: TypedResetBoundary, useResetBoundary: useRegisteredReset }
256
+ }