react-x11 2.51.0 → 2.52.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.
Files changed (91) hide show
  1. package/README.md +87 -54
  2. package/docs/README.md +410 -0
  3. package/docs/accessibility.md +628 -0
  4. package/docs/appearance.md +416 -0
  5. package/docs/architecture/accessibility.md +470 -0
  6. package/docs/architecture/animation.md +707 -0
  7. package/docs/architecture/container-queries.md +525 -0
  8. package/docs/architecture/custom-layout.md +334 -0
  9. package/docs/architecture/drag-and-drop.md +941 -0
  10. package/docs/architecture/element-layer-contents.md +622 -0
  11. package/docs/architecture/frame-pacing.md +255 -0
  12. package/docs/architecture/grid-layout.md +358 -0
  13. package/docs/architecture/protocol-efficiency.md +357 -0
  14. package/docs/architecture/video.md +1149 -0
  15. package/docs/architecture/windowed-regions.md +854 -0
  16. package/docs/click-to-component.md +125 -0
  17. package/docs/clipboard.md +281 -0
  18. package/docs/components.md +1749 -0
  19. package/docs/dbus.md +277 -0
  20. package/docs/debugging.md +464 -0
  21. package/docs/desktop-calendar.md +284 -0
  22. package/docs/desktop.md +648 -0
  23. package/docs/devtools.md +250 -0
  24. package/docs/drag-and-drop.md +663 -0
  25. package/docs/ecosystem/animation.md +394 -0
  26. package/docs/ecosystem/data-fetching.md +201 -0
  27. package/docs/ecosystem/dev-tooling.md +436 -0
  28. package/docs/ecosystem/forms.md +313 -0
  29. package/docs/ecosystem/headless.md +219 -0
  30. package/docs/ecosystem/i18n.md +89 -0
  31. package/docs/ecosystem/icons.md +410 -0
  32. package/docs/ecosystem/layout.md +216 -0
  33. package/docs/ecosystem/routing.md +146 -0
  34. package/docs/ecosystem/state.md +361 -0
  35. package/docs/ecosystem/testing.md +500 -0
  36. package/docs/ecosystem/theming.md +540 -0
  37. package/docs/ecosystem.md +473 -0
  38. package/docs/elements.md +2762 -0
  39. package/docs/embedding.md +288 -0
  40. package/docs/events.md +1025 -0
  41. package/docs/extending.md +2242 -0
  42. package/docs/eyedropper.md +223 -0
  43. package/docs/filedialog.md +258 -0
  44. package/docs/frame.md +580 -0
  45. package/docs/gl.md +170 -0
  46. package/docs/globalmenu.md +278 -0
  47. package/docs/glx.md +137 -0
  48. package/docs/macos.md +3295 -0
  49. package/docs/notifications.md +164 -0
  50. package/docs/packaging.md +611 -0
  51. package/docs/permissions.md +148 -0
  52. package/docs/react-features.md +430 -0
  53. package/docs/remote.md +223 -0
  54. package/docs/scale.md +339 -0
  55. package/docs/security.md +132 -0
  56. package/docs/styling.md +1928 -0
  57. package/docs/system.md +599 -0
  58. package/docs/testing.md +514 -0
  59. package/docs/typescript.md +117 -0
  60. package/docs/uri-schemes.md +380 -0
  61. package/docs/wayland-backend.md +591 -0
  62. package/docs/wayland.md +545 -0
  63. package/docs/windows-embedding.md +271 -0
  64. package/docs/windows-gl.md +221 -0
  65. package/docs/windows-integrations.md +233 -0
  66. package/docs/windows.md +1404 -0
  67. package/package.json +10 -3
  68. package/src/bus.js +39 -26
  69. package/src/cli/create.d.ts +43 -0
  70. package/src/cli/create.js +305 -0
  71. package/src/cli/env.js +484 -0
  72. package/src/cli/index.js +29 -2
  73. package/src/cli/templates/AGENTS.template.md +195 -0
  74. package/src/cli/templates/DESIGN.template.md +269 -0
  75. package/src/desktopintegration.js +2 -1
  76. package/src/desktopsettings.js +193 -24
  77. package/src/filedialog.js +3 -3
  78. package/src/keyboardstate.js +28 -0
  79. package/src/node.d.ts +10 -10
  80. package/src/portal.js +31 -0
  81. package/src/screencolor.js +5 -10
  82. package/src/types/system.d.ts +8 -6
  83. package/src/wayland/app.js +59 -0
  84. package/src/wayland/clipboard.js +14 -7
  85. package/src/wayland/connection.js +3 -0
  86. package/src/wayland/context2d.js +129 -10
  87. package/src/wayland/protocols/index.json +6 -0
  88. package/src/wayland/protocols/xdg-foreign-unstable-v2.json +1 -0
  89. package/src/wayland/seat.js +28 -1
  90. package/src/wayland/swapchain.js +23 -0
  91. package/src/wayland/xkb.js +137 -0
