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 +15 -0
- package/LICENSE +21 -0
- package/README.md +232 -0
- package/dist/chunk-3YHI7BCU.js +544 -0
- package/dist/chunk-3YHI7BCU.js.map +1 -0
- package/dist/chunk-DOYB4MKH.cjs +562 -0
- package/dist/chunk-DOYB4MKH.cjs.map +1 -0
- package/dist/index.cjs +197 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +213 -0
- package/dist/index.d.ts +213 -0
- package/dist/index.js +134 -0
- package/dist/index.js.map +1 -0
- package/dist/jsx-dev-runtime.cjs +32 -0
- package/dist/jsx-dev-runtime.cjs.map +1 -0
- package/dist/jsx-dev-runtime.d.cts +8 -0
- package/dist/jsx-dev-runtime.d.ts +8 -0
- package/dist/jsx-dev-runtime.js +9 -0
- package/dist/jsx-dev-runtime.js.map +1 -0
- package/dist/jsx-runtime.cjs +34 -0
- package/dist/jsx-runtime.cjs.map +1 -0
- package/dist/jsx-runtime.d.cts +9 -0
- package/dist/jsx-runtime.d.ts +9 -0
- package/dist/jsx-runtime.js +10 -0
- package/dist/jsx-runtime.js.map +1 -0
- package/package.json +101 -0
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
|