rerender-lens 0.1.0 → 0.2.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 +95 -6
- package/README.md +156 -74
- package/dist/index.cjs +1051 -68
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +232 -22
- package/dist/index.d.ts +232 -22
- package/dist/index.js +1028 -10
- package/dist/index.js.map +1 -1
- package/package.json +10 -24
- package/dist/chunk-3YHI7BCU.js +0 -544
- package/dist/chunk-3YHI7BCU.js.map +0 -1
- package/dist/chunk-DOYB4MKH.cjs +0 -562
- package/dist/chunk-DOYB4MKH.cjs.map +0 -1
- package/dist/jsx-dev-runtime.cjs +0 -32
- package/dist/jsx-dev-runtime.cjs.map +0 -1
- package/dist/jsx-dev-runtime.d.cts +0 -8
- package/dist/jsx-dev-runtime.d.ts +0 -8
- package/dist/jsx-dev-runtime.js +0 -9
- package/dist/jsx-dev-runtime.js.map +0 -1
- package/dist/jsx-runtime.cjs +0 -34
- package/dist/jsx-runtime.cjs.map +0 -1
- package/dist/jsx-runtime.d.cts +0 -9
- package/dist/jsx-runtime.d.ts +0 -9
- package/dist/jsx-runtime.js +0 -10
- package/dist/jsx-runtime.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,15 +1,104 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
- **Sessions.** Record, use the app, Stop; apply a fix; record again. The Sessions view compares
|
|
6
|
+
two sessions: avoidable re-renders per component with deltas, totals, wasted time, and which
|
|
7
|
+
suggested fixes went away. Summaries persist per origin; exports include them.
|
|
8
|
+
- **Provider attribution.** A `useContext` change now names the component that renders the
|
|
9
|
+
Provider and, for object values, which keys changed. When only some keys changed the reason
|
|
10
|
+
and the Fixes view say so and suggest splitting the context or selecting slices.
|
|
11
|
+
- **Children diffs.** Elements are serialized with their props, so a changed `children` prop
|
|
12
|
+
shows the differing leaf (`children[0].props.label`); re-created children get their own advice
|
|
13
|
+
and fix snippet (memoize or hoist them).
|
|
14
|
+
- **Commit priority.** Every report carries the priority React gave the commit (discrete input,
|
|
15
|
+
continuous input, transition / async, low, idle); the Commits list and report show it. The
|
|
16
|
+
bridge `info()` also counts scheduled roots vs commits.
|
|
17
|
+
- Not done: custom hook names for hook changes. React DevTools gets them by re-running the
|
|
18
|
+
component with a fake dispatcher; doing that from a commit hook is too invasive.
|
|
19
|
+
- The panel outside DevTools: **Open side panel** shows it in Chrome's side panel next to the
|
|
20
|
+
page, **Open in window** in its own window (toolbar popup, or the new buttons in the DevTools
|
|
21
|
+
panel). Both use the background relay plus `chrome.scripting` instead of DevTools APIs, follow
|
|
22
|
+
the active tab unless pinned, and stack the layout when narrower than 720px. Firefox gets the
|
|
23
|
+
same page as a sidebar.
|
|
24
|
+
- UI refresh: new palette and spacing in light and dark, icon toolbar with labels that collapse
|
|
25
|
+
in narrow layouts, segmented view switcher, a summary strip (renders, avoidable, wasted time,
|
|
26
|
+
top offender, best fix, all clickable), clearer selection and banners.
|
|
27
|
+
- `selfDuration` is now the component's own render time; the previous value (the whole subtree)
|
|
28
|
+
moved to `treeDuration`. Children that bailed out contribute nothing, as in the Profiler.
|
|
29
|
+
- `memoized` on every report (`React.memo` / `PureComponent`). Reports of unmemoized components
|
|
30
|
+
with avoidable prop changes explain that `React.memo` is needed as well; the panel's Fixes view
|
|
31
|
+
lists it first.
|
|
32
|
+
- Bridge `info()` reports `source` (`page` or `extension`) and `injected`. The panel warns when the
|
|
33
|
+
page runs its own copy next to the injected one.
|
|
34
|
+
- Extension: per-origin "Let React DevTools create the hook" toggle (popup and Settings). With it
|
|
35
|
+
on, the injector waits one task so React DevTools' own script can install the global hook first.
|
|
36
|
+
- Elements sidebar: an element without a React component above it now says so instead of
|
|
37
|
+
"not running".
|
|
38
|
+
- jsdom tests for `background.js`, `popup.js` and `sidebar.js` (shared fake `chrome`).
|
|
39
|
+
- Panel source moved to TypeScript (`extension/src/panel.ts`); `npm run build` writes the
|
|
40
|
+
committed `extension/panel.js`, and CI fails when it is out of date.
|
|
41
|
+
- Tree and live stream are virtualized (only the rows in view exist in the DOM); the stream now
|
|
42
|
+
keeps every buffered report instead of the last 300. `panel.html?demo&flood=5000` loads
|
|
43
|
+
synthetic data for scale testing.
|
|
44
|
+
- Keyboard: `/` focuses search, `f` opens the Fix tab, `Esc` clears the page highlight. The
|
|
45
|
+
details tab is remembered per origin.
|
|
46
|
+
- `different` object props show the differing leaves (`filters.page: 1 → 2`) without expanding
|
|
47
|
+
the whole value; the kind label names the first differing path.
|
|
48
|
+
- Root-cause page: click a root cause in a commit to see every commit it started, the components
|
|
49
|
+
it re-rendered avoidably, and the fixes for them.
|
|
50
|
+
|
|
51
|
+
## 0.2.0
|
|
52
|
+
|
|
53
|
+
Library
|
|
54
|
+
|
|
55
|
+
- Every report carries `commitId` (shared by the reports of one React commit) and, in dev
|
|
56
|
+
builds, `source` (`_debugSource` on React <= 18, parsed from `_debugStack` on React 19).
|
|
57
|
+
- DevTools bridge protocol 2: `hello` carries library version, protocol, React renderers and
|
|
58
|
+
options; `pull(since)` for polling panels; `info()`, `configure()`, `getOptions()`,
|
|
59
|
+
`highlight(instanceId)`, `flashAvoidable(on)` (in-page overlay), `inspect(node)`.
|
|
60
|
+
- `getRenderers()` / `isProductionReact()`: what react-dom registered on the DevTools hook.
|
|
61
|
+
`attach` wraps `hook.inject` so hooks created by react-refresh (Vite) are covered too.
|
|
62
|
+
- `serializeOptions` / `deserializeOptions`, `VERSION`.
|
|
63
|
+
- An IIFE build (`extension/vendor/rerender-lens.js`, global `RerenderLens`) for injection.
|
|
64
|
+
|
|
65
|
+
Extension
|
|
66
|
+
|
|
67
|
+
- Store-ready packaging: `npm run build:ext` writes Chrome, Edge and Firefox zips; the manifest
|
|
68
|
+
version follows `package.json`; CI uploads the zips; the release workflow publishes to the
|
|
69
|
+
Chrome Web Store when secrets are configured. Listing text and privacy policy in
|
|
70
|
+
`extension/store/`.
|
|
71
|
+
- Zero-config mode: enable an origin from the toolbar popup or the panel's Settings and tick
|
|
72
|
+
*Inject the library*; the extension loads rerender-lens into the page before React.
|
|
73
|
+
Optional host permissions for non-local hosts; local development hosts are on by default.
|
|
74
|
+
- Three-state status (no page / no library / connected with library and React versions),
|
|
75
|
+
protocol-mismatch and production-build warnings.
|
|
76
|
+
- Polling fallback through `inspectedWindow.eval` when no content script is present.
|
|
77
|
+
- New views: Offenders (sortable table), Commits (root causes, render cascade, contexts),
|
|
78
|
+
Fixes (ranked by avoidable re-renders removed, with snippets). Per-report Fix tab.
|
|
79
|
+
- Hover-to-highlight and flash of avoidable renders in the page; "open source" links into the
|
|
80
|
+
Sources panel; Copy as Markdown; JSON export/import.
|
|
81
|
+
- Settings drawer driving `configure()` live, persisted per origin; panel state (filters,
|
|
82
|
+
collapsed nodes, split width) persisted per origin.
|
|
83
|
+
- Elements-panel sidebar for `$0`; toolbar badge with the avoidable count per tab.
|
|
84
|
+
- Reports are batched per animation frame; malformed payloads are ignored.
|
|
85
|
+
- Playwright end-to-end suite (`npm run e2e`) running Chromium with the extension against the
|
|
86
|
+
example app (relay, badge, injection, panel).
|
|
87
|
+
|
|
3
88
|
## 0.1.0
|
|
4
89
|
|
|
5
90
|
Initial release.
|
|
6
91
|
|
|
7
|
-
- `init(
|
|
92
|
+
- `init(options)` observes React commits through the DevTools global hook and reports every
|
|
93
|
+
update of a tracked component. No patching, no wrappers: Fast Refresh, memo comparators,
|
|
94
|
+
forwardRef, classes and the automatic JSX runtime all work unchanged.
|
|
8
95
|
- Change classification: `deep-equal`, `function`, `element`, `different`, `added`, `removed`.
|
|
9
96
|
- Trigger classification: `props`, `parent`, `state`, `hooks`, `mixed`; `avoidable` flag.
|
|
10
|
-
-
|
|
11
|
-
|
|
97
|
+
- `parent` (nearest ancestor that rendered, and why), `owner`, `path`, `instanceId`,
|
|
98
|
+
`renderCount`, `selfDuration` on every report.
|
|
99
|
+
- Hook state (`useState`, `useReducer`, `useSyncExternalStore`) and `useContext` diffs.
|
|
100
|
+
- `useWhyRerender(name, values)` hook for tracking one component without `init`.
|
|
12
101
|
- `createCollector()` with `assertNoAvoidable()` for tests; `combineNotifiers()`.
|
|
13
|
-
- `createDevtoolsNotifier()` posting structured-clone-safe reports on `window
|
|
14
|
-
- `
|
|
15
|
-
-
|
|
102
|
+
- `createDevtoolsNotifier()` posting structured-clone-safe reports on `window`.
|
|
103
|
+
- `ignoreHotReload` and `maxReportsPerComponent` options.
|
|
104
|
+
- Example Vite app with three deliberate re-render bugs.
|
package/README.md
CHANGED
|
@@ -1,16 +1,75 @@
|
|
|
1
1
|
# rerender-lens
|
|
2
2
|
|
|
3
|
-
Find avoidable React re-renders and
|
|
4
|
-
|
|
5
|
-
in tests, a hook for tracking a single component, and a bridge for a DevTools panel.
|
|
3
|
+
Find avoidable React re-renders and see exactly what caused them: which prop, which state,
|
|
4
|
+
which context, and which ancestor started the update.
|
|
6
5
|
|
|
6
|
+
It reads the fiber tree after every commit through the same global hook React DevTools uses.
|
|
7
|
+
Nothing in React is patched or wrapped, so it works with Fast Refresh, `React.memo` comparators,
|
|
8
|
+
`forwardRef`, class components, the automatic JSX runtime, and any bundler.
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
▸ [rerender-lens] <ProductRow> avoidable re-render: 1 equal by value, 1 new function
|
|
12
|
+
- caused by <ProductPage> re-rendering (its state changed).
|
|
13
|
+
- 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.
|
|
14
|
+
- prop "onSelect" is a new function instance on every render: wrap it in useCallback (or hoist it out of the parent's render).
|
|
15
|
+
at App > ProductPage > ProductRow
|
|
7
16
|
```
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
17
|
+
|
|
18
|
+
Two ways to use it: the **DevTools extension** (a "Re-renders" panel next to Elements and
|
|
19
|
+
Console, no app code needed) or the **npm package** (console output, test assertions, and the
|
|
20
|
+
bridge the extension reads).
|
|
21
|
+
|
|
22
|
+
## Chrome extension
|
|
23
|
+
|
|
24
|
+
The extension adds a **Re-renders** panel to DevTools: a component tree with avoidable counts,
|
|
25
|
+
why each component rendered, which ancestor started the cascade, the props and hooks that
|
|
26
|
+
changed, and the fix as a snippet you can copy. Other views rank components by wasted renders
|
|
27
|
+
(Offenders), group renders by React commit with their root cause (Commits), and rank every fix
|
|
28
|
+
by how many re-renders it removes (Fixes). It also gives you an Elements-panel sidebar for the
|
|
29
|
+
selected node, a badge with the avoidable count of the tab, hover-to-highlight in the page, and
|
|
30
|
+
"open source" links into the Sources panel.
|
|
31
|
+
|
|
32
|
+
### Install
|
|
33
|
+
|
|
34
|
+
Until the Web Store listing is live, install it unpacked:
|
|
35
|
+
|
|
36
|
+
1. Download `rerender-lens-chrome-<version>.zip` from the
|
|
37
|
+
[latest release](https://github.com/NexaLeaf/rerender-lens/releases) and unzip it, or build it
|
|
38
|
+
from a clone with `npm install && npm run build` and use the `extension/` folder.
|
|
39
|
+
2. Open `chrome://extensions`, turn on **Developer mode**, click **Load unpacked**, pick the folder.
|
|
40
|
+
3. Open your app, open DevTools, pick the **Re-renders** tab.
|
|
41
|
+
|
|
42
|
+
Edge loads the same folder from `edge://extensions`. Firefox 128+ uses the `firefox` zip from the
|
|
43
|
+
release, via `about:debugging`.
|
|
44
|
+
|
|
45
|
+
### Connect a page
|
|
46
|
+
|
|
47
|
+
Local development hosts (`localhost`, `127.0.0.1`, `*.localhost`, `*.local`) work out of the box.
|
|
48
|
+
Any other site: click the toolbar icon and **Enable on this site** (a one-time host permission
|
|
49
|
+
for that origin only).
|
|
50
|
+
|
|
51
|
+
Then pick one of two modes:
|
|
52
|
+
|
|
53
|
+
- **Inject the library** (no app code): tick *Inject the library* in the toolbar popup or in the
|
|
54
|
+
panel's Settings and reload. The extension loads rerender-lens into the page before React,
|
|
55
|
+
tracking every `React.memo` / `PureComponent` by default. Change what is tracked from
|
|
56
|
+
Settings; the choice is saved per origin.
|
|
57
|
+
- **The page runs the library**: install the package and pass the DevTools notifier (see below).
|
|
58
|
+
This is the way to go when you also want console output or want to commit the setup.
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { init, createDevtoolsNotifier } from 'rerender-lens';
|
|
62
|
+
init({ trackAllMemoized: true, silent: true, notifier: createDevtoolsNotifier() });
|
|
11
63
|
```
|
|
12
64
|
|
|
13
|
-
|
|
65
|
+
Production React builds are detected and flagged (names may be minified, hooks unlabeled). If
|
|
66
|
+
React DevTools is also installed and its Components tab comes up empty with injection on, tick
|
|
67
|
+
*Let React DevTools create the hook* for that origin.
|
|
68
|
+
|
|
69
|
+
`extension/README.md` has the details: keyboard shortcuts, how the transport works, Firefox and
|
|
70
|
+
Edge packaging, and the store listing.
|
|
71
|
+
|
|
72
|
+
## Install the package
|
|
14
73
|
|
|
15
74
|
```sh
|
|
16
75
|
npm i -D rerender-lens
|
|
@@ -20,16 +79,12 @@ Peer dependency: `react >= 16.8`. Tested with React 18 and 19.
|
|
|
20
79
|
|
|
21
80
|
## Setup
|
|
22
81
|
|
|
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
82
|
```ts
|
|
27
83
|
// src/rerender-lens.ts
|
|
28
|
-
import React from 'react'; // default import, not `import * as React`
|
|
29
84
|
import { init } from 'rerender-lens';
|
|
30
85
|
|
|
31
86
|
if (import.meta.env.DEV) {
|
|
32
|
-
init(
|
|
87
|
+
init({ trackAllMemoized: true });
|
|
33
88
|
}
|
|
34
89
|
```
|
|
35
90
|
|
|
@@ -40,31 +95,16 @@ import { createRoot } from 'react-dom/client';
|
|
|
40
95
|
...
|
|
41
96
|
```
|
|
42
97
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
```
|
|
98
|
+
Import the setup file before `react-dom`. React DOM looks for the DevTools hook once, when its
|
|
99
|
+
module loads; `init` creates the hook if nothing else did. If the React DevTools extension is
|
|
100
|
+
installed, or Vite's Fast Refresh preamble runs, the hook already exists and order does not matter.
|
|
60
101
|
|
|
61
|
-
|
|
62
|
-
element, libraries) is covered by `init` alone.
|
|
102
|
+
No `jsxImportSource`, no default-import requirement, no Babel plugin.
|
|
63
103
|
|
|
64
104
|
## Choosing what to track
|
|
65
105
|
|
|
66
106
|
```ts
|
|
67
|
-
init(
|
|
107
|
+
init({
|
|
68
108
|
trackAllMemoized: true, // every React.memo and PureComponent
|
|
69
109
|
include: [/^Grid/, 'Sidebar'], // by display name: string, RegExp or predicate
|
|
70
110
|
exclude: ['DevOverlay'],
|
|
@@ -81,7 +121,8 @@ export default track(memo(Sidebar));
|
|
|
81
121
|
export const Cell = track((props: CellProps) => ..., 'Cell'); // name for anonymous arrows
|
|
82
122
|
```
|
|
83
123
|
|
|
84
|
-
Marking sets the static `rerenderLens = true`; you can also set it by hand.
|
|
124
|
+
Marking sets the static `rerenderLens = true`; you can also set it by hand. `configure()`
|
|
125
|
+
changes any option at runtime without remounting anything.
|
|
85
126
|
|
|
86
127
|
## What a report contains
|
|
87
128
|
|
|
@@ -94,7 +135,16 @@ Every update of a tracked component produces a `RenderReport`:
|
|
|
94
135
|
| `avoidable` | `true` when nothing genuinely changed |
|
|
95
136
|
| `propChanges` | one entry per changed prop with `path`, `kind`, `prev`, `next` |
|
|
96
137
|
| `stateChanges` | class components: `this.state` diff |
|
|
97
|
-
| `hookChanges` | `useState`, `useReducer`, `
|
|
138
|
+
| `hookChanges` | `useState`, `useReducer`, `useSyncExternalStore` and `useContext` values that changed; context entries carry `provider` (who renders it) and `changedKeys` / `totalKeys` for object values |
|
|
139
|
+
| `parent` | nearest ancestor that rendered in the same commit, and why |
|
|
140
|
+
| `owner` | component that created the element (dev builds) |
|
|
141
|
+
| `path` | component ancestry from the root |
|
|
142
|
+
| `instanceId`, `renderCount` | stable per mounted instance |
|
|
143
|
+
| `memoized` | `React.memo` / `PureComponent`: props alone decide whether it re-renders |
|
|
144
|
+
| `selfDuration`, `treeDuration` | own render time, and with everything below that rendered (dev/profiling builds) |
|
|
145
|
+
| `commitId` | shared by every report of one React commit |
|
|
146
|
+
| `commitPriority` | `immediate` (clicks, keys), `user-blocking` (continuous input), `normal` (transitions, async), `low`, `idle` |
|
|
147
|
+
| `source` | file, line and column where the element was created (dev builds) |
|
|
98
148
|
| `reasons` | human-readable explanations with the fix |
|
|
99
149
|
|
|
100
150
|
Change kinds:
|
|
@@ -107,37 +157,36 @@ Change kinds:
|
|
|
107
157
|
| `different` | a real change | none needed |
|
|
108
158
|
| `added` / `removed` | key appeared or disappeared | usually a real change |
|
|
109
159
|
|
|
110
|
-
A `parent` trigger with no changes at all means: identical props,
|
|
160
|
+
A `parent` trigger with no changes at all means: identical props, an ancestor re-rendered.
|
|
111
161
|
Wrap the component in `React.memo`.
|
|
112
162
|
|
|
113
|
-
By default only avoidable re-renders are printed
|
|
114
|
-
|
|
163
|
+
By default only avoidable re-renders are printed; pass `logAll: true` to print every report.
|
|
164
|
+
The `notifier` always receives every report.
|
|
115
165
|
|
|
116
166
|
## Use in tests
|
|
117
167
|
|
|
118
168
|
```ts
|
|
119
|
-
import React from 'react';
|
|
120
169
|
import { init, disable, createCollector } from 'rerender-lens';
|
|
121
170
|
|
|
122
171
|
const collector = createCollector();
|
|
123
|
-
beforeAll(() => init(
|
|
172
|
+
beforeAll(() => init({ trackAllMemoized: true, silent: true, notifier: collector.notifier }));
|
|
124
173
|
afterAll(disable);
|
|
125
174
|
beforeEach(collector.clear);
|
|
126
175
|
|
|
127
176
|
test('typing in the search box does not re-render the grid rows', async () => {
|
|
128
177
|
render(<ProductPage />);
|
|
129
178
|
await user.type(screen.getByRole('searchbox'), 'abc');
|
|
130
|
-
collector.assertNoAvoidable(); // throws
|
|
179
|
+
collector.assertNoAvoidable(); // throws listing every component and reason
|
|
131
180
|
// or: expect(collector.avoidable).toHaveLength(0)
|
|
132
181
|
});
|
|
133
182
|
```
|
|
134
183
|
|
|
135
|
-
|
|
136
|
-
|
|
184
|
+
In Vitest or Jest, call `ensureDevtoolsHook()` (or `init`) from a `setupFiles` entry so the hook
|
|
185
|
+
exists before `react-dom` is imported by your tests.
|
|
137
186
|
|
|
138
187
|
## Track a single component from the inside
|
|
139
188
|
|
|
140
|
-
|
|
189
|
+
Works anywhere without `init`, for example inside library packages or Storybook:
|
|
141
190
|
|
|
142
191
|
```ts
|
|
143
192
|
import { useWhyRerender } from 'rerender-lens';
|
|
@@ -149,35 +198,52 @@ function Row(props: RowProps) {
|
|
|
149
198
|
}
|
|
150
199
|
```
|
|
151
200
|
|
|
152
|
-
Pass whatever values you want compared.
|
|
153
|
-
called, or the `options` third argument.
|
|
201
|
+
Pass whatever values you want compared.
|
|
154
202
|
|
|
155
203
|
## DevTools bridge
|
|
156
204
|
|
|
157
205
|
```ts
|
|
158
206
|
import { createDevtoolsNotifier } from 'rerender-lens';
|
|
159
207
|
|
|
160
|
-
init(
|
|
208
|
+
init({ trackAllMemoized: true, silent: true, notifier: createDevtoolsNotifier() });
|
|
161
209
|
```
|
|
162
210
|
|
|
163
211
|
Each report is serialized (functions become `ƒ name`, elements `<Type>`, cycles cut) and posted
|
|
164
|
-
on `window` as `{ __rerenderLens: true, version:
|
|
165
|
-
reports are buffered
|
|
166
|
-
|
|
212
|
+
on `window` as `{ __rerenderLens: true, version: 2, type: 'report', payload }`. The last 300
|
|
213
|
+
reports are buffered. The bridge at `window.__RERENDER_LENS_DEVTOOLS__` exposes:
|
|
214
|
+
|
|
215
|
+
| Method | |
|
|
216
|
+
| --- | --- |
|
|
217
|
+
| `replay()` / `clear()` | re-post or drop the buffer |
|
|
218
|
+
| `pull(since)` | reports newer than a sequence number, for panels that poll instead of listening |
|
|
219
|
+
| `info()` | library version, protocol, React renderers (version, dev/prod), current options |
|
|
220
|
+
| `configure(options)` / `getOptions()` | change options at runtime; matchers as strings (`"/^Grid/"`) |
|
|
221
|
+
| `highlight(instanceId)` / `flashAvoidable(on)` | outline a component's DOM in the page, or flash avoidable renders |
|
|
222
|
+
| `inspect(node)` | component, instance id and recent reports for a DOM node (DevTools `$0`) |
|
|
223
|
+
|
|
224
|
+
The DevTools extension consumes this (see [Chrome extension](#chrome-extension) above); with
|
|
225
|
+
injection enabled it needs no `init` call at all.
|
|
226
|
+
|
|
227
|
+
Every report also carries `commitId` (shared by all reports of one React commit) and, in dev
|
|
228
|
+
builds, `source` (where the element was created).
|
|
167
229
|
|
|
168
230
|
## API
|
|
169
231
|
|
|
170
232
|
```ts
|
|
171
|
-
init(
|
|
172
|
-
configure(options): void
|
|
173
|
-
disable(): void
|
|
233
|
+
init(options?): () => void // returns disable
|
|
234
|
+
configure(options): void // merge options at runtime
|
|
235
|
+
disable(): void
|
|
174
236
|
isEnabled(): boolean
|
|
175
237
|
track(component, name?): component
|
|
238
|
+
ensureDevtoolsHook(): hook // create the global hook early (test setup files)
|
|
176
239
|
useWhyRerender(name, values, options?)
|
|
177
240
|
createCollector(): { reports, avoidable, notifier, clear, assertNoAvoidable }
|
|
178
241
|
combineNotifiers(...notifiers): Notifier
|
|
179
|
-
createDevtoolsNotifier({ bufferSize?, target?, maxDepth? }): Notifier
|
|
180
|
-
|
|
242
|
+
createDevtoolsNotifier({ bufferSize?, target?, maxDepth?, flashAvoidable? }): Notifier
|
|
243
|
+
getRenderers(), isProductionReact() // what react-dom registered on the DevTools hook
|
|
244
|
+
serializeOptions(o), deserializeOptions(o) // Options <-> JSON-safe form used by the bridge
|
|
245
|
+
VERSION
|
|
246
|
+
deepEqual(a, b), diffRecords(prev, next), classify(prev, next)
|
|
181
247
|
```
|
|
182
248
|
|
|
183
249
|
`Options`:
|
|
@@ -187,45 +253,61 @@ deepEqual(a, b), diffRecords(prev, next), classify(prev, next) // the primitiv
|
|
|
187
253
|
| `trackAllMemoized` | `false` | track every `memo` / `PureComponent` |
|
|
188
254
|
| `trackAllComponents` | `false` | track everything (noisy) |
|
|
189
255
|
| `include` / `exclude` | | display-name matchers |
|
|
190
|
-
| `trackHooks` | `true` |
|
|
256
|
+
| `trackHooks` | `true` | diff hook state and contexts |
|
|
191
257
|
| `logAll` | `false` | print non-avoidable reports too |
|
|
192
258
|
| `silent` | `false` | never print; notifier still runs |
|
|
193
259
|
| `notifier` | | receives every `RenderReport` |
|
|
194
260
|
| `collapse` | `true` | `console.groupCollapsed` vs `console.group` |
|
|
195
261
|
| `console` | `console` | sink for printing |
|
|
262
|
+
| `ignoreHotReload` | `true` | skip commits caused by Fast Refresh |
|
|
263
|
+
| `maxReportsPerComponent` | `0` | stop printing a component after N reports |
|
|
196
264
|
|
|
197
265
|
## How it works, and the caveats that follow
|
|
198
266
|
|
|
199
|
-
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
`useSyncExternalStore
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
-
|
|
211
|
-
|
|
212
|
-
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
267
|
+
- After each commit, the fiber tree is walked from the root, skipping subtrees React bailed out
|
|
268
|
+
of. A component counts as re-rendered when React set its `PerformedWork` flag. Props, class
|
|
269
|
+
state, hook state nodes and context dependencies are compared against the fiber's alternate.
|
|
270
|
+
- Hook labels come from React's dev-only hook type list when it maps one-to-one onto the state
|
|
271
|
+
nodes; otherwise nodes are labelled by inspection (`useState`, `useReducer`,
|
|
272
|
+
`useSyncExternalStore`, or `state` for internal nodes of `useTransition` and friends).
|
|
273
|
+
- A commit in which Fast Refresh swapped a component's code is skipped entirely
|
|
274
|
+
(`ignoreHotReload`). The edited component's children re-render with identical props during
|
|
275
|
+
that commit, which would otherwise look like avoidable re-renders.
|
|
276
|
+
- The mount render is never reported. StrictMode's double render happens inside one commit and
|
|
277
|
+
is reported once.
|
|
278
|
+
- Everything runs during React's commit callback, synchronously. It is meant for development;
|
|
279
|
+
keep it out of production builds.
|
|
280
|
+
- Fiber field names have been stable since React 16.9 (`flags` was `effectTag` before 17; both
|
|
281
|
+
are handled). Future React versions may change internals; the walk is wrapped so a failure
|
|
282
|
+
logs one warning instead of breaking the app.
|
|
283
|
+
|
|
284
|
+
## Example app
|
|
285
|
+
|
|
286
|
+
`examples/vite-react` has three deliberate bugs. From the repo root:
|
|
287
|
+
|
|
288
|
+
```sh
|
|
289
|
+
npm install
|
|
290
|
+
npm --prefix examples/vite-react install
|
|
291
|
+
npm run dev:example
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
`http://localhost:5199/` runs the library itself and reports to the console and the extension.
|
|
295
|
+
`http://localhost:5199/plain.html` is the same app without the library, for trying the
|
|
296
|
+
extension's *Inject the library* mode.
|
|
216
297
|
|
|
217
298
|
## Migrating from why-did-you-render
|
|
218
299
|
|
|
219
300
|
| why-did-you-render | rerender-lens |
|
|
220
301
|
| --- | --- |
|
|
221
|
-
| `whyDidYouRender(React, opts)` | `init(
|
|
302
|
+
| `whyDidYouRender(React, opts)` | `init(opts)` |
|
|
222
303
|
| `trackAllPureComponents` | `trackAllMemoized` |
|
|
223
304
|
| `Comp.whyDidYouRender = true` | `track(Comp)` or `Comp.rerenderLens = true` |
|
|
224
305
|
| `include` / `exclude` (RegExp[]) | same, plus strings and predicates |
|
|
225
306
|
| `trackHooks` | same |
|
|
226
307
|
| `logOnDifferentValues` | `logAll` |
|
|
308
|
+
| `logOwnerReasons` | always on: `parent` and `owner` fields |
|
|
227
309
|
| `notifier` (`{ Component, prevProps, ... }`) | `notifier` (`RenderReport`) |
|
|
228
|
-
| `jsxImportSource: '@welldone-software/why-did-you-render'` |
|
|
310
|
+
| `jsxImportSource: '@welldone-software/why-did-you-render'` | not needed |
|
|
229
311
|
|
|
230
312
|
## License
|
|
231
313
|
|