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 ADDED
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ Changes are recorded here before each release. During `0.x`, patches preserve
4
+ the public API; minor releases may contain explicitly documented breaking changes.
5
+
6
+ ## 0.1.0 — 2026-09-11
7
+
8
+ ### Added
9
+
10
+ - Declarative remount rules through `withRemount`, `Remount`, and the key hooks.
11
+ - `useResettableState` for resetting a state value while preserving the component.
12
+ - Shared and typed reset boundaries, including named reset requests and payloads.
13
+ - TypeScript declarations and API documentation in editor tooltips.
14
+ - ESM and CommonJS package builds with React 16.14 and later support.
15
+ - A copy-in CLI with TypeScript and JavaScript modules, direct imports, and a
16
+ complete MIT notice in every copied file.
17
+
18
+ The copy-in JSX modules require the automatic JSX transform. See the README
19
+ for component compatibility and comparison semantics.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rafael Buzatto de Campos
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,475 @@
1
+ # react-fresh-key
2
+
3
+ **React remount and state reset utilities.**
4
+
5
+ Reset component state when selected props change, or let a descendant reset an entire subtree. Define the reset rule alongside a reusable component so its callers can use it consistently.
6
+
7
+ TypeScript support · No runtime dependencies beyond React · Copy into your project or install from npm
8
+
9
+ Runtime: React 16.14 and later. Types are checked against React 17, 18, and 19.
10
+
11
+ ## Why?
12
+
13
+ When a component needs a fresh start after a prop changes, it's tempting to reset its state in `useEffect`. This is a discouraged practice by React's team. For a full reset, [React recommends changing the component's `key`](https://react.dev/learn/you-might-not-need-an-effect#resetting-all-state-when-a-prop-changes):
14
+
15
+ ```tsx
16
+ <Editor key={documentId} documentId={documentId} />
17
+ ```
18
+
19
+ That's better, but putting the `key` at every call site makes the parent responsible for knowing **when the child's internals need a reset**. If Editor later needs a fresh mount when `mode` changes too, every parent must be updated. The component's internal reset requirements have become a concern for all of its consumers.
20
+
21
+ `react-fresh-key` brings that responsibility back to where it belongs. Declare the reset rule alongside the component; callers pass its normal props. `withRemount` handles the keyed boundary, so changes to the reset rule stay with the component that needs them.
22
+
23
+ ## Quick start
24
+
25
+ Copy the HOC into an existing React project:
26
+
27
+ ```bash
28
+ npx react-fresh-key add with-remount
29
+ # Or with pnpm:
30
+ pnpm dlx react-fresh-key add with-remount
31
+ ```
32
+
33
+ In a TypeScript project, put this example in `src/App.tsx`:
34
+
35
+ ```tsx
36
+ import { useState } from 'react'
37
+ import { withRemount } from './lib/fresh-key/withRemount'
38
+
39
+ type EditorProps = { documentId: string }
40
+
41
+ function Editor({ documentId }: EditorProps) {
42
+ const [draft, setDraft] = useState('')
43
+
44
+ return (
45
+ <label>
46
+ Draft for {documentId}
47
+ <textarea
48
+ value={draft}
49
+ onChange={(event) => setDraft(event.target.value)}
50
+ />
51
+ </label>
52
+ )
53
+ }
54
+
55
+ // Create the wrapper once, outside component rendering.
56
+ const DocumentEditor = withRemount(Editor, ['documentId'])
57
+
58
+ export default function App() {
59
+ const [documentId, setDocumentId] = useState('intro')
60
+
61
+ return (
62
+ <>
63
+ <button
64
+ onClick={() => setDocumentId((id) => id === 'intro' ? 'notes' : 'intro')}
65
+ >
66
+ Switch document
67
+ </button>
68
+ <DocumentEditor documentId={documentId} />
69
+ </>
70
+ )
71
+ }
72
+ ```
73
+
74
+ Type a draft, then switch documents. The editor mounts fresh and clears its draft each time the document changes. Re-rendering with the same `documentId` preserves the draft.
75
+
76
+ For a package installation, use `npm install react-fresh-key` or `pnpm add react-fresh-key` and change the library import to `import { withRemount } from 'react-fresh-key'`.
77
+
78
+ ## Choosing a reset
79
+
80
+ When the parent owns the identity decision, React's `key` prop is often enough. Otherwise, choose based on what needs to reset and where the reset decision belongs:
81
+
82
+ | What needs to reset? | Use |
83
+ | --- | --- |
84
+ | One state value, while keeping the component mounted | `useResettableState` |
85
+ | A whole component, with its rule declared at the definition site | `withRemount` |
86
+ | An explicit subtree | `<Remount>` |
87
+ | A subtree whose `key` you want to apply yourself | `useRemountKey` or `useRemountKeyWhen` |
88
+ | A subtree, when one of its descendants requests it | `<ResetBoundary>` and `useResetBoundary` |
89
+ | A subtree with named, typed reset requests or its own reset context | `createResetBoundary<Events>()` |
90
+ | Identity controlled by the parent | React's `key` prop |
91
+
92
+ **A remount discards local React state, refs, and DOM state throughout the affected subtree.** Effects clean up and run again. Focus, uncontrolled input values, and scroll position can be lost. Use `useResettableState` when resetting one value is enough.
93
+
94
+ ## Installation
95
+
96
+ ### Copy the source into your project
97
+
98
+ The copy-in CLI is useful when you want to own and adapt these small utilities:
99
+
100
+ ```bash
101
+ # Choose individual pieces; their local dependencies are included.
102
+ npx react-fresh-key add with-remount reset-boundary --dir src/lib/fresh-key
103
+
104
+ # Or copy every public API.
105
+ npx react-fresh-key add all
106
+
107
+ # List the available pieces.
108
+ npx react-fresh-key list
109
+ ```
110
+
111
+ The same commands work with `pnpm dlx` in place of `npx`.
112
+
113
+ The CLI requires Node.js 18 or later. It chooses TypeScript when the directory you run it from contains `tsconfig.json`; otherwise it emits JavaScript. Use `--ts` or `--js` to choose explicitly.
114
+
115
+ Copied `.tsx` and `.jsx` modules require the **automatic JSX transform**. For TypeScript, use `"jsx": "react-jsx"` in `tsconfig.json`, or `"jsx": "preserve"` when your framework handles the automatic transform. The npm builds already have JSX compiled and do not require this setting.
116
+
117
+ | Option | Behavior |
118
+ | --- | --- |
119
+ | `--dir`, `-d` | Output directory; defaults to `src/lib/fresh-key` |
120
+ | `--ts` / `--js` | Choose TypeScript or JavaScript output |
121
+ | `--force`, `-f` | Replace existing source files |
122
+ | `--dry-run` | Preview the operation without writing files |
123
+
124
+ The CLI copies the selected source modules and their local dependencies. Existing source files are skipped unless you use `--force`. It does not create an index file; existing index files, including those generated by earlier versions, are left untouched.
125
+
126
+ Each copied module includes the MIT license notice. Keep that notice when editing or redistributing the code.
127
+
128
+ Import directly from each module using a path relative to your file, or your project's configured alias. For example, from `src/App.tsx` with the default output location:
129
+
130
+ ```tsx
131
+ import { withRemount } from './lib/fresh-key/withRemount'
132
+ import { ResetBoundary, useResetBoundary } from './lib/fresh-key/ResetBoundary'
133
+ ```
134
+
135
+ ### Install as a dependency
136
+
137
+ Use a package dependency when you want to manage updates through your package manager:
138
+
139
+ ```bash
140
+ npm install react-fresh-key
141
+ # Or with pnpm:
142
+ pnpm add react-fresh-key
143
+ ```
144
+
145
+ ```tsx
146
+ import { withRemount, useResettableState } from 'react-fresh-key'
147
+ ```
148
+
149
+ Both installation paths use the same implementation. A copy-in installation exposes the pieces you selected; use `add all` for the full API. The examples below use package imports; for a copy-in installation, copy the relevant pieces and import each API from its local module.
150
+
151
+ ## API
152
+
153
+ ### `withRemount(Component, watch)`
154
+
155
+ Returns a component that remounts its wrapped component when the watch rule detects a change. Props continue to reach the wrapped component on ordinary updates.
156
+
157
+ Using `Editor` from the quick start:
158
+
159
+ ```tsx
160
+ import { withRemount } from 'react-fresh-key'
161
+
162
+ const DocumentEditor = withRemount(Editor, ['documentId'])
163
+ ```
164
+
165
+ Watch prop names, select a value or dependency list, or supply an explicit predicate:
166
+
167
+ ```tsx
168
+ withRemount(Editor, ['documentId'])
169
+ withRemount(Editor, (props) => props.documentId)
170
+ withRemount(Editor, { select: (props) => [props.documentId] })
171
+ withRemount(Editor, {
172
+ when: (previous, next) => previous.documentId !== next.documentId,
173
+ })
174
+ ```
175
+
176
+ A bare function is a selector. Use `{ when }` for predicates; a bare two-argument function is rejected. Selectors and prop-name lists use the [comparison rules](#comparison-rules) below.
177
+
178
+ **Create the wrapper outside rendering.** Calling `withRemount` inside another component creates a new component type on each render and defeats state preservation.
179
+
180
+ Selectors and predicates run during rendering. Keep them pure: do not send analytics, mutate inputs, or perform other side effects inside them. React may evaluate them more than once.
181
+
182
+ Refs are forwarded when the wrapped component supports them. A parent can also supply a `key`; changing it causes an additional remount. A stable parent key does not disable the wrapper's own reset rule. See [TypeScript and component compatibility](#typescript-and-component-compatibility) for defaults, lazy components, and generics.
183
+
184
+ ### `<Remount>`
185
+
186
+ Remounts its children when its dependencies change. The component containing the boundary stays mounted.
187
+
188
+ Using the same `Editor`:
189
+
190
+ ```tsx
191
+ import { Remount } from 'react-fresh-key'
192
+
193
+ function EditorPanel({ documentId }: EditorProps) {
194
+ return (
195
+ <Remount deps={[documentId]}>
196
+ <Editor documentId={documentId} />
197
+ </Remount>
198
+ )
199
+ }
200
+ ```
201
+
202
+ For a predicate, provide both `watch` and `when`:
203
+
204
+ ```tsx
205
+ function ConditionalEditorPanel({ documentId }: EditorProps) {
206
+ return (
207
+ <Remount
208
+ watch={documentId}
209
+ when={(previous, next) => previous !== next}
210
+ >
211
+ <Editor documentId={documentId} />
212
+ </Remount>
213
+ )
214
+ }
215
+ ```
216
+
217
+ Choose either `deps` or `watch` + `when`, and keep that mode for the lifetime of the boundary. A boundary can contain multiple children; they remount together.
218
+
219
+ ### `useRemountKey(deps)` and `useRemountKeyWhen(value, when)`
220
+
221
+ Return a numeric key to apply to a child. A hook cannot remount the component calling it; put the key on the subtree you want to reset.
222
+
223
+ ```tsx
224
+ import { useRemountKey, useRemountKeyWhen } from 'react-fresh-key'
225
+
226
+ function EditorWithKey({ documentId }: EditorProps) {
227
+ const key = useRemountKey([documentId])
228
+ return <Editor key={key} documentId={documentId} />
229
+ }
230
+
231
+ function EditorWithPredicate({ documentId }: EditorProps) {
232
+ const key = useRemountKeyWhen(
233
+ documentId,
234
+ (previous, next) => previous !== next
235
+ )
236
+ return <Editor key={key} documentId={documentId} />
237
+ }
238
+ ```
239
+
240
+ The key starts at `0`. When a reset is detected, the hook returns the new key in that render. Follow the [comparison and input-stability rules](#comparison-rules) when passing objects or arrays directly to these hooks.
241
+
242
+ ### `useResettableState(initial, deps)`
243
+
244
+ Returns `[value, setValue]`. When dependencies change, the value resets to the current `initial` value without remounting the component.
245
+
246
+ ```tsx
247
+ import { useResettableState } from 'react-fresh-key'
248
+
249
+ function CommentBox({ postId }: { postId: string }) {
250
+ const [draft, setDraft] = useResettableState('', [postId])
251
+
252
+ return (
253
+ <textarea
254
+ aria-label="Comment"
255
+ value={draft}
256
+ onChange={(event) => setDraft(event.target.value)}
257
+ />
258
+ )
259
+ }
260
+ ```
261
+
262
+ Changing `postId` clears the draft while preserving the mounted textarea and other component state. Functional updates such as `setDraft((draft) => draft + '!')` are supported, and the setter has a stable identity.
263
+
264
+ A lazy initializer, such as `useResettableState(() => createDraft(), [postId])`, runs on initialization and when dependencies trigger a reset. It must be pure and may run more than once. Changing `initial` alone does not reset the state.
265
+
266
+ ### `<ResetBoundary>` and `useResetBoundary()`
267
+
268
+ A descendant calls the returned `reset` function to remount everything inside the nearest boundary:
269
+
270
+ ```tsx
271
+ import { useState } from 'react'
272
+ import { ResetBoundary, useResetBoundary } from 'react-fresh-key'
273
+
274
+ function Wizard() {
275
+ const [step, setStep] = useState(1)
276
+ const reset = useResetBoundary()
277
+
278
+ return (
279
+ <>
280
+ <p>Step {step}</p>
281
+ <button onClick={() => setStep((step) => step + 1)}>Next</button>
282
+ <button onClick={reset}>Start over</button>
283
+ </>
284
+ )
285
+ }
286
+
287
+ export function Checkout() {
288
+ return (
289
+ <ResetBoundary onReset={() => console.log('Restart requested')}>
290
+ <Wizard />
291
+ </ResetBoundary>
292
+ )
293
+ }
294
+ ```
295
+
296
+ Boundaries can nest, and `reset` has a stable identity. Calling `useResetBoundary` outside a boundary throws.
297
+
298
+ `onReset` runs synchronously as part of the reset request. Use it to observe requests, such as for analytics; it is not a notification that the new subtree has mounted. Put focus setup for the newly mounted UI in that UI's own mount logic.
299
+
300
+ ### `createResetBoundary<Events>()`
301
+
302
+ Creates a boundary and hook that share their own context. Define an event map whose keys name reset requests and whose values describe their payloads. Create the factory once at module scope, then export the boundary and hook under names that fit your feature:
303
+
304
+ ```tsx
305
+ // clientReset.ts
306
+ import { createResetBoundary } from 'react-fresh-key'
307
+
308
+ const checkoutReset = createResetBoundary<{
309
+ restart: { step: number }
310
+ clear: void
311
+ }>()
312
+
313
+ export const ClientResetBoundary = checkoutReset.ResetBoundary
314
+ export const useResetClientRegistration = checkoutReset.useResetBoundary
315
+ ```
316
+
317
+ A descendant selects a request key by calling the hook. The returned function accepts that key's payload:
318
+
319
+ ```tsx
320
+ // ClientRegistration.tsx
321
+ import { useState } from 'react'
322
+ import { ClientResetBoundary, useResetClientRegistration } from './clientReset'
323
+
324
+ function RegistrationSteps() {
325
+ const [step, setStep] = useState(1)
326
+ const reset = useResetClientRegistration('restart')
327
+ const clear = useResetClientRegistration('clear')
328
+
329
+ return (
330
+ <>
331
+ <p>Step {step}</p>
332
+ <button onClick={() => setStep((step) => step + 1)}>Next</button>
333
+ <button onClick={() => reset({ step })}>Restart</button>
334
+ <button onClick={() => clear()}>Clear</button>
335
+ </>
336
+ )
337
+ }
338
+
339
+ export function ClientRegistration() {
340
+ return (
341
+ <ClientResetBoundary
342
+ onReset={(event) => {
343
+ if (event.key === 'restart') {
344
+ console.log('Restart requested from step', event.payload.step)
345
+ } else {
346
+ console.log('Clear requested')
347
+ }
348
+ }}
349
+ >
350
+ <RegistrationSteps />
351
+ </ClientResetBoundary>
352
+ )
353
+ }
354
+ ```
355
+
356
+ `onReset` receives a discriminated `{ key, payload }` event: checking `key` narrows the payload type. A required payload must be supplied; a payload type of `void`, `undefined`, or a union including `undefined` allows a call without arguments. Use arrow handlers as above to supply the intended payload explicitly. If the key is a union of several possible actions, the payload must be valid for every possible action.
357
+
358
+ Each factory is isolated. Its hook targets the nearest boundary from **that same factory**, and throws if there is none; a boundary from another factory or the global `ResetBoundary` does not satisfy it. Exporting an alias or destructuring the returned hook is safe, with no binding required. The reset callback stays stable while its registered key and provider stay unchanged.
359
+
360
+ Payloads are request metadata passed unchanged to `onReset`. The callback runs synchronously as part of the request, just like the global boundary. The global `ResetBoundary` and `useResetBoundary()` keep their zero-argument API, including `onClick={reset}`.
361
+
362
+ For copy-in installations, `createResetBoundary` is included in the existing `reset-boundary` piece and imported directly from its `ResetBoundary` module.
363
+
364
+ ## Comparison rules
365
+
366
+ React uses `Object.is` for its dependency arrays. This library extends that comparison for plain objects and ordinary arrays:
367
+
368
+ | Watched value | Comparison |
369
+ | --- | --- |
370
+ | Primitives | `Object.is`, including its handling of `NaN` and signed zero |
371
+ | Plain objects, including objects with a null prototype | Same own enumerable properties, compared one level deep with `Object.is`; symbol keys are included |
372
+ | Ordinary arrays | Same length and own enumerable properties, compared one level deep; extra properties and symbol keys are included |
373
+ | Functions, `Map`, `Set`, `Date`, and class instances, including Array subclasses | Reference identity |
374
+
375
+ **Distinct flat objects with equal content preserve state.** This is an intentional reset-semantics choice: a new object reference alone may trigger a React effect but will not necessarily trigger a reset here. Treat watched values as immutable; mutating an existing reference is not detected.
376
+
377
+ A selector that returns an array supplies a dependency list. Each dependency is compared using the table above; any other selector result is treated as one watched value.
378
+
379
+ For `useRemountKeyWhen` and `<Remount watch={...} when={...}>`, the predicate runs when the watched value differs under these rules. For `withRemount`'s `{ when }` form, it runs when props differ shallowly, using `Object.is` per prop. Predicates receive the previous tracked snapshot and incoming input. The snapshot advances on a detected change even when the predicate returns `false`; it is not tied to the last remount.
380
+
381
+ ### Inputs to hooks must be able to compare equal
382
+
383
+ Flat literals can be written directly inside a hook call:
384
+
385
+ ```tsx
386
+ useRemountKey([documentId])
387
+ useRemountKey([{ id: documentId }])
388
+ useRemountKeyWhen({ id: documentId }, (previous, next) => previous.id !== next.id)
389
+ ```
390
+
391
+ Values with a fresh nested reference on every render cannot settle:
392
+
393
+ ```tsx
394
+ // Avoid creating these inside the hook call on every render.
395
+ useRemountKey([{ document: { id: documentId } }])
396
+ useRemountKey([new Date()])
397
+ ```
398
+
399
+ These inputs can cause React's “Too many re-renders” error. Watch the relevant primitives, or use an existing reference from props, state, or a memoized value. The same constraint applies to `useResettableState` dependencies.
400
+
401
+ `<Remount>` receives its inputs as props, which remain stable during its internal render retry. `withRemount` uses the props object to anchor its retry. Fresh nested values therefore do not create this internal loop in those components, but can cause remounts on each parent update.
402
+
403
+ ## TypeScript and component compatibility
404
+
405
+ Props and ref types are inferred from the wrapped component. The package exposes both ESM and CommonJS builds with corresponding TypeScript declarations.
406
+
407
+ | Component | `withRemount` behavior |
408
+ | --- | --- |
409
+ | Function or class component | Preserves declared props and supported ref types |
410
+ | Class with `defaultProps`, including through `memo` | Defaulted props stay optional at call sites; selectors and predicates receive those defaults |
411
+ | `lazy` component, including through nested `memo` | Requires props as declared, because inner defaults are unavailable before the lazy module loads |
412
+ | Generic component | Does not preserve the generic type parameter; place `<Remount>` inside the generic component to retain inference |
413
+
414
+ JavaScript parameter defaults inside a component run when that component renders; the wrapper cannot evaluate them for a selector. Selectors should handle any optional props they receive.
415
+
416
+ The public types are `DepsList`, `WatchSpec`, `RemountedProps`, `RemountProps`, `RemountDepsProps`, `RemountWhenProps`, `ResetBoundaryProps`, `ResetEvent<Events>`, and `TypedResetBoundaryProps<Events>`.
417
+
418
+ Server rendering, hydration, StrictMode, and interrupted transitions are covered by tests. Server and client inputs and initializers must still produce consistent output. In frameworks using React Server Components, consume these utilities behind a `'use client'` boundary.
419
+
420
+ ## How resets work
421
+
422
+ The declarative remount APIs store a snapshot of watched inputs and a numeric key. When the watch rule calls for a remount, they update the key in their own state during rendering and return the new key immediately. React restarts that render before committing the keyed subtree. This follows React's documented [pattern for storing information from previous renders](https://react.dev/reference/react/useState#storing-information-from-previous-renders).
423
+
424
+ `useResettableState` uses the same snapshot pattern to replace only its state value. Global and factory-created reset boundaries change their key when a descendant calls their reset function; typed request keys identify the event sent to `onReset`, while the whole boundary subtree remounts.
425
+
426
+ Dependency-driven resets are not scheduled from an effect, avoiding an intermediate commit with state from the previous inputs. Ordinary component effects still follow React's lifecycle, and remounting still runs effect cleanup and setup.
427
+
428
+ ## Contributing
429
+
430
+ Use Node.js 24 and pnpm 10.27.0 for development and release checks. The pnpm version is pinned in `package.json`; [install pnpm](https://pnpm.io/10.x/installation) before running the commands below. The installed package and CLI support Node.js 18 and later.
431
+
432
+ ```bash
433
+ pnpm install --frozen-lockfile
434
+ pnpm run check
435
+ # Also verify the packed package against real React installations (needs network access).
436
+ pnpm run check:release
437
+ ```
438
+
439
+ Biome handles linting and formatting for maintained JavaScript, TypeScript, JSX, TSX, and JSON files, omitting optional JavaScript and TypeScript semicolons. `.editorconfig` keeps editors aligned with these formatting defaults, including two-space indentation and LF line endings.
440
+
441
+ | Command | Behavior |
442
+ | --- | --- |
443
+ | `pnpm run lint` | Report lint findings without changing files |
444
+ | `pnpm run lint:fix` | Apply safe lint fixes; review any remaining findings manually |
445
+ | `pnpm run format` | Write formatting changes |
446
+ | `pnpm run format:check` | Check formatting without changing files |
447
+ | `pnpm run check:style` | Run the combined lint and formatting check with `biome ci --error-on-warnings .` |
448
+
449
+ Generated `dist/` and `templates/`, dependency and store directories, `.tmp-vendor/`, and `pnpm-lock.yaml` are excluded from Biome. TypeScript checks remain part of validation: linting does not replace the API type assertions or React compatibility checks.
450
+
451
+ `pnpm run check` includes `check:style`, type checks, tests, a build, and export checks. CI runs that same command; release checks and the `preversion` and `prepublishOnly` hooks include it too.
452
+
453
+ `pnpm run test` includes runtime tests, type assertions, React 17/18/19 type compatibility, SSR/hydration, interrupted transitions, and CLI integration. Use `pnpm run test:types` to run only the type suite. Check the command's exit status and reported type errors as well as the passed-test count.
454
+
455
+ | Path | Purpose |
456
+ | --- | --- |
457
+ | `src/` | Library source |
458
+ | `bin/cli.mjs` | Copy-in CLI |
459
+ | `registry.json` | Pieces, dependencies, and public exports |
460
+ | `scripts/build-templates.mjs` | Generates TypeScript and JavaScript templates from source |
461
+ | `test/` | Runtime, type, compatibility, and distribution tests |
462
+
463
+ Templates are generated during the build. The CLI tests typecheck and run direct imports from copied modules, check that every public API is available, and verify that the registry matches the package's public exports. Package declarations and export resolution are checked with [Are the types wrong?](https://arethetypeswrong.github.io/).
464
+
465
+ `pnpm run check:release` also installs the packed artifact into isolated consumers with React 16.14, 17, 18, and 19, then checks native ESM, CommonJS, server rendering, and the installed CLI. After a build, run one version with `pnpm run check:package --react 17.0.2`. CI installs from `pnpm-lock.yaml` with `--frozen-lockfile` and checks packed consumers on Node.js 18 and 24.
466
+
467
+ Versions follow [Semantic Versioning](https://semver.org/). During `0.x`, patches preserve the public API; minor releases may introduce breaking changes, which are called out in the [changelog](./CHANGELOG.md). See [RELEASING.md](./RELEASING.md) for version bumps and publication steps.
468
+
469
+ ## Prior art
470
+
471
+ [`react-remount`](https://github.com/sag1v/react-remount) and [`react-remount-component`](https://github.com/alexkrolick/react-remount-component) explored declarative component remounting. This library brings together prop-based remount rules, key hooks, state-only resets, and descendant-triggered reset boundaries, with TypeScript and copy-in distribution.
472
+
473
+ ## License
474
+
475
+ MIT © [Rafael Buzatto de Campos](https://github.com/rbuzatto)
package/RELEASING.md ADDED
@@ -0,0 +1,89 @@
1
+ # Releasing
2
+
3
+ Use Node.js 24 and pnpm 10.27.0, pinned in the `packageManager` field. `package.json`
4
+ is the version source of truth; the CLI reads it and the build includes it in
5
+ template headers. Commit dependency changes to `pnpm-lock.yaml`. Generated
6
+ `dist/` and `templates/` files are not committed.
7
+
8
+ ## Before the first release
9
+
10
+ 1. Confirm that `origin` points to `https://github.com/rbuzatto/react-fresh-key.git`
11
+ and that `repository`, `homepage`, and `bugs` in `package.json` refer to this
12
+ repository.
13
+ 2. Run `pnpm install --frozen-lockfile`, then `pnpm run check:release`. This includes
14
+ Biome lint and formatting checks, TypeScript checks, tests, a fresh build,
15
+ declaration/export checks, and installation of the packed artifact against
16
+ React 16.14, 17, 18, and 19. The package checks require network access and use
17
+ temporary consumer projects.
18
+ 3. Review `pnpm --config.ignore-scripts=true pack --dry-run`. Confirm the archive contains the
19
+ builds, declarations, CLI, registry, templates, documentation, and license.
20
+ 4. Keep version `0.1.0` for this first publication. Replace `Unreleased` in its
21
+ changelog heading with the release date, commit the release notes, and make
22
+ sure `git status --short` is empty.
23
+ 5. Create the first tag with `git tag -a v0.1.0 -m "Release 0.1.0"`. Push the
24
+ commit and tag to the configured remote and wait for CI to pass.
25
+ 6. Authenticate to the intended npm account with `pnpm login`, check it with
26
+ `pnpm whoami`, and run `pnpm publish` with your publish branch at that exact
27
+ tagged commit. pnpm defaults to publishing from `main` or `master`; configure
28
+ `publishBranch` in `pnpm-workspace.yaml` if the repository uses another branch.
29
+
30
+ `pnpm publish` runs the complete release checks through `prepublishOnly`, including
31
+ lint and formatting. CI validates pushes, pull requests, and version tags; it does
32
+ not publish packages.
33
+
34
+ ## Subsequent versions
35
+
36
+ Use [Semantic Versioning](https://semver.org/): while the package is below `1.0`,
37
+ patch releases preserve the public API, and minor releases may make breaking
38
+ changes. Describe breaking changes and migration steps in `CHANGELOG.md`.
39
+ At `1.0`, use major versions for breaking public API changes.
40
+
41
+ 1. Prepare and commit the changes and a dated changelog entry for the next version.
42
+ 2. Start from a clean worktree and run one of:
43
+
44
+ ```bash
45
+ pnpm version patch
46
+ pnpm version minor
47
+ ```
48
+
49
+ pnpm 10 delegates this command to the npm bundled with Node.js. The `preversion`
50
+ hook runs the release checks, including lint and formatting, then
51
+ `package.json` is updated. The `version` hook rebuilds the outputs with the new
52
+ version before the version commit and tag are created. `pnpm-lock.yaml` does
53
+ not store this package's root version, so a version-only bump does not change
54
+ it. A failed hook must be resolved before proceeding; inspect the worktree
55
+ because a failed build after the bump can leave uncommitted version changes.
56
+ 3. Push the version commit and tag to the configured remote, wait for CI, then
57
+ publish from the tagged commit. `prepublishOnly` checks the new version again.
58
+
59
+ Do not reuse a version that npm has already published. Do not bypass release
60
+ hooks with `--ignore-scripts` when publishing.
61
+
62
+ ## Individual checks
63
+
64
+ ```bash
65
+ pnpm run lint
66
+ pnpm run format:check
67
+ pnpm run check:style
68
+ pnpm run check
69
+ pnpm run check:package
70
+ pnpm run check:package --react 17.0.2
71
+ ```
72
+
73
+ `lint` and `format:check` report findings without writing files. `check:style`
74
+ runs both through `biome ci --error-on-warnings .`. Before committing, use `pnpm run lint:fix` for
75
+ safe lint fixes and `pnpm run format` to apply formatting; review remaining lint
76
+ findings manually. Biome covers maintained JS/TS/JSX/TSX/JSON files, excluding
77
+ generated outputs, dependency/store directories, `.tmp-vendor/`, and `pnpm-lock.yaml`.
78
+
79
+ `check` includes `check:style`, TypeScript checks, source/type tests, builds, and
80
+ export checks. The TypeScript checks still validate the public API and React
81
+ compatibility. `check:package` uses the existing build, packs it without lifecycle
82
+ scripts, and verifies the actual installed package. Run `pnpm run build` first
83
+ when invoking it on its own.
84
+
85
+ CI installs the pinned pnpm version from `packageManager`, caches its store, and
86
+ uses `pnpm install --frozen-lockfile`, then `pnpm run check` for lint, formatting,
87
+ and source validation. The package matrix uses Node.js 18 and 24.
88
+ Build tooling runs on Node.js 24, separately from the Node.js 18 minimum supported
89
+ by consumers.