rerender-lens 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,15 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ Initial release.
6
+
7
+ - `init(React, options)` patches `createElement` and the state hooks; tracked components report every update.
8
+ - Change classification: `deep-equal`, `function`, `element`, `different`, `added`, `removed`.
9
+ - Trigger classification: `props`, `parent`, `state`, `hooks`, `mixed`; `avoidable` flag.
10
+ - Supports function components, `React.memo` (custom comparators preserved), `forwardRef`, `memo(forwardRef())`, class and `PureComponent` components.
11
+ - `useWhyRerender(name, values)` hook for tracking one component without patching.
12
+ - `createCollector()` with `assertNoAvoidable()` for tests; `combineNotifiers()`.
13
+ - `createDevtoolsNotifier()` posting structured-clone-safe reports on `window` for a DevTools panel.
14
+ - `rerender-lens/jsx-runtime` and `rerender-lens/jsx-dev-runtime` for the automatic JSX runtime.
15
+ - StrictMode double-render is not reported.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Palanisamy Muthusamy
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,232 @@
1
+ # rerender-lens
2
+
3
+ Find avoidable React re-renders and learn exactly which prop, state or hook caused them.
4
+ A modern alternative to `why-did-you-render`: TypeScript, structured reports you can assert on
5
+ in tests, a hook for tracking a single component, and a bridge for a DevTools panel.
6
+
7
+ ```
8
+ [rerender-lens] <ProductRow> avoidable re-render: 1 equal by value, 1 new function
9
+ - prop "style" is a new reference but deep-equal to the previous value: memoize the object with useMemo, or hoist it to module scope if it is constant.
10
+ - prop "onSelect" is a new function instance on every render: wrap it in useCallback (or hoist it out of the parent's render).
11
+ ```
12
+
13
+ ## Install
14
+
15
+ ```sh
16
+ npm i -D rerender-lens
17
+ ```
18
+
19
+ Peer dependency: `react >= 16.8`. Tested with React 18 and 19.
20
+
21
+ ## Setup
22
+
23
+ Create a file that runs **before anything else imports React components**, and import it first
24
+ in your entry point. Development only.
25
+
26
+ ```ts
27
+ // src/rerender-lens.ts
28
+ import React from 'react'; // default import, not `import * as React`
29
+ import { init } from 'rerender-lens';
30
+
31
+ if (import.meta.env.DEV) {
32
+ init(React, { trackAllMemoized: true });
33
+ }
34
+ ```
35
+
36
+ ```ts
37
+ // src/main.tsx
38
+ import './rerender-lens';
39
+ import { createRoot } from 'react-dom/client';
40
+ ...
41
+ ```
42
+
43
+ ### Automatic JSX runtime (Vite, Next, TS `react-jsx`)
44
+
45
+ With the automatic runtime the compiler never calls `React.createElement`, so also point
46
+ `jsxImportSource` at this package in development:
47
+
48
+ ```ts
49
+ // vite.config.ts
50
+ export default defineConfig(({ mode }) => ({
51
+ esbuild: mode === 'development' ? { jsxImportSource: 'rerender-lens' } : undefined,
52
+ plugins: [react({ jsxImportSource: mode === 'development' ? 'rerender-lens' : 'react' })],
53
+ }));
54
+ ```
55
+
56
+ ```jsonc
57
+ // tsconfig.json (or a tsconfig.dev.json)
58
+ { "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "rerender-lens" } }
59
+ ```
60
+
61
+ Anything still using `React.createElement` (the classic runtime, `React.cloneElement` of a new
62
+ element, libraries) is covered by `init` alone.
63
+
64
+ ## Choosing what to track
65
+
66
+ ```ts
67
+ init(React, {
68
+ trackAllMemoized: true, // every React.memo and PureComponent
69
+ include: [/^Grid/, 'Sidebar'], // by display name: string, RegExp or predicate
70
+ exclude: ['DevOverlay'],
71
+ });
72
+ ```
73
+
74
+ Or mark a component:
75
+
76
+ ```ts
77
+ import { track } from 'rerender-lens';
78
+
79
+ export const ProductRow = track(function ProductRow(props: Props) { ... });
80
+ export default track(memo(Sidebar));
81
+ export const Cell = track((props: CellProps) => ..., 'Cell'); // name for anonymous arrows
82
+ ```
83
+
84
+ Marking sets the static `rerenderLens = true`; you can also set it by hand.
85
+
86
+ ## What a report contains
87
+
88
+ Every update of a tracked component produces a `RenderReport`:
89
+
90
+ | Field | Meaning |
91
+ | --- | --- |
92
+ | `component` | display name |
93
+ | `trigger` | `props`, `parent`, `state`, `hooks` or `mixed` |
94
+ | `avoidable` | `true` when nothing genuinely changed |
95
+ | `propChanges` | one entry per changed prop with `path`, `kind`, `prev`, `next` |
96
+ | `stateChanges` | class components: `this.state` diff |
97
+ | `hookChanges` | `useState`, `useReducer`, `useContext`, `useSyncExternalStore` values that changed |
98
+ | `reasons` | human-readable explanations with the fix |
99
+
100
+ Change kinds:
101
+
102
+ | `kind` | Means | Fix |
103
+ | --- | --- | --- |
104
+ | `deep-equal` | new reference, same contents | `useMemo`, or hoist a constant |
105
+ | `function` | new function, same body | `useCallback` |
106
+ | `element` | new element, same type and props | `useMemo` the element or pass it as `children` |
107
+ | `different` | a real change | none needed |
108
+ | `added` / `removed` | key appeared or disappeared | usually a real change |
109
+
110
+ A `parent` trigger with no changes at all means: identical props, the parent re-rendered.
111
+ Wrap the component in `React.memo`.
112
+
113
+ By default only avoidable re-renders are printed to the console; pass `logAll: true` to print
114
+ every report. The `notifier` always receives every report.
115
+
116
+ ## Use in tests
117
+
118
+ ```ts
119
+ import React from 'react';
120
+ import { init, disable, createCollector } from 'rerender-lens';
121
+
122
+ const collector = createCollector();
123
+ beforeAll(() => init(React, { trackAllMemoized: true, silent: true, notifier: collector.notifier }));
124
+ afterAll(disable);
125
+ beforeEach(collector.clear);
126
+
127
+ test('typing in the search box does not re-render the grid rows', async () => {
128
+ render(<ProductPage />);
129
+ await user.type(screen.getByRole('searchbox'), 'abc');
130
+ collector.assertNoAvoidable(); // throws with every component and reason
131
+ // or: expect(collector.avoidable).toHaveLength(0)
132
+ });
133
+ ```
134
+
135
+ Import order matters here too: `init` must run before the modules under test read
136
+ `React.useState` etc. A `setupFiles` entry in Vitest/Jest is the usual place.
137
+
138
+ ## Track a single component from the inside
139
+
140
+ No patching, works anywhere, including inside library packages or Storybook:
141
+
142
+ ```ts
143
+ import { useWhyRerender } from 'rerender-lens';
144
+
145
+ function Row(props: RowProps) {
146
+ const theme = useContext(ThemeContext);
147
+ useWhyRerender('Row', { ...props, theme });
148
+ ...
149
+ }
150
+ ```
151
+
152
+ Pass whatever values you want compared. Reports use the notifier from `init` when it was
153
+ called, or the `options` third argument.
154
+
155
+ ## DevTools bridge
156
+
157
+ ```ts
158
+ import { createDevtoolsNotifier } from 'rerender-lens';
159
+
160
+ init(React, { trackAllMemoized: true, silent: true, notifier: createDevtoolsNotifier() });
161
+ ```
162
+
163
+ Each report is serialized (functions become `ƒ name`, elements `<Type>`, cycles cut) and posted
164
+ on `window` as `{ __rerenderLens: true, version: 1, type: 'report', payload }`. The last 300
165
+ reports are buffered; `window.__RERENDER_LENS_DEVTOOLS__.replay()` re-posts them and `.clear()`
166
+ drops them. Any extension or in-page panel can consume this.
167
+
168
+ ## API
169
+
170
+ ```ts
171
+ init(React, options?): () => void // returns disable
172
+ configure(options): void // merge options at runtime
173
+ disable(): void // restore React
174
+ isEnabled(): boolean
175
+ track(component, name?): component
176
+ useWhyRerender(name, values, options?)
177
+ createCollector(): { reports, avoidable, notifier, clear, assertNoAvoidable }
178
+ combineNotifiers(...notifiers): Notifier
179
+ createDevtoolsNotifier({ bufferSize?, target?, maxDepth? }): Notifier
180
+ deepEqual(a, b), diffRecords(prev, next), classify(prev, next) // the primitives
181
+ ```
182
+
183
+ `Options`:
184
+
185
+ | Option | Default | |
186
+ | --- | --- | --- |
187
+ | `trackAllMemoized` | `false` | track every `memo` / `PureComponent` |
188
+ | `trackAllComponents` | `false` | track everything (noisy) |
189
+ | `include` / `exclude` | | display-name matchers |
190
+ | `trackHooks` | `true` | capture and diff hook values |
191
+ | `logAll` | `false` | print non-avoidable reports too |
192
+ | `silent` | `false` | never print; notifier still runs |
193
+ | `notifier` | | receives every `RenderReport` |
194
+ | `collapse` | `true` | `console.groupCollapsed` vs `console.group` |
195
+ | `console` | `console` | sink for printing |
196
+
197
+ ## How it works, and the caveats that follow
198
+
199
+ - `init` replaces `React.createElement` with one that maps a tracked component type to a cached
200
+ wrapper. The wrapper renders the original, keeps the previous props and hook values in a ref,
201
+ and diffs them on the next render. Class components get a subclass whose `componentDidUpdate`
202
+ diffs `props` and `state` and then calls yours.
203
+ - Hook values are captured by patching `React.useState`, `useReducer`, `useContext` and
204
+ `useSyncExternalStore`. This only reaches code that reads those functions **after** `init`
205
+ ran, which is why the setup file must be imported first. With `import * as React`, the
206
+ namespace is read-only in most bundlers; pass the default import.
207
+ - A wrapper is a different element type from the original, so the first time a component
208
+ becomes tracked (or stops being tracked after `configure`) it remounts. Decide tracking at
209
+ startup; use `configure` for reporting options.
210
+ - The mount render is never reported. Under `StrictMode` the duplicate dev render is detected
211
+ (same props object, same hook values) and skipped.
212
+ - `trackHooks: false` disables state-triggered reports for function components, because the
213
+ wrapper then cannot distinguish a state update from a StrictMode re-invocation.
214
+ - Diffing is structural and happens during render. It is meant for development; do not enable
215
+ it in production builds.
216
+
217
+ ## Migrating from why-did-you-render
218
+
219
+ | why-did-you-render | rerender-lens |
220
+ | --- | --- |
221
+ | `whyDidYouRender(React, opts)` | `init(React, opts)` |
222
+ | `trackAllPureComponents` | `trackAllMemoized` |
223
+ | `Comp.whyDidYouRender = true` | `track(Comp)` or `Comp.rerenderLens = true` |
224
+ | `include` / `exclude` (RegExp[]) | same, plus strings and predicates |
225
+ | `trackHooks` | same |
226
+ | `logOnDifferentValues` | `logAll` |
227
+ | `notifier` (`{ Component, prevProps, ... }`) | `notifier` (`RenderReport`) |
228
+ | `jsxImportSource: '@welldone-software/why-did-you-render'` | `jsxImportSource: 'rerender-lens'` |
229
+
230
+ ## License
231
+
232
+ MIT