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.
- package/CHANGELOG.md +19 -0
- package/LICENSE +21 -0
- package/README.md +475 -0
- package/RELEASING.md +89 -0
- package/bin/cli.mjs +199 -0
- package/dist/index.cjs +309 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +390 -0
- package/dist/index.d.ts +390 -0
- package/dist/index.js +286 -0
- package/dist/index.js.map +1 -0
- package/package.json +98 -0
- package/registry.json +48 -0
- package/templates/js/Remount.jsx +60 -0
- package/templates/js/ResetBoundary.jsx +143 -0
- package/templates/js/useRemountKey.js +113 -0
- package/templates/js/useResettableState.js +85 -0
- package/templates/js/utils.js +95 -0
- package/templates/js/withRemount.jsx +162 -0
- package/templates/ts/Remount.tsx +103 -0
- package/templates/ts/ResetBoundary.tsx +256 -0
- package/templates/ts/useRemountKey.ts +115 -0
- package/templates/ts/useResettableState.ts +93 -0
- package/templates/ts/utils.ts +109 -0
- package/templates/ts/withRemount.tsx +281 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../scripts/react-jsx-runtime.mjs","../src/useRemountKey.ts","../src/utils.ts","../src/useResettableState.ts","../src/Remount.tsx","../src/withRemount.tsx","../src/ResetBoundary.tsx"],"sourcesContent":["// esbuild injects only the bindings needed by the classic JSX transform.\nexport { createElement as __freshKeyCreateElement, Fragment as __freshKeyFragment } from 'react'\n","import { useState } from 'react'\nimport { areDepsEqual, isEqualDep, type DepsList } from './utils'\n\n/**\n * Returns a numeric key that changes whenever any value in `deps` changes.\n * Values are compared with `Object.is` — like React's own dependency\n * comparison — except that plain objects and ordinary arrays are compared\n * one level deep. Distinct objects with equal content preserve the key;\n * `[{ id }]` written inline therefore converges instead of looping.\n *\n * A hook cannot remount the component it runs in, so apply the key to an\n * inner subtree:\n *\n * ```tsx\n * function Profile({ userId }: { userId: string }) {\n * const key = useRemountKey([userId])\n * return <ProfileForm key={key} userId={userId} />\n * }\n * ```\n *\n * The key is derived purely during render (the setState-in-render\n * \"derived state\" pattern), so it is SSR-deterministic and StrictMode-safe.\n * In the render where deps change, the *new* key is returned immediately.\n *\n * **Stability contract** — put primitives, references you already hold\n * (props, state, memoized values), or flat plain objects/arrays in `deps`.\n * A value that is a *new, non-equal* reference on every render — a nested\n * object literal, `new Map()`, `new Date()`, a class instance built inline —\n * can never compare equal and will trip React's re-render limit. Hold such\n * values in state or `useMemo` first.\n *\n * @param deps Watched values, compared using {@link DepsList} rules. Treat the\n * list and its values as immutable; changing their contents in place is not\n * detected reliably. An empty list keeps the initial key.\n * @returns A key local to this hook instance, starting at `0` and increasing\n * when dependencies change. Apply it to a descendant element's `key` prop.\n */\nexport function useRemountKey(deps: DepsList): number {\n const [state, setState] = useState<{ deps: DepsList; key: number }>({\n deps,\n key: 0,\n })\n if (!areDepsEqual(state.deps, deps)) {\n // Documented React pattern: setting state during render restarts the\n // render immediately with the new state, before touching the DOM.\n const key = state.key + 1\n setState({ deps, key })\n return key\n }\n return state.key\n}\n\n/**\n * Predicate flavor of {@link useRemountKey}: tracks an arbitrary `value` and\n * bumps the key when `when(prev, next)` returns `true` for a changed value.\n *\n * `prev` is the last recorded value. Whenever a changed value is observed,\n * the snapshot advances even if the predicate returns `false`. Equal values\n * retain the existing snapshot and skip the predicate. Equality uses\n * `Object.is`, or a one-level shallow comparison for plain objects and ordinary\n * arrays, so a flat inline object (`useRemountKeyWhen({ id, tab }, …)`) converges.\n *\n * ```tsx\n * const key = useRemountKeyWhen(user, (prev, next) => prev.id !== next.id)\n * ```\n *\n * **Stability contract**: the same as {@link useRemountKey} — a value that is\n * a new, non-equal reference on every render (nested literals, Map, Date,\n * class instances built inline) cannot converge; hold it in state or `useMemo`.\n *\n * @param value The immutable value to track. Replace changed objects instead\n * of mutating them in place.\n * @param when A pure render-time predicate comparing the last recorded value\n * with the new value. It is skipped on initial render and for equal values;\n * changing only the predicate does not trigger a comparison.\n * @returns A key starting at `0`, increased when a changed value satisfies\n * `when`. Apply it to a descendant element; it cannot remount this hook's owner.\n */\nexport function useRemountKeyWhen<T>(value: T, when: (prev: T, next: T) => boolean): number {\n const [state, setState] = useState<{ value: T; key: number }>({\n value,\n key: 0,\n })\n if (!isEqualDep(state.value, value)) {\n const key = when(state.value, value) ? state.key + 1 : state.key\n setState({ value, key })\n return key\n }\n return state.key\n}\n","/**\n * Watched values used by remount and state-reset APIs.\n *\n * List length and corresponding entries are compared. Entries use `Object.is`,\n * except that plain objects and ordinary arrays compare one level deep using\n * their own enumerable string and symbol properties; arrays also compare\n * length. Equal content preserves state even when object identities differ.\n * Nested objects, functions, Maps, Sets, Dates, and class instances (including\n * array subclasses) compare by identity.\n *\n * Treat the list and its values as immutable. A new list is fine when its\n * entries compare equal; values created anew on every render that cannot\n * compare equal can cause hooks to hit React's re-render limit.\n */\nexport type DepsList = readonly unknown[]\n\nfunction isPlainObject(value: unknown): value is Record<PropertyKey, unknown> {\n if (value === null || typeof value !== 'object') return false\n const proto = Object.getPrototypeOf(value)\n return proto === Object.prototype || proto === null\n}\n\n/** An `Array` proper — not a subclass, which is treated as a class instance. */\nfunction isOrdinaryArray(value: unknown): value is unknown[] {\n return Array.isArray(value) && Object.getPrototypeOf(value) === Array.prototype\n}\n\n/** Own enumerable keys, including symbol keys (which `Object.keys` omits). */\nfunction ownEnumerableKeys(value: object): PropertyKey[] {\n const keys: PropertyKey[] = Object.keys(value)\n for (const sym of Object.getOwnPropertySymbols(value)) {\n if (Object.prototype.propertyIsEnumerable.call(value, sym)) keys.push(sym)\n }\n return keys\n}\n\n/** Shallow-compare two objects: same own enumerable keys, `Object.is` per value. */\nexport function shallowEqual(a: object, b: object): boolean {\n if (Object.is(a, b)) return true\n const keysA = ownEnumerableKeys(a)\n const keysB = ownEnumerableKeys(b)\n if (keysA.length !== keysB.length) return false\n for (const key of keysA) {\n if (\n !Object.prototype.propertyIsEnumerable.call(b, key) ||\n !Object.is((a as Record<PropertyKey, unknown>)[key], (b as Record<PropertyKey, unknown>)[key])\n ) {\n return false\n }\n }\n return true\n}\n\n/**\n * Equality for a single watched value: `Object.is`, except that two *plain*\n * objects (`{}` / `Object.create(null)`) or two ordinary arrays are compared\n * one level deep — every own enumerable property, symbol keys included, plus\n * array length. This lets the common inline-literal shapes converge —\n * `useRemountKey([{ id }])`, `useRemountKeyWhen({ id, tab }, …)` — without\n * hiding real changes. Anything else (class instances, including `Array`\n * subclasses; Map/Set; Date; functions; nested objects' inner values) is\n * compared by identity.\n */\nexport function isEqualDep(a: unknown, b: unknown): boolean {\n if (Object.is(a, b)) return true\n if (isOrdinaryArray(a) && isOrdinaryArray(b)) return a.length === b.length && shallowEqual(a, b)\n if (isPlainObject(a) && isPlainObject(b)) return shallowEqual(a, b)\n return false\n}\n\n/** Compare two dependency lists element-wise with {@link isEqualDep}. */\nexport function areDepsEqual(a: DepsList, b: DepsList): boolean {\n if (a === b) return true\n if (a.length !== b.length) return false\n for (let i = 0; i < a.length; i++) {\n if (!isEqualDep(a[i], b[i])) return false\n }\n return true\n}\n\n/** Normalize a selector result into a deps list. */\nexport function toDeps(value: unknown): DepsList {\n return Array.isArray(value) ? value : [value]\n}\n","import { useCallback, useState, type Dispatch, type SetStateAction } from 'react'\nimport { areDepsEqual, type DepsList } from './utils'\n\nfunction resolveInitial<T>(initial: T | (() => T)): T {\n return typeof initial === 'function' ? (initial as () => T)() : initial\n}\n\n/**\n * Like `useState`, but the state snaps back to `initial` whenever any value\n * in `deps` changes.\n *\n * This is the cheap alternative to a full remount: only this piece of state\n * resets — DOM state (focus, scroll, uncontrolled inputs), refs, and effects\n * are untouched. Reach for this first; remount only when you need everything\n * gone.\n *\n * ```tsx\n * function Comment({ postId }: { postId: string }) {\n * const [draft, setDraft] = useResettableState('', [postId])\n * // draft clears when the user navigates to another post\n * }\n * ```\n *\n * Reset happens purely during render (no effect, no flash of stale state),\n * and the render in which deps change already sees the fresh value — so\n * code after the hook never observes state that belongs to the old deps.\n *\n * `deps` follow {@link DepsList} comparison rules: `Object.is`, with plain\n * objects and ordinary arrays compared one level deep. Dependencies must be\n * able to compare equal across rerenders: nested literals or new class\n * instances created on every render can cause a re-render loop. Keep those\n * values in state or memoize them, and replace changed values rather than\n * mutating them in place.\n *\n * @param initial The value, or a pure lazy initializer, used on mount and on\n * each dependency change. Resets use the current render's `initial`; changing\n * only this argument does not reset existing state. Wrap a function-valued\n * initial state in an initializer, as with React's `useState`.\n * @param deps Immutable watched values that determine when to reset. An empty\n * list disables dependency-driven resets.\n * @returns The current state and a stable setter accepting either the next\n * value or a pure updater function. A dependency change returns fresh state\n * in the same render, without remounting the component.\n */\nexport function useResettableState<T>(\n initial: T | (() => T),\n deps: DepsList\n): [T, Dispatch<SetStateAction<T>>] {\n const [state, setState] = useState<{ deps: DepsList; value: T }>(() => ({\n deps,\n value: resolveInitial(initial),\n }))\n\n const setValue = useCallback<Dispatch<SetStateAction<T>>>((action) => {\n setState((s) => ({\n deps: s.deps,\n value: typeof action === 'function' ? (action as (prev: T) => T)(s.value) : action,\n }))\n }, [])\n\n if (!areDepsEqual(state.deps, deps)) {\n const value = resolveInitial(initial)\n setState({ deps, value })\n return [value, setValue]\n }\n\n return [state.value, setValue]\n}\n","import { Fragment, type ReactElement, type ReactNode } from 'react'\nimport { useRemountKey, useRemountKeyWhen } from './useRemountKey'\nimport type { DepsList } from './utils'\n\n/** Dependency-list mode for {@link Remount}; do not combine with `watch` or `when`. */\nexport interface RemountDepsProps {\n /**\n * Remount children when a dependency changes. Values use `Object.is`, except\n * plain objects and ordinary arrays are compared one level deep.\n */\n deps: DepsList\n watch?: never\n when?: never\n /** Children that remount together; the component containing this boundary stays mounted. */\n children?: ReactNode\n}\n\n/** Predicate mode for {@link Remount}; provide both `watch` and `when`, without `deps`. */\nexport interface RemountWhenProps<T> {\n deps?: never\n /**\n * Value compared across renders before consulting `when`. Uses `Object.is`,\n * except plain objects and ordinary arrays are compared one level deep.\n */\n watch: T\n /**\n * Pure predicate called during render when `watch` changes. Return `true` to\n * remount children. The tracked snapshot advances even when this returns `false`.\n * React may evaluate the predicate more than once; avoid side effects.\n *\n * @param prev Previous tracked value, not necessarily the value at the last remount.\n * @param next Incoming `watch` value.\n */\n when: (prev: T, next: T) => boolean\n /** Children that remount together; the component containing this boundary stays mounted. */\n children?: ReactNode\n}\n\n/** Choose `deps` or `watch` + `when`, and keep that mode for the boundary's lifetime. */\nexport type RemountProps<T = unknown> = RemountDepsProps | RemountWhenProps<T>\n\nconst alwaysFalse = () => false\n\n/**\n * Declarative remount boundary. Everything inside is unmounted and mounted\n * fresh (state, refs, effects, DOM) when the watched values change.\n *\n * Used by a parent it is nicer `key` syntax; used *inside* a component around\n * its own subtree, it lets the component own its reset policy:\n *\n * ```tsx\n * <Remount deps={[userId]}>\n * <ProfileForm userId={userId} />\n * </Remount>\n *\n * <Remount watch={user} when={(prev, next) => prev.id !== next.id}>\n * <ProfileForm user={user} />\n * </Remount>\n * ```\n *\n * Pick one mode (`deps` or `watch`+`when`) and stick to it for the lifetime\n * of the element.\n *\n * @param props Watched inputs, reset policy, and children to remount together.\n * @returns A keyed fragment; no wrapper DOM element is added.\n */\nexport function Remount<T = unknown>(props: RemountProps<T>): ReactElement {\n const usingWhen = typeof props.when === 'function'\n // Both hooks run unconditionally so the hook order is stable even if a\n // caller (incorrectly) switches modes between renders.\n const depsKey = useRemountKey(usingWhen ? [] : (props.deps ?? []))\n const whenKey = useRemountKeyWhen(\n usingWhen ? (props.watch as T) : (undefined as T),\n usingWhen ? (props.when as (prev: T, next: T) => boolean) : alwaysFalse\n )\n const key = usingWhen ? whenKey : depsKey\n return <Fragment key={`remount-${key}`}>{props.children}</Fragment>\n}\n","import {\n forwardRef,\n useState,\n type ComponentProps,\n type ComponentPropsWithRef,\n type ComponentType,\n type ForwardRefExoticComponent,\n type JSX,\n type LazyExoticComponent,\n type MemoExoticComponent,\n} from 'react'\nimport { areDepsEqual, shallowEqual, toDeps, type DepsList } from './utils'\n\n/**\n * What to watch for remounting:\n *\n * - `['userId', 'mode']` — prop names; remount when any of them changes.\n * - `(props) => props.user.id` — selector; remount when the returned value\n * (or any element, if it returns an array) changes. Same as `{ select }`.\n * - `{ select: (props) => value | deps[] }` — explicit selector form.\n * - `{ when: (prev, next) => boolean }` — predicate over previous/next props;\n * remount when it returns `true`.\n *\n * Predicates are always explicit (`{ when }`). A bare function is always a\n * selector — there is no arity-based guessing.\n *\n * Selected values use `Object.is`, except plain objects and ordinary arrays\n * are compared one level deep. Selectors and predicates run during render:\n * keep them pure, because React may evaluate them more than once.\n *\n * Declared `defaultProps` are applied before callbacks, including through\n * `memo`. Inner defaults behind `lazy` and JavaScript parameter defaults\n * cannot be resolved by the wrapper; handle any optional props accordingly.\n */\nexport type WatchSpec<P extends object> =\n | ReadonlyArray<keyof P>\n | ((props: P) => unknown)\n | {\n /**\n * Pure selector of the incoming props. Return one watched value or an\n * array of dependencies; remount when any selected dependency changes.\n *\n * @param props Incoming props with available `defaultProps` applied.\n */\n select: (props: P) => unknown\n }\n | {\n /**\n * Pure predicate called when props differ shallowly (`Object.is` per\n * prop). Return `true` to remount. The snapshot advances on a detected\n * change even when this returns `false`.\n *\n * @param prev Previous tracked props, not necessarily those at the last remount.\n * @param next Incoming props. Available `defaultProps` are applied to both arguments.\n */\n when: (prev: P, next: P) => boolean\n }\n\n/**\n * `true` when `C` is a `lazy` component, possibly wrapped in any number of\n * `memo` layers. Mirrors the runtime `unwrapMemo`, which also looks through\n * every memo layer and then stops at a lazy object it cannot open.\n */\ntype ContainsLazy<C> =\n C extends LazyExoticComponent<infer _Component>\n ? true\n : C extends MemoExoticComponent<infer T>\n ? ContainsLazy<T>\n : false\n\n/**\n * The props the wrapper accepts: whatever React's JSX checker would accept\n * for the original component (so props covered by `defaultProps` stay\n * optional — through `memo` too), with the ref type inferred from the\n * original.\n *\n * `lazy` is the one exception, at any depth of `memo` nesting: its inner\n * component's defaults are unknowable until the module loads, so the wrapper\n * requires those props as declared. That keeps the types truthful about what\n * selectors and predicates receive.\n */\n// biome-ignore lint/suspicious/noExplicitAny: This component constraint accepts required props; the concrete C preserves their types.\nexport type RemountedProps<C extends ComponentType<any>> =\n ContainsLazy<C> extends true\n ? ComponentPropsWithRef<C>\n : JSX.LibraryManagedAttributes<C, ComponentPropsWithRef<C>>\n\ntype NormalizedWatch<P extends object> =\n | { mode: 'select'; select: (props: P) => DepsList }\n | { mode: 'when'; when: (prev: P, next: P) => boolean }\n\nfunction normalizeWatch<P extends object>(watch: WatchSpec<P>): NormalizedWatch<P> {\n if (Array.isArray(watch)) {\n const names = watch as ReadonlyArray<keyof P>\n return { mode: 'select', select: (props) => names.map((n) => props[n]) }\n }\n if (typeof watch === 'function') {\n if (watch.length >= 2) {\n throw new Error(\n 'react-fresh-key: a bare function passed to withRemount is a selector `(props) => value`. ' +\n 'For a predicate `(prev, next) => boolean`, pass `{ when: fn }` instead.'\n )\n }\n const select = watch as (props: P) => unknown\n return { mode: 'select', select: (props) => toDeps(select(props)) }\n }\n if (watch && typeof watch === 'object') {\n if ('select' in watch && typeof watch.select === 'function') {\n const select = watch.select\n return { mode: 'select', select: (props) => toDeps(select(props)) }\n }\n if ('when' in watch && typeof watch.when === 'function') {\n return { mode: 'when', when: watch.when }\n }\n }\n throw new Error(\n 'react-fresh-key: invalid watch spec. Expected a prop-name array, a selector function, { select }, or { when }.'\n )\n}\n\nconst REACT_MEMO_TYPE = Symbol.for('react.memo')\n\n/**\n * `memo(C)` is an object `{ $$typeof, type: C }`; React applies the *inner*\n * component's `defaultProps` when it renders, and `JSX.LibraryManagedAttributes`\n * unwraps `memo` the same way. Mirror both so types and runtime agree.\n * (`lazy` cannot be unwrapped before it loads; see {@link RemountedProps}.)\n */\nfunction unwrapMemo(Component: unknown): unknown {\n let current = Component\n while (\n current !== null &&\n typeof current === 'object' &&\n (current as { $$typeof?: unknown }).$$typeof === REACT_MEMO_TYPE\n ) {\n current = (current as { type: unknown }).type\n }\n return current\n}\n\n/**\n * Apply `defaultProps` the way React does (a default fills in only when the\n * prop is `undefined`). Returns the same object when nothing needs filling.\n */\nfunction makeDefaultsResolver<P extends object>(Component: unknown): (props: P) => P {\n const defaults = (unwrapMemo(Component) as { defaultProps?: Record<string, unknown> } | null)\n ?.defaultProps\n if (!defaults) return (props) => props\n const keys = Object.keys(defaults)\n return (props) => {\n let resolved: Record<string, unknown> | null = null\n for (const key of keys) {\n if ((props as Record<string, unknown>)[key] === undefined) {\n resolved ??= { ...(props as Record<string, unknown>) }\n resolved[key] = defaults[key]\n }\n }\n return (resolved ?? props) as P\n }\n}\n\ninterface WatchState<P> {\n /** The raw props object the current snapshot was taken from (the anchor). */\n props: P\n /** Selected deps (select mode only). */\n deps: DepsList | null\n key: number\n}\n\n/**\n * Single-useState implementation so the hook order is identical for both\n * modes, with the raw `props` object as the convergence anchor: after a\n * render-phase setState, React re-invokes the component with the *same*\n * props object, so the comparison branch is skipped on that second pass.\n * That makes the derivation converge even when a selector returns a fresh\n * object on every call (it remounts once per props change instead of looping).\n */\nfunction useWatchKey<P extends object>(\n props: P,\n spec: NormalizedWatch<P>,\n resolve: (props: P) => P\n): number {\n const [state, setState] = useState<WatchState<P>>(() => ({\n props,\n deps: spec.mode === 'select' ? spec.select(resolve(props)) : null,\n key: 0,\n }))\n\n if (state.props !== props) {\n if (spec.mode === 'select') {\n const deps = spec.select(resolve(props))\n if (!areDepsEqual(state.deps as DepsList, deps)) {\n const key = state.key + 1\n setState({ props, deps, key })\n return key\n }\n // Unchanged: leave the snapshot alone (no extra render pass).\n } else if (!shallowEqual(state.props, props)) {\n // Only consult the predicate when props actually changed (shallow),\n // so parent re-renders with identical props stay single-pass.\n const key = spec.when(resolve(state.props), resolve(props)) ? state.key + 1 : state.key\n setState({ props, deps: null, key })\n return key\n }\n }\n\n return state.key\n}\n\n/**\n * Wrap a component with a remount policy declared *at the definition site*,\n * so call sites don't need to know which props warrant a fresh mount:\n *\n * ```tsx\n * // Profile.tsx\n * function Profile({ userId }: ProfileProps) { ... }\n * export default withRemount(Profile, ['userId'])\n *\n * // Anywhere else — no key juggling required:\n * <Profile userId={id} />\n * ```\n *\n * Parents can still override identity the normal way with their own `key`.\n * Refs are forwarded, and the ref type is inferred from the wrapped\n * component (a `forwardRef<HTMLInputElement, …>` component yields a wrapper\n * that accepts `Ref<HTMLInputElement>` and nothing else). Props covered by\n * `defaultProps` stay optional, except behind `lazy`; see {@link RemountedProps}.\n *\n * Call this once outside rendering. Creating a wrapper during render creates\n * a new component type each time and discards its state. Selectors and\n * predicates must be pure; they run during render and may be evaluated again.\n *\n * @param Component Component whose entire subtree remounts when the rule matches.\n * @param watch Prop names, a selector, `{ select }`, or `{ when }`; see {@link WatchSpec}.\n * @returns A component accepting the original props and supported ref type.\n * @throws If the watch specification is invalid; pass predicates as `{ when }`.\n */\n// biome-ignore lint/suspicious/noExplicitAny: This component constraint accepts required props; the concrete C preserves their types.\nexport function withRemount<C extends ComponentType<any>>(\n Component: C,\n watch: WatchSpec<ComponentProps<C>>\n): ForwardRefExoticComponent<RemountedProps<C>> {\n const spec = normalizeWatch(watch)\n const resolve = makeDefaultsResolver<ComponentProps<C>>(Component)\n const Inner = Component as ComponentType<ComponentProps<C>>\n\n const Wrapped = forwardRef<unknown, ComponentProps<C>>((props, ref) => {\n const key = useWatchKey(props as ComponentProps<C>, spec, resolve)\n return <Inner key={key} ref={ref} {...(props as ComponentProps<C>)} />\n })\n\n const name = Component.displayName ?? Component.name ?? 'Component'\n Wrapped.displayName = `withRemount(${name})`\n\n return Wrapped as unknown as ForwardRefExoticComponent<RemountedProps<C>>\n}\n","import {\n Fragment,\n createContext,\n useCallback,\n useContext,\n useEffect,\n useRef,\n useState,\n type ReactElement,\n type ReactNode,\n} from 'react'\n\nconst ResetContext = createContext<(() => void) | null>(null)\n\n/** Props for the shared, zero-argument {@link ResetBoundary}. */\nexport interface ResetBoundaryProps {\n /** Subtree to remount on reset. Its local state, refs, effects, and DOM are recreated. */\n children?: ReactNode\n /**\n * Observes each reset request synchronously, with no arguments (for example, analytics).\n * This is not a mount lifecycle callback; initialize focus in the remounted UI itself.\n * Use {@link createResetBoundary} to receive typed request metadata.\n */\n onReset?: () => void\n}\n\n/**\n * A typed reset request. Check `key` to narrow `payload` to that action's type.\n * `Events` maps registration keys to the payloads declared by the component author.\n */\nexport type ResetEvent<Events extends object> = {\n [Key in keyof Events]-?: {\n /** Registration key selected by the descendant's hook. */\n key: Key\n /** Original argument passed to reset, or undefined for a call without a payload. */\n payload: Events[Key]\n }\n}[keyof Events]\n\n/** Props for a boundary returned by {@link createResetBoundary}. */\nexport interface TypedResetBoundaryProps<Events extends object> {\n /** Subtree to remount when a descendant requests a reset through this factory's hook. */\n children?: ReactNode\n /**\n * Observes each reset request synchronously. Does not signal that the new subtree has mounted.\n * @param event - Registration key and unchanged payload. Narrow on `event.key` before reading action-specific fields.\n */\n onReset?: (event: ResetEvent<Events>) => void\n}\n\ntype ResetArgs<Payload> = undefined extends Payload ? [payload?: Payload] : [payload: Payload]\n\n// Infer the intersection through function parameters: a dynamic key needs a\n// payload valid for every possible action. A union of callable signatures\n// alone would incorrectly allow omitting required data when one key is void.\ntype RegistrationPayload<Events extends object, Key extends keyof Events> = {\n [K in Key]-?: (payload: Events[K]) => void\n}[Key] extends (payload: infer Payload) => void\n ? Payload\n : never\n\ntype RegisteredReset<Events extends object, Key extends keyof Events> = {\n /**\n * Requests a remount from the matching boundary and reports this registration's key and payload.\n * Supply a payload unless the action's declared type accepts undefined.\n * @param payload - Data for the selected action. May be omitted only if its type accepts undefined.\n */\n // biome-ignore lint/style/useShorthandFunctionType: Signature-level JSDoc supplies editor help for the returned reset function.\n (...args: ResetArgs<RegistrationPayload<Events, Key>>): void\n}\n\nfunction useResetRequest<Args extends unknown[]>(onReset: ((...args: Args) => void) | undefined) {\n const [generation, setGeneration] = useState(0)\n const onResetRef = useRef(onReset)\n useEffect(() => {\n onResetRef.current = onReset\n })\n\n const request = useCallback((...args: Args) => {\n setGeneration((g) => g + 1)\n onResetRef.current?.(...args)\n }, [])\n\n return { generation, request }\n}\n\n/**\n * Imperative reset from below: any descendant can call the function returned\n * by {@link useResetBoundary} to unmount and freshly mount everything inside\n * the nearest boundary — state, refs, effects, DOM.\n *\n * Handy for \"start over\" buttons, clearing a wizard after submit, or error\n * recovery, without the parent knowing anything about it:\n *\n * ```tsx\n * <ResetBoundary>\n * <CheckoutWizard />\n * </ResetBoundary>\n *\n * // deep inside CheckoutWizard:\n * const reset = useResetBoundary();\n * <button onClick={reset}>Start over</button>\n * ```\n *\n * Boundaries nest; `useResetBoundary` targets the nearest one.\n * @param props - Subtree and optional synchronous reset observer.\n * @returns A resettable subtree accessible through {@link useResetBoundary}.\n */\nexport function ResetBoundary({ children, onReset }: ResetBoundaryProps): ReactElement {\n const { generation, request } = useResetRequest(onReset)\n // Preserve the zero-argument API, including when used as onClick={reset}.\n const reset = useCallback(() => request(), [request])\n\n return (\n <ResetContext.Provider value={reset}>\n <Fragment key={`reset-${generation}`}>{children}</Fragment>\n </ResetContext.Provider>\n )\n}\n\n/**\n * Returns the nearest {@link ResetBoundary}'s reset function.\n * @returns A stable, zero-argument function that remounts the boundary's entire subtree.\n * @throws If no shared {@link ResetBoundary} is above the caller. Factory boundaries use their own hook.\n */\nexport function useResetBoundary(): () => void {\n const reset = useContext(ResetContext)\n if (reset === null) {\n throw new Error('react-fresh-key: useResetBoundary must be used inside a <ResetBoundary>.')\n }\n return reset\n}\n\n/**\n * Creates a boundary and registration hook that share a typed reset contract.\n * Create it once at module scope; aliases and destructured exports are safe.\n *\n * ```tsx\n * const checkoutReset = createResetBoundary<{\n * restart: { step: number }\n * completed: { orderId: string }\n * dismissed: void\n * }>()\n * export const ClientResetBoundary = checkoutReset.ResetBoundary\n * export const useResetClientRegistration = checkoutReset.useResetBoundary\n *\n * // Inside a descendant of ClientResetBoundary:\n * const reset = useResetClientRegistration('restart')\n * // In an event handler:\n * reset({ step: 2 })\n * ```\n *\n * The hook targets the nearest boundary created by this factory call. It\n * captures a registration key without subscriptions or registration effects.\n * The reset function is stable while that key and provider remain unchanged.\n * Payloads are forwarded unchanged to `onReset({ key, payload })`.\n * @returns A matching boundary and hook. Export either under an application-specific name without binding.\n */\nexport function createResetBoundary<Events extends object>(): {\n /** Boundary providing this factory's reset scope and typed request observer. */\n ResetBoundary: {\n /**\n * Provides this factory's reset scope. Nested instances use the nearest matching provider.\n * @param props - Subtree to remount and optional `onReset` observer of typed requests.\n * @returns A resettable subtree accessible through this factory's hook.\n */\n // biome-ignore lint/style/useShorthandFunctionType: Signature-level JSDoc preserves editor help when the boundary is aliased.\n (props: TypedResetBoundaryProps<Events>): ReactElement\n }\n /** Registration hook for the nearest boundary from this factory; safe to alias or destructure. */\n useResetBoundary: {\n /**\n * Selects a reset action from the nearest boundary created by this factory.\n * Safe to alias, for example `export const useResetClientRegistration = scope.useResetBoundary`.\n * @param key - An action key from the shared event map; determines the reset payload's type.\n * @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.\n * @throws If no boundary from this factory call is above the caller.\n */\n // biome-ignore lint/style/useShorthandFunctionType: Signature-level JSDoc preserves editor help when the hook is aliased.\n <Key extends keyof Events>(key: Key): RegisteredReset<Events, Key>\n }\n} {\n const Context = createContext<((event: ResetEvent<Events>) => void) | null>(null)\n\n /**\n * Provides this factory's reset scope; its subtree remounts when the matching hook requests it.\n * @param props - Children and optional synchronous `onReset` request observer.\n */\n function TypedResetBoundary({\n children,\n onReset,\n }: TypedResetBoundaryProps<Events>): ReactElement {\n const { generation, request } = useResetRequest(onReset)\n return (\n <Context.Provider value={request}>\n <Fragment key={`reset-${generation}`}>{children}</Fragment>\n </Context.Provider>\n )\n }\n\n /**\n * Selects a reset action from the nearest boundary created by this factory.\n * @param key - Registration key included in each request's `{ key, payload }` event.\n * @returns A reset callback stable while the key and provider are unchanged.\n * @throws If no boundary from this factory is above the caller.\n */\n function useRegisteredReset<Key extends keyof Events>(key: Key): RegisteredReset<Events, Key> {\n const request = useContext(Context)\n const reset = useCallback(\n /**\n * Requests a remount and forwards this registration's key and payload to `onReset`.\n * @param payload - Request metadata forwarded unchanged to the matching boundary.\n */\n (payload?: Events[Key]) => {\n // The public signature relates each key to its own payload. TypeScript\n // cannot express that correlation when constructing a generic union.\n request?.({ key, payload } as ResetEvent<Events>)\n },\n [request, key]\n )\n\n if (request === null) {\n throw new Error(\n 'react-fresh-key: useResetBoundary must be used inside a boundary from the same createResetBoundary() call.'\n )\n }\n return reset as RegisteredReset<Events, Key>\n }\n\n return { ResetBoundary: TypedResetBoundary, useResetBoundary: useRegisteredReset }\n}\n"],"mappings":"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","names":["useState","useState","Fragment","Fragment","useState","useState","Fragment","useCallback","useState"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-fresh-key",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Declare React remount & state-reset policy where it belongs — in the component, not at every call site.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.cts",
|
|
17
|
+
"default": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"bin": {
|
|
22
|
+
"react-fresh-key": "./bin/cli.mjs"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"bin",
|
|
27
|
+
"templates",
|
|
28
|
+
"registry.json",
|
|
29
|
+
"CHANGELOG.md",
|
|
30
|
+
"RELEASING.md"
|
|
31
|
+
],
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18"
|
|
34
|
+
},
|
|
35
|
+
"sideEffects": false,
|
|
36
|
+
"keywords": [
|
|
37
|
+
"react",
|
|
38
|
+
"remount",
|
|
39
|
+
"key",
|
|
40
|
+
"reset",
|
|
41
|
+
"state",
|
|
42
|
+
"reset-state",
|
|
43
|
+
"hoc",
|
|
44
|
+
"hooks",
|
|
45
|
+
"reset-boundary"
|
|
46
|
+
],
|
|
47
|
+
"author": "Rafael Buzatto de Campos <mightyrafa@gmail.com> (https://github.com/rbuzatto)",
|
|
48
|
+
"license": "MIT",
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "git+https://github.com/rbuzatto/react-fresh-key.git"
|
|
52
|
+
},
|
|
53
|
+
"homepage": "https://github.com/rbuzatto/react-fresh-key#readme",
|
|
54
|
+
"bugs": {
|
|
55
|
+
"url": "https://github.com/rbuzatto/react-fresh-key/issues"
|
|
56
|
+
},
|
|
57
|
+
"peerDependencies": {
|
|
58
|
+
"react": ">=16.14.0"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@arethetypeswrong/cli": "^0.18.5",
|
|
62
|
+
"@biomejs/biome": "2.5.13",
|
|
63
|
+
"@testing-library/dom": "^10.4.1",
|
|
64
|
+
"@testing-library/react": "^16.1.0",
|
|
65
|
+
"@types/node": "^22.20.2",
|
|
66
|
+
"@types/react": "^19.0.0",
|
|
67
|
+
"@types/react-dom": "^19.0.0",
|
|
68
|
+
"@types/react17": "npm:@types/react@^17.0.93",
|
|
69
|
+
"@types/react18": "npm:@types/react@^18.3.31",
|
|
70
|
+
"jsdom": "^25.0.1",
|
|
71
|
+
"react": "^19.0.0",
|
|
72
|
+
"react-dom": "^19.0.0",
|
|
73
|
+
"tsup": "^8.3.5",
|
|
74
|
+
"typescript": "^5.7.2",
|
|
75
|
+
"vitest": "^2.1.8"
|
|
76
|
+
},
|
|
77
|
+
"scripts": {
|
|
78
|
+
"build": "pnpm run build:lib && pnpm run build:templates",
|
|
79
|
+
"build:lib": "tsup",
|
|
80
|
+
"build:templates": "node scripts/build-templates.mjs",
|
|
81
|
+
"test": "vitest run",
|
|
82
|
+
"test:watch": "vitest",
|
|
83
|
+
"test:types": "vitest run --typecheck.only",
|
|
84
|
+
"lint": "biome lint --error-on-warnings .",
|
|
85
|
+
"lint:fix": "biome lint --write .",
|
|
86
|
+
"format": "biome format --write .",
|
|
87
|
+
"format:check": "biome format .",
|
|
88
|
+
"check:style": "biome ci --error-on-warnings .",
|
|
89
|
+
"typecheck": "tsc --noEmit",
|
|
90
|
+
"typecheck:compat": "tsc --noEmit -p test/compat/tsconfig.react17.json && tsc --noEmit -p test/compat/tsconfig.react18.json && tsc --noEmit -p test/compat/tsconfig.react19.json",
|
|
91
|
+
"check:exports": "attw --pack .",
|
|
92
|
+
"check:package": "node scripts/check-package.mjs",
|
|
93
|
+
"check": "pnpm run check:style && pnpm run typecheck && pnpm run test && pnpm run build && pnpm run check:exports",
|
|
94
|
+
"check:release": "pnpm run check && pnpm run check:package",
|
|
95
|
+
"preversion": "pnpm run check:release",
|
|
96
|
+
"version": "pnpm run build"
|
|
97
|
+
}
|
|
98
|
+
}
|
package/registry.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"items": {
|
|
4
|
+
"utils": {
|
|
5
|
+
"internal": true,
|
|
6
|
+
"files": ["utils"],
|
|
7
|
+
"requires": [],
|
|
8
|
+
"exports": [],
|
|
9
|
+
"types": ["DepsList"],
|
|
10
|
+
"description": "Shared comparison helpers (pulled in automatically)."
|
|
11
|
+
},
|
|
12
|
+
"use-remount-key": {
|
|
13
|
+
"files": ["useRemountKey"],
|
|
14
|
+
"requires": ["utils"],
|
|
15
|
+
"exports": ["useRemountKey", "useRemountKeyWhen"],
|
|
16
|
+
"types": [],
|
|
17
|
+
"description": "Hooks returning a key that changes when deps (or a predicate) say so."
|
|
18
|
+
},
|
|
19
|
+
"use-resettable-state": {
|
|
20
|
+
"files": ["useResettableState"],
|
|
21
|
+
"requires": ["utils"],
|
|
22
|
+
"exports": ["useResettableState"],
|
|
23
|
+
"types": [],
|
|
24
|
+
"description": "useState that snaps back to initial when deps change — no remount."
|
|
25
|
+
},
|
|
26
|
+
"remount": {
|
|
27
|
+
"files": ["Remount"],
|
|
28
|
+
"requires": ["use-remount-key"],
|
|
29
|
+
"exports": ["Remount"],
|
|
30
|
+
"types": ["RemountProps", "RemountDepsProps", "RemountWhenProps"],
|
|
31
|
+
"description": "<Remount> declarative boundary (deps or watch+when)."
|
|
32
|
+
},
|
|
33
|
+
"with-remount": {
|
|
34
|
+
"files": ["withRemount"],
|
|
35
|
+
"requires": ["utils"],
|
|
36
|
+
"exports": ["withRemount"],
|
|
37
|
+
"types": ["WatchSpec", "RemountedProps"],
|
|
38
|
+
"description": "withRemount HOC: declare remount policy at the definition site."
|
|
39
|
+
},
|
|
40
|
+
"reset-boundary": {
|
|
41
|
+
"files": ["ResetBoundary"],
|
|
42
|
+
"requires": [],
|
|
43
|
+
"exports": ["ResetBoundary", "useResetBoundary", "createResetBoundary"],
|
|
44
|
+
"types": ["ResetBoundaryProps", "ResetEvent", "TypedResetBoundaryProps"],
|
|
45
|
+
"description": "Reset boundaries and typed reset registrations for descendant-triggered resets."
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
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 { Fragment } from 'react';
|
|
27
|
+
import { useRemountKey, useRemountKeyWhen } from './useRemountKey';
|
|
28
|
+
const alwaysFalse = () => false;
|
|
29
|
+
/**
|
|
30
|
+
* Declarative remount boundary. Everything inside is unmounted and mounted
|
|
31
|
+
* fresh (state, refs, effects, DOM) when the watched values change.
|
|
32
|
+
*
|
|
33
|
+
* Used by a parent it is nicer `key` syntax; used *inside* a component around
|
|
34
|
+
* its own subtree, it lets the component own its reset policy:
|
|
35
|
+
*
|
|
36
|
+
* ```tsx
|
|
37
|
+
* <Remount deps={[userId]}>
|
|
38
|
+
* <ProfileForm userId={userId} />
|
|
39
|
+
* </Remount>
|
|
40
|
+
*
|
|
41
|
+
* <Remount watch={user} when={(prev, next) => prev.id !== next.id}>
|
|
42
|
+
* <ProfileForm user={user} />
|
|
43
|
+
* </Remount>
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* Pick one mode (`deps` or `watch`+`when`) and stick to it for the lifetime
|
|
47
|
+
* of the element.
|
|
48
|
+
*
|
|
49
|
+
* @param props Watched inputs, reset policy, and children to remount together.
|
|
50
|
+
* @returns A keyed fragment; no wrapper DOM element is added.
|
|
51
|
+
*/
|
|
52
|
+
export function Remount(props) {
|
|
53
|
+
const usingWhen = typeof props.when === 'function';
|
|
54
|
+
// Both hooks run unconditionally so the hook order is stable even if a
|
|
55
|
+
// caller (incorrectly) switches modes between renders.
|
|
56
|
+
const depsKey = useRemountKey(usingWhen ? [] : (props.deps ?? []));
|
|
57
|
+
const whenKey = useRemountKeyWhen(usingWhen ? props.watch : undefined, usingWhen ? props.when : alwaysFalse);
|
|
58
|
+
const key = usingWhen ? whenKey : depsKey;
|
|
59
|
+
return <Fragment key={`remount-${key}`}>{props.children}</Fragment>;
|
|
60
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
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 { Fragment, createContext, useCallback, useContext, useEffect, useRef, useState, } from 'react';
|
|
27
|
+
const ResetContext = createContext(null);
|
|
28
|
+
function useResetRequest(onReset) {
|
|
29
|
+
const [generation, setGeneration] = useState(0);
|
|
30
|
+
const onResetRef = useRef(onReset);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
onResetRef.current = onReset;
|
|
33
|
+
});
|
|
34
|
+
const request = useCallback((...args) => {
|
|
35
|
+
setGeneration((g) => g + 1);
|
|
36
|
+
onResetRef.current?.(...args);
|
|
37
|
+
}, []);
|
|
38
|
+
return { generation, request };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Imperative reset from below: any descendant can call the function returned
|
|
42
|
+
* by {@link useResetBoundary} to unmount and freshly mount everything inside
|
|
43
|
+
* the nearest boundary — state, refs, effects, DOM.
|
|
44
|
+
*
|
|
45
|
+
* Handy for "start over" buttons, clearing a wizard after submit, or error
|
|
46
|
+
* recovery, without the parent knowing anything about it:
|
|
47
|
+
*
|
|
48
|
+
* ```tsx
|
|
49
|
+
* <ResetBoundary>
|
|
50
|
+
* <CheckoutWizard />
|
|
51
|
+
* </ResetBoundary>
|
|
52
|
+
*
|
|
53
|
+
* // deep inside CheckoutWizard:
|
|
54
|
+
* const reset = useResetBoundary();
|
|
55
|
+
* <button onClick={reset}>Start over</button>
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* Boundaries nest; `useResetBoundary` targets the nearest one.
|
|
59
|
+
* @param props - Subtree and optional synchronous reset observer.
|
|
60
|
+
* @returns A resettable subtree accessible through {@link useResetBoundary}.
|
|
61
|
+
*/
|
|
62
|
+
export function ResetBoundary({ children, onReset }) {
|
|
63
|
+
const { generation, request } = useResetRequest(onReset);
|
|
64
|
+
// Preserve the zero-argument API, including when used as onClick={reset}.
|
|
65
|
+
const reset = useCallback(() => request(), [request]);
|
|
66
|
+
return (<ResetContext.Provider value={reset}>
|
|
67
|
+
<Fragment key={`reset-${generation}`}>{children}</Fragment>
|
|
68
|
+
</ResetContext.Provider>);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Returns the nearest {@link ResetBoundary}'s reset function.
|
|
72
|
+
* @returns A stable, zero-argument function that remounts the boundary's entire subtree.
|
|
73
|
+
* @throws If no shared {@link ResetBoundary} is above the caller. Factory boundaries use their own hook.
|
|
74
|
+
*/
|
|
75
|
+
export function useResetBoundary() {
|
|
76
|
+
const reset = useContext(ResetContext);
|
|
77
|
+
if (reset === null) {
|
|
78
|
+
throw new Error('react-fresh-key: useResetBoundary must be used inside a <ResetBoundary>.');
|
|
79
|
+
}
|
|
80
|
+
return reset;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Creates a boundary and registration hook that share a typed reset contract.
|
|
84
|
+
* Create it once at module scope; aliases and destructured exports are safe.
|
|
85
|
+
*
|
|
86
|
+
* ```tsx
|
|
87
|
+
* const checkoutReset = createResetBoundary<{
|
|
88
|
+
* restart: { step: number }
|
|
89
|
+
* completed: { orderId: string }
|
|
90
|
+
* dismissed: void
|
|
91
|
+
* }>()
|
|
92
|
+
* export const ClientResetBoundary = checkoutReset.ResetBoundary
|
|
93
|
+
* export const useResetClientRegistration = checkoutReset.useResetBoundary
|
|
94
|
+
*
|
|
95
|
+
* // Inside a descendant of ClientResetBoundary:
|
|
96
|
+
* const reset = useResetClientRegistration('restart')
|
|
97
|
+
* // In an event handler:
|
|
98
|
+
* reset({ step: 2 })
|
|
99
|
+
* ```
|
|
100
|
+
*
|
|
101
|
+
* The hook targets the nearest boundary created by this factory call. It
|
|
102
|
+
* captures a registration key without subscriptions or registration effects.
|
|
103
|
+
* The reset function is stable while that key and provider remain unchanged.
|
|
104
|
+
* Payloads are forwarded unchanged to `onReset({ key, payload })`.
|
|
105
|
+
* @returns A matching boundary and hook. Export either under an application-specific name without binding.
|
|
106
|
+
*/
|
|
107
|
+
export function createResetBoundary() {
|
|
108
|
+
const Context = createContext(null);
|
|
109
|
+
/**
|
|
110
|
+
* Provides this factory's reset scope; its subtree remounts when the matching hook requests it.
|
|
111
|
+
* @param props - Children and optional synchronous `onReset` request observer.
|
|
112
|
+
*/
|
|
113
|
+
function TypedResetBoundary({ children, onReset, }) {
|
|
114
|
+
const { generation, request } = useResetRequest(onReset);
|
|
115
|
+
return (<Context.Provider value={request}>
|
|
116
|
+
<Fragment key={`reset-${generation}`}>{children}</Fragment>
|
|
117
|
+
</Context.Provider>);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Selects a reset action from the nearest boundary created by this factory.
|
|
121
|
+
* @param key - Registration key included in each request's `{ key, payload }` event.
|
|
122
|
+
* @returns A reset callback stable while the key and provider are unchanged.
|
|
123
|
+
* @throws If no boundary from this factory is above the caller.
|
|
124
|
+
*/
|
|
125
|
+
function useRegisteredReset(key) {
|
|
126
|
+
const request = useContext(Context);
|
|
127
|
+
const reset = useCallback(
|
|
128
|
+
/**
|
|
129
|
+
* Requests a remount and forwards this registration's key and payload to `onReset`.
|
|
130
|
+
* @param payload - Request metadata forwarded unchanged to the matching boundary.
|
|
131
|
+
*/
|
|
132
|
+
(payload) => {
|
|
133
|
+
// The public signature relates each key to its own payload. TypeScript
|
|
134
|
+
// cannot express that correlation when constructing a generic union.
|
|
135
|
+
request?.({ key, payload });
|
|
136
|
+
}, [request, key]);
|
|
137
|
+
if (request === null) {
|
|
138
|
+
throw new Error('react-fresh-key: useResetBoundary must be used inside a boundary from the same createResetBoundary() call.');
|
|
139
|
+
}
|
|
140
|
+
return reset;
|
|
141
|
+
}
|
|
142
|
+
return { ResetBoundary: TypedResetBoundary, useResetBoundary: useRegisteredReset };
|
|
143
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
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 { useState } from 'react';
|
|
27
|
+
import { areDepsEqual, isEqualDep } from './utils';
|
|
28
|
+
/**
|
|
29
|
+
* Returns a numeric key that changes whenever any value in `deps` changes.
|
|
30
|
+
* Values are compared with `Object.is` — like React's own dependency
|
|
31
|
+
* comparison — except that plain objects and ordinary arrays are compared
|
|
32
|
+
* one level deep. Distinct objects with equal content preserve the key;
|
|
33
|
+
* `[{ id }]` written inline therefore converges instead of looping.
|
|
34
|
+
*
|
|
35
|
+
* A hook cannot remount the component it runs in, so apply the key to an
|
|
36
|
+
* inner subtree:
|
|
37
|
+
*
|
|
38
|
+
* ```tsx
|
|
39
|
+
* function Profile({ userId }: { userId: string }) {
|
|
40
|
+
* const key = useRemountKey([userId])
|
|
41
|
+
* return <ProfileForm key={key} userId={userId} />
|
|
42
|
+
* }
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* The key is derived purely during render (the setState-in-render
|
|
46
|
+
* "derived state" pattern), so it is SSR-deterministic and StrictMode-safe.
|
|
47
|
+
* In the render where deps change, the *new* key is returned immediately.
|
|
48
|
+
*
|
|
49
|
+
* **Stability contract** — put primitives, references you already hold
|
|
50
|
+
* (props, state, memoized values), or flat plain objects/arrays in `deps`.
|
|
51
|
+
* A value that is a *new, non-equal* reference on every render — a nested
|
|
52
|
+
* object literal, `new Map()`, `new Date()`, a class instance built inline —
|
|
53
|
+
* can never compare equal and will trip React's re-render limit. Hold such
|
|
54
|
+
* values in state or `useMemo` first.
|
|
55
|
+
*
|
|
56
|
+
* @param deps Watched values, compared using {@link DepsList} rules. Treat the
|
|
57
|
+
* list and its values as immutable; changing their contents in place is not
|
|
58
|
+
* detected reliably. An empty list keeps the initial key.
|
|
59
|
+
* @returns A key local to this hook instance, starting at `0` and increasing
|
|
60
|
+
* when dependencies change. Apply it to a descendant element's `key` prop.
|
|
61
|
+
*/
|
|
62
|
+
export function useRemountKey(deps) {
|
|
63
|
+
const [state, setState] = useState({
|
|
64
|
+
deps,
|
|
65
|
+
key: 0,
|
|
66
|
+
});
|
|
67
|
+
if (!areDepsEqual(state.deps, deps)) {
|
|
68
|
+
// Documented React pattern: setting state during render restarts the
|
|
69
|
+
// render immediately with the new state, before touching the DOM.
|
|
70
|
+
const key = state.key + 1;
|
|
71
|
+
setState({ deps, key });
|
|
72
|
+
return key;
|
|
73
|
+
}
|
|
74
|
+
return state.key;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Predicate flavor of {@link useRemountKey}: tracks an arbitrary `value` and
|
|
78
|
+
* bumps the key when `when(prev, next)` returns `true` for a changed value.
|
|
79
|
+
*
|
|
80
|
+
* `prev` is the last recorded value. Whenever a changed value is observed,
|
|
81
|
+
* the snapshot advances even if the predicate returns `false`. Equal values
|
|
82
|
+
* retain the existing snapshot and skip the predicate. Equality uses
|
|
83
|
+
* `Object.is`, or a one-level shallow comparison for plain objects and ordinary
|
|
84
|
+
* arrays, so a flat inline object (`useRemountKeyWhen({ id, tab }, …)`) converges.
|
|
85
|
+
*
|
|
86
|
+
* ```tsx
|
|
87
|
+
* const key = useRemountKeyWhen(user, (prev, next) => prev.id !== next.id)
|
|
88
|
+
* ```
|
|
89
|
+
*
|
|
90
|
+
* **Stability contract**: the same as {@link useRemountKey} — a value that is
|
|
91
|
+
* a new, non-equal reference on every render (nested literals, Map, Date,
|
|
92
|
+
* class instances built inline) cannot converge; hold it in state or `useMemo`.
|
|
93
|
+
*
|
|
94
|
+
* @param value The immutable value to track. Replace changed objects instead
|
|
95
|
+
* of mutating them in place.
|
|
96
|
+
* @param when A pure render-time predicate comparing the last recorded value
|
|
97
|
+
* with the new value. It is skipped on initial render and for equal values;
|
|
98
|
+
* changing only the predicate does not trigger a comparison.
|
|
99
|
+
* @returns A key starting at `0`, increased when a changed value satisfies
|
|
100
|
+
* `when`. Apply it to a descendant element; it cannot remount this hook's owner.
|
|
101
|
+
*/
|
|
102
|
+
export function useRemountKeyWhen(value, when) {
|
|
103
|
+
const [state, setState] = useState({
|
|
104
|
+
value,
|
|
105
|
+
key: 0,
|
|
106
|
+
});
|
|
107
|
+
if (!isEqualDep(state.value, value)) {
|
|
108
|
+
const key = when(state.value, value) ? state.key + 1 : state.key;
|
|
109
|
+
setState({ value, key });
|
|
110
|
+
return key;
|
|
111
|
+
}
|
|
112
|
+
return state.key;
|
|
113
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
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 { useCallback, useState } from 'react';
|
|
27
|
+
import { areDepsEqual } from './utils';
|
|
28
|
+
function resolveInitial(initial) {
|
|
29
|
+
return typeof initial === 'function' ? initial() : initial;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Like `useState`, but the state snaps back to `initial` whenever any value
|
|
33
|
+
* in `deps` changes.
|
|
34
|
+
*
|
|
35
|
+
* This is the cheap alternative to a full remount: only this piece of state
|
|
36
|
+
* resets — DOM state (focus, scroll, uncontrolled inputs), refs, and effects
|
|
37
|
+
* are untouched. Reach for this first; remount only when you need everything
|
|
38
|
+
* gone.
|
|
39
|
+
*
|
|
40
|
+
* ```tsx
|
|
41
|
+
* function Comment({ postId }: { postId: string }) {
|
|
42
|
+
* const [draft, setDraft] = useResettableState('', [postId])
|
|
43
|
+
* // draft clears when the user navigates to another post
|
|
44
|
+
* }
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* Reset happens purely during render (no effect, no flash of stale state),
|
|
48
|
+
* and the render in which deps change already sees the fresh value — so
|
|
49
|
+
* code after the hook never observes state that belongs to the old deps.
|
|
50
|
+
*
|
|
51
|
+
* `deps` follow {@link DepsList} comparison rules: `Object.is`, with plain
|
|
52
|
+
* objects and ordinary arrays compared one level deep. Dependencies must be
|
|
53
|
+
* able to compare equal across rerenders: nested literals or new class
|
|
54
|
+
* instances created on every render can cause a re-render loop. Keep those
|
|
55
|
+
* values in state or memoize them, and replace changed values rather than
|
|
56
|
+
* mutating them in place.
|
|
57
|
+
*
|
|
58
|
+
* @param initial The value, or a pure lazy initializer, used on mount and on
|
|
59
|
+
* each dependency change. Resets use the current render's `initial`; changing
|
|
60
|
+
* only this argument does not reset existing state. Wrap a function-valued
|
|
61
|
+
* initial state in an initializer, as with React's `useState`.
|
|
62
|
+
* @param deps Immutable watched values that determine when to reset. An empty
|
|
63
|
+
* list disables dependency-driven resets.
|
|
64
|
+
* @returns The current state and a stable setter accepting either the next
|
|
65
|
+
* value or a pure updater function. A dependency change returns fresh state
|
|
66
|
+
* in the same render, without remounting the component.
|
|
67
|
+
*/
|
|
68
|
+
export function useResettableState(initial, deps) {
|
|
69
|
+
const [state, setState] = useState(() => ({
|
|
70
|
+
deps,
|
|
71
|
+
value: resolveInitial(initial),
|
|
72
|
+
}));
|
|
73
|
+
const setValue = useCallback((action) => {
|
|
74
|
+
setState((s) => ({
|
|
75
|
+
deps: s.deps,
|
|
76
|
+
value: typeof action === 'function' ? action(s.value) : action,
|
|
77
|
+
}));
|
|
78
|
+
}, []);
|
|
79
|
+
if (!areDepsEqual(state.deps, deps)) {
|
|
80
|
+
const value = resolveInitial(initial);
|
|
81
|
+
setState({ deps, value });
|
|
82
|
+
return [value, setValue];
|
|
83
|
+
}
|
|
84
|
+
return [state.value, setValue];
|
|
85
|
+
}
|