@@ -0,0 +1,436 @@
1
+ # Developer tooling
2
+
3
+ This is the payoff of a pure-JS stack: a react-x11 app is an ordinary Node
4
+ process, so the whole V8 and React tooling story applies unmodified. Nothing
5
+ on this page needs an adapter.
6
+
7
+ The four tools answer four different questions, and it is worth knowing which
8
+ is which before you reach for one:
9
+
10
+ | Question | Tool |
11
+ | ---------------------------------------------- | ------------------------------------------------ |
12
+ | Which component rendered, and with what props? | [React DevTools](#react-devtools) |
13
+ | Was that render _wasted_? | [why-did-you-render](#why-did-you-render) |
14
+ | Where did the milliseconds go? | [`node --inspect`](#node-inspect), [`0x`](#0x) |
15
+ | Why is memory growing? | [`node --inspect`](#node-inspect) heap snapshots |
16
+
17
+ `react-scan` is not on this page: its documented entry point does not resolve
18
+ under bare Node, and its programmatic API silently never fires. See the
19
+ [negative-results register](../ecosystem.md#silent-failures). React's own
20
+ `<Profiler>` works unchanged under react-x11 and covers the same ground for
21
+ commit timings.
22
+
23
+ ## React DevTools {#react-devtools}
24
+
25
+ **Out of the box — already wired behind an env var.** react-devtools-core@7.0.1.
26
+
27
+ `react-devtools-core` is the backend half of React DevTools, made for exactly
28
+ this situation: a React tree running outside a browser page. It connects over
29
+ WebSocket to the standalone DevTools UI and streams the fiber tree, props,
30
+ hooks and profiler data. The integration lives in
31
+ `src/DevToolsIntegration.js` and costs nothing when off.
32
+
33
+ ```sh
34
+ npm i -D react-devtools-core
35
+
36
+ # terminal 1 — standalone UI, listens on :8097
37
+ npx react-devtools
38
+
39
+ # terminal 2 — any app
40
+ REACT_X11_DEVTOOLS=1 npm run examples:dashboard
41
+ ```
42
+
43
+ `src/Reconciler.js` checks `REACT_X11_DEVTOOLS` when you first call
44
+ `render()`/`createRoot()`, and installs the DevTools global hook _before the
45
+ first commit_ — that ordering is what makes mounted roots visible, and it
46
+ holds because a commit can only follow a root.
47
+ `REACT_X11_DEVTOOLS_HOST` and `REACT_X11_DEVTOOLS_PORT` point the backend at
48
+ a UI elsewhere, which is useful when the app runs on a headless box next to
49
+ the X server.
50
+
51
+ Working today: the live component tree, props/hooks/state inspection, and
52
+ highlight-on-hover — hovering an element in the tree tints its rect in the
53
+ X11 window. See [devtools](../devtools.md) for the full walkthrough.
54
+
55
+ - **Start the standalone UI first**, or wait for the backend's retry. The app
56
+ side retries; the UI side does not attach to an already-running app unless
57
+ the hook was installed at startup, and `REACT_X11_DEVTOOLS` cannot be
58
+ turned on after the fact.
59
+ - The Profiler's commit-oriented views (flamegraph, ranked) work with the dev
60
+ reconciler. The **Timeline** view cannot — it needs `injectProfilingHooks`,
61
+ which react-reconciler 0.33 does not expose at all.
62
+ - `injectIntoDevTools` takes zero arguments in react-reconciler 0.33, so the
63
+ config object passed to it is silently discarded and
64
+ `findFiberByHostInstance` is never registered — DevTools cannot map a host
65
+ node back to its fiber. The tree, props and highlight features do not need
66
+ it. Do not add options to that call expecting them to do anything; the
67
+ supported channel is `extraDevToolsConfig` on the host config.
68
+
69
+ ## react-refresh {#react-refresh}
70
+
71
+ **Supported — `react-x11 dev` is the command, and `react-x11/refresh` the
72
+ entry point under it.** Fast Refresh without a bundler: edit a component,
73
+ and the mounted window updates in place with hook state intact. The X
74
+ connection, the window, the task list, half-typed text in an input — all
75
+ survive.
76
+
77
+ ```sh
78
+ npx react-x11 dev app.jsx
79
+ ```
80
+
81
+ A new app comes set up for it: `npm create react-x11@latest my-app` (or
82
+ `npx react-x11 create my-app`) writes `src/App.jsx`, an entry for a plain
83
+ run, and a `dev` script, with the toolchain below already in its
84
+ devDependencies. Beside them go `DESIGN.md`, the app's design brief, and
85
+ `AGENTS.md`, which tells a coding agent how react-x11 differs from
86
+ react-dom, where the docs installed with the package are, and how to check
87
+ its work by rendering the app to a PNG in light and dark
88
+ (`--no-agents-md` leaves it out).
89
+
90
+ ### `react-x11 dev` {#react-x11-dev}
91
+
92
+ The package's `bin`, and the route with nothing to set up. What it does,
93
+ each because the bare loader leaves it to the app:
94
+
95
+ - **It is the entry, and imports the file.** The file named on node's
96
+ command line is the one module hot-module-replacement never watches (see
97
+ below), so here the file is imported instead, which makes it an ordinary
98
+ hot module: an app in one file reloads.
99
+ - **It mounts the file's default export.** A file that mounts a root of its
100
+ own while it is imported runs as it is — `dev` sees the root appear and
101
+ mounts nothing — and then the modules it imports reload while it does
102
+ not. A file that runs both ways mounts itself behind
103
+ `if (!import.meta.hot)`, which is false under `dev` and true under `node`,
104
+ `tsx` and `bun`; `examples/tasks.jsx` is written that way. One that mounts
105
+ itself unguarded _and_ exports only components is told, at start, that
106
+ every save re-runs its `createRoot` and opens another window.
107
+ - **It supplies a `<window>` while the tree renders none.** The tree is
108
+ mounted as it is; a drawn element at the root throws an error with
109
+ `code: 'NOT_IN_WINDOW'` before anything commits, and on that `dev` mounts
110
+ it again inside a window titled after the file, sized to its content with
111
+ a 320×200 floor. Decided again on every reload: render a `<window>` and
112
+ `dev`'s goes (the component remounts, so its state resets — `dev` says
113
+ so), lose it and it comes back. A tree that renders a `<window>` _beside_
114
+ content in none stays wrapped, with a note saying which content needs a
115
+ window. Closing the window ends the process, as closing an app's primary
116
+ window does.
117
+ - **It says when a save will not reload.** A module reloads in place only
118
+ when everything it exports is a component by React Refresh's rule — a
119
+ capitalized name — so `export default () => …` and a module exporting a
120
+ constant beside its component run, but saving them reloads nothing. `dev`
121
+ says so, and why, when it starts, instead of leaving the first save to
122
+ find out.
123
+ - **It asks before it installs the toolchain.** The four packages below are
124
+ optional peer dependencies, so an app's users never download them; `dev`
125
+ finds the ones missing, pins them to the ranges react-x11 is written
126
+ against, and runs the project's package manager — from its lockfile, else
127
+ the one that launched `dev`, else npm — when you say yes, then starts
128
+ again. With no terminal to ask on it prints the command and exits.
129
+ - **It connects to [React DevTools](#react-devtools)** whenever
130
+ `npx react-devtools` is running, started before the app or after it —
131
+ wherever `react-devtools-core` is installed. Where it is not, and a
132
+ DevTools UI is listening when `dev` starts, `dev` prints the install
133
+ command. `REACT_X11_DEVTOOLS=0` turns it off.
134
+ - **It turns on [click-to-component](../click-to-component.md).**
135
+ Alt+Click (Option+Click on macOS) an element and its JSX line opens in
136
+ your editor. `REACT_X11_CLICK_TO_COMPONENT=0` turns it off.
137
+ - **It runs the project's react-x11.** A second copy of the renderer is a
138
+ root nobody refreshes, so a global or npx-cached `react-x11` hands over to
139
+ the copy the file imports, and a file that imports none is told to
140
+ install it.
141
+
142
+ **Errors are shown in a window**, as well as printed:
143
+
144
+ - **A render error** takes the app's tree down, as it would anywhere, and a
145
+ window titled `app.jsx — error` stands in for it. It shows the message, the
146
+ components it was thrown under, the source around the line, and the
147
+ stack's frames — React's, Node's and react-x11's own counted rather than
148
+ listed. The next save tries the app again; the save that fixes it brings
149
+ it back, and an app that throws again is back in the window.
150
+ - **A save that does not compile** opens the same window beside the app,
151
+ which keeps running the module's last version with its state; it closes
152
+ once the file compiles, or when you close it.
153
+ - A line in it opens in your editor on a click, the way
154
+ [click-to-component](../click-to-component.md) does, by
155
+ `REACT_X11_EDITOR` (`cursor` when it is unset).
156
+ - A throw from an event handler leaves the app running, so it is only
157
+ printed, and dispatch continues.
158
+
159
+ Stack lines point at the source's own lines, through every loader layer's
160
+ source map; `dev` turns source maps on for you. Arguments after the file
161
+ are the app's: `react-x11 dev app.jsx --size large` hands the app the
162
+ `process.argv` that `node app.jsx --size large` would. `.jsx` files are
163
+ compiled and hot; `.js` and `.mjs` run as Node runs them, and the `.jsx`
164
+ modules they import are hot. TypeScript is not supported yet.
165
+
166
+ ### Under `--import` {#refresh-import}
167
+
168
+ `dev` hosts the loader in its own process; a tool that hosts it itself —
169
+ a workbench, a custom dev loop — registers it with `--import`:
170
+
171
+ ```sh
172
+ node --enable-source-maps --import react-x11/refresh/register main.jsx
173
+ ```
174
+
175
+ The entry — the file named on the command line — is the one module that is
176
+ **never reloaded**: hot-module-replacement starts watching a module when
177
+ another module imports it, and nothing imports the entry. So an app written
178
+ as one file does not reload at all, and nothing says so. Keep the entry to
179
+ mounting, and the components in modules it imports:
180
+
181
+ ```jsx
182
+ // main.jsx — written once, never edited while the app runs
183
+ import React from 'react';
184
+ import ReactX11 from 'react-x11'; // default import: see the rules below
185
+ import App from './app.jsx'; // `export default function App() { ... }`
186
+
187
+ const root = await ReactX11.createRoot();
188
+ root.render(<App />);
189
+ ```
190
+
191
+ A save that does not compile is otherwise only printed — hot-module-replacement
192
+ catches the error, and the app keeps the module's last version — so a host
193
+ that wants to show it subscribes to the loader:
194
+
195
+ ```js
196
+ import { onTransform } from 'react-x11/refresh/loader';
197
+
198
+ onTransform(({ filename, error }) => {
199
+ // error is null when filename compiled
200
+ });
201
+ ```
202
+
203
+ A tool that needs the loader's options writes its own two-line `--import`
204
+ module instead of `react-x11/refresh/register`:
205
+
206
+ ```js
207
+ // tool-register.mjs, then: node --import ./tool-register.mjs app.jsx
208
+ import { registerRefresh } from 'react-x11/refresh/loader';
209
+
210
+ await registerRefresh({
211
+ extensions: ['.jsx'], // which files are hot modules
212
+ ignore: (path) => path.includes('/stores/'), // identity that must survive
213
+ prelude: ['globalThis.__TOOL__ = true'], // injected into every hot module
214
+ });
215
+ ```
216
+
217
+ ### How it reloads {#refresh-rules}
218
+
219
+ Both routes run the same loader. It instruments every `.jsx`
220
+ module and decides per module whether it is a **refresh boundary**: a
221
+ module whose exports are all components self-accepts, so an edit to it (or
222
+ to anything below it with no boundary of its own) re-evaluates up to that
223
+ module and re-renders the edited components in place. A component whose
224
+ hook signature changed remounts alone. There are no accept handlers to
225
+ write; the one optional seam is the reload event:
226
+
227
+ ```js
228
+ import { onReload } from 'react-x11/refresh';
229
+
230
+ onReload(({ urls, refreshed }) => {
231
+ console.log(`${urls.length} module(s) reloaded`);
232
+ });
233
+ ```
234
+
235
+ Node 22.15 or newer is required, for synchronous `module.registerHooks`,
236
+ and the toolchain rides four optional peer dependencies, which `dev` offers
237
+ to install and `--import` expects installed:
238
+
239
+ ```sh
240
+ npm install --save-dev @babel/core @babel/plugin-transform-react-jsx react-refresh hot-module-replacement
241
+ ```
242
+
243
+ Under the hood two loader layers stack: babel (classic JSX transform plus
244
+ `react-refresh/babel`) instruments the components, and above it
245
+ `hot-module-replacement` rewrites static imports into live bindings and
246
+ wires `import.meta.hot`. The runtime half patches the DevTools global hook
247
+ before the renderer registers with it — ordering the loader guarantees by
248
+ injecting the runtime import into every hot module's prelude. The hot
249
+ boundary excludes `node_modules` and react-x11's own sources, so React,
250
+ the renderer and `react-refresh/runtime` stay singletons.
251
+
252
+ Each layer puts lines of its own above a module — babel's the refresh
253
+ prelude, hot-module-replacement's a preamble — and each says so in its source
254
+ map and composes the map of the layer beneath it. So a stack through a hot
255
+ module points at the source's own line and column, which is what
256
+ click-to-component opens: run Node with `--enable-source-maps`, which
257
+ `dev` turns on itself. The composing half is hot-module-replacement
258
+ 4.0.2's, which is why that is the floor; `test/refresh-stack.test.js` fails
259
+ on 4.0.1.
260
+
261
+ The constraints are enforced errors now, not folklore:
262
+
263
+ - **A named import called at module top level inside a hot module is a
264
+ transform error.** Named imports become live bindings initialized in a
265
+ microtask, so at module scope the value is still undefined. Use the
266
+ default import at top level (`React.createContext(...)`, not
267
+ `createContext(...)`); inside components anything goes. The entry is
268
+ transformed like the rest, so a named `createRoot` import followed by a
269
+ top-level `await createRoot()` is this error: write
270
+ `ReactX11.createRoot()` there.
271
+ - **Only the classic JSX runtime.** `jsxRuntime: 'automatic'` throws: the
272
+ automatic runtime appends its `react/jsx-runtime` import to the last
273
+ import's line, which the line-oriented import rewrite breaks.
274
+ - **One statement per prelude entry.** A `prelude` entry holding two
275
+ statements or a newline throws, because the import rewrite replaces a
276
+ whole import statement's span on its line.
277
+
278
+ What is _not_ enforced, so it is not rediscovered as a bug:
279
+
280
+ - Under `--import`, an edit to the entry is dropped, whatever the entry
281
+ exports: it is never watched (above), so editing it needs a restart. An
282
+ edit anywhere else reaches the tree only if a boundary sits on its import
283
+ chain; a module whose importers lead to the entry without passing a
284
+ component-only module is dropped the same way. Neither says anything —
285
+ `dev` says the second, at start, for the file it runs.
286
+ - **Module-scope side effects re-run on every reload** of the module and
287
+ of everything between it and its boundary. Keep hot modules'
288
+ top level idempotent, and keep identity that must survive (contexts,
289
+ stores) in modules outside the hot graph — via the `ignore` seam or by
290
+ extension. `registerElement` at module scope is safe: once the first hot
291
+ re-import begins, a duplicate registration replaces silently instead of
292
+ throwing (the
293
+ [re-registration policy](../extending.md#hot-reload)) — mounted nodes keep the old
294
+ prototype until they remount, the same staleness contract Fast Refresh
295
+ has for classes. Remember the named-import rule applies to it like
296
+ anything else: call it as `Host.registerElement(...)` through a
297
+ namespace or default import.
298
+ - Fast Refresh needs the dev reconciler; `NODE_ENV=production` is a
299
+ registration error. There is no production hot path, by design.
300
+
301
+ ## `node --inspect` {#node-inspect}
302
+
303
+ **Out of the box.** Node built-in.
304
+
305
+ Node's V8 inspector protocol: `node --inspect` exposes a debug port, and
306
+ Chrome's `chrome://inspect` (or VS Code's auto-attach) connects a full
307
+ DevTools instance — sourcemap-aware CPU profiler, heap snapshots, allocation
308
+ timelines, live debugging.
309
+
310
+ Everything that costs time in a react-x11 app is visible as ordinary JS
311
+ frames, which is why this is the recommended profiling path:
312
+
313
+ - **CPU profiles of commits.** Reconciler frames (`performUnitOfWork`,
314
+ `completeWork`), yoga layout (`calculateLayout`), style resolution and ntk
315
+ paint/flush show up in one call tree. That answers "is it React, layout, or
316
+ painting" in a single recording.
317
+ - **Heap snapshots of retained nodes.** Every drawn element is a retained
318
+ node object reachable from the ntk window's `_reactX11Node`. Snapshot,
319
+ filter the constructor list, and leaked subtrees — a `<popup>` that never
320
+ unmounted, listeners held after close — are directly countable. Two
321
+ snapshots plus "Objects allocated between…" isolates a leak per
322
+ interaction.
323
+
324
+ ```sh
325
+ # interactive: profile a running app from Chrome
326
+ node --inspect --import tsx examples/dashboard.jsx
327
+ # chrome://inspect -> Open dedicated DevTools for Node -> Performance / Memory
328
+
329
+ # capture-on-exit CPU profile, no UI needed
330
+ node --cpu-prof --cpu-prof-dir=./profiles --import tsx examples/dashboard.jsx
331
+
332
+ # heap snapshot of a live app without restarting it
333
+ kill -USR1 <pid> # inspector comes up on :9229, then attach and snapshot
334
+ ```
335
+
336
+ - **Profile with `NODE_ENV=production` when chasing React costs.** The dev
337
+ reconciler's stack-frame bookkeeping (`runWithFiberInDEV`) inflates commit
338
+ times and clutters the tree. For layout and paint costs it matters less.
339
+ - The inspector port is an arbitrary-code-execution door. Bind it to
340
+ `127.0.0.1` (the default) and never expose it on a network; for remote
341
+ boxes, tunnel it with `ssh -L 9229:localhost:9229`.
342
+ - This and the React DevTools Profiler are complementary: DevTools names the
343
+ _components_ per commit, the V8 profiler names the _functions_.
344
+
345
+ ## `0x` {#0x}
346
+
347
+ **Out of the box.** 0x@6.0.0.
348
+
349
+ A single-command flamegraph generator: `0x -- node app.js` runs the process
350
+ under the V8 sampling profiler and, on exit, writes a self-contained
351
+ interactive `flamegraph.html`. No integration at all, and the interesting
352
+ frames are directly searchable — `performUnitOfWork`/`completeWork`, `yoga`,
353
+ and your own components.
354
+
355
+ ```sh
356
+ # profile an example, write the output into a throwaway dir
357
+ npx 0x --output-dir /tmp/rx11-prof -- node --import tsx examples/dashboard.jsx
358
+ # interact with the window until the slow thing happens, then Ctrl-C
359
+ # -> file:///tmp/rx11-prof/flamegraph.html
360
+
361
+ # tips inside the flamegraph UI:
362
+ # search "yoga" -> layout cost
363
+ # search "performUnitOfWork" -> react reconcile cost
364
+ # "Merge" view -> collapses recursion for a cleaner ranking
365
+ ```
366
+
367
+ **Why 0x and not clinic.js:** Clinic's flame command is a wrapper around 0x,
368
+ so for flamegraphs it adds moving parts without adding signal. Its other
369
+ tools target server event-loop and async-I/O patterns that do not map onto a
370
+ GUI render loop, and the package has been dormant since early 2024 while 0x
371
+ keeps shipping.
372
+
373
+ - Sampling profiler: sub-millisecond handlers need a longer capture to show
374
+ up, so drive the interaction in a loop.
375
+ - Profile with `NODE_ENV=production` when measuring React itself.
376
+ - Running through the tsx loader is fine, though frames from `.jsx` files
377
+ show transformed function names. Component names usually survive.
378
+ - 0x answers "where does CPU time go", and says nothing about _which commit_
379
+ or _why it re-rendered_. Pair it with the DevTools Profiler and
380
+ why-did-you-render. For heap questions use `node --inspect`; 0x is CPU-only.
381
+
382
+ ## why-did-you-render {#why-did-you-render}
383
+
384
+ **Out of the box.** @welldone-software/why-did-you-render@10.0.1.
385
+
386
+ Patches the `React` namespace so that components you flag report _avoidable_
387
+ re-renders — props or hook state that changed by reference but are equal by
388
+ value. It hooks React itself, not `react-dom`, which is exactly why it works
389
+ under a custom renderer.
390
+
391
+ ```jsx
392
+ import React from 'react';
393
+ import whyDidYouRender from '@welldone-software/why-did-you-render';
394
+ import { createRoot } from 'react-x11';
395
+
396
+ whyDidYouRender(React); // before anything renders
397
+
398
+ const Label = ({ user }) => <text>{`hi ${user.name}`}</text>;
399
+ Label.whyDidYouRender = true;
400
+
401
+ // Every render passes a fresh-but-equal object — the classic wasted render:
402
+ const App = () => (
403
+ <window width={300} height={200} title="wdyr">
404
+ <box style={{ flexGrow: 1 }}>
405
+ <Label user={{ name: 'ada' }} />
406
+ </box>
407
+ </window>
408
+ );
409
+
410
+ (await createRoot()).render(<App />);
411
+ // terminal: "Label … props.user: different objects that are equal by value."
412
+ ```
413
+
414
+ - Props tracking reports a re-render caused by a new-but-deep-equal object
415
+ prop with `diffType: 'deepEquals'` and the offending path. Hook tracking
416
+ (`trackHooks`, on by default) reports `setState` with an equal-by-value
417
+ object the same way.
418
+ - The default notifier prints readable `console.group` output in a terminal —
419
+ no browser console needed. A custom `notifier` receives the structured
420
+ `{displayName, reason: {propsDifferences, hookDifferences}}` object, which
421
+ is also how you assert on wasted renders in tests.
422
+ - Works with the automatic JSX runtime as compiled by tsx and esbuild; the
423
+ babel alias the docs describe for webpack setups is not needed.
424
+ - **Install it before components are defined and before the first render** —
425
+ the patch wraps `React.createElement` and the hook entry points on the
426
+ namespace.
427
+ - Dev-only by design. With `NODE_ENV=production` React's prod build drops the
428
+ internals it reads; under plain `node`/`tsx` you get the dev build, which
429
+ is the right mode for it anyway.
430
+ - Updates scheduled outside an event handler flush on a delayed task in React
431
+ 19, so in a scripted repro, wait a macrotask before asserting the
432
+ notification fired.
433
+
434
+ This complements React DevTools: DevTools tells you _that_ a component
435
+ rendered; why-did-you-render tells you the render was _wasted_ and which
436
+ value to memoize.