react-x11 2.51.1 → 2.53.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/README.md +94 -61
- package/docs/README.md +256 -0
- package/docs/accessibility.md +575 -0
- package/docs/appearance.md +317 -0
- package/docs/click-to-component.md +92 -0
- package/docs/clipboard.md +262 -0
- package/docs/components.md +1231 -0
- package/docs/dbus.md +205 -0
- package/docs/debugging.md +371 -0
- package/docs/desktop-calendar.md +247 -0
- package/docs/desktop.md +523 -0
- package/docs/devtools.md +133 -0
- package/docs/drag-and-drop.md +585 -0
- package/docs/ecosystem/animation.md +360 -0
- package/docs/ecosystem/data-fetching.md +186 -0
- package/docs/ecosystem/dev-tooling.md +393 -0
- package/docs/ecosystem/forms.md +277 -0
- package/docs/ecosystem/headless.md +194 -0
- package/docs/ecosystem/i18n.md +86 -0
- package/docs/ecosystem/icons.md +377 -0
- package/docs/ecosystem/layout.md +206 -0
- package/docs/ecosystem/routing.md +135 -0
- package/docs/ecosystem/state.md +344 -0
- package/docs/ecosystem/testing.md +474 -0
- package/docs/ecosystem/theming.md +494 -0
- package/docs/ecosystem.md +439 -0
- package/docs/elements.md +2079 -0
- package/docs/embedding.md +239 -0
- package/docs/events.md +742 -0
- package/docs/extending.md +1700 -0
- package/docs/eyedropper.md +164 -0
- package/docs/filedialog.md +222 -0
- package/docs/frame.md +436 -0
- package/docs/gl.md +173 -0
- package/docs/globalmenu.md +195 -0
- package/docs/macos.md +592 -0
- package/docs/notifications.md +150 -0
- package/docs/packaging.md +468 -0
- package/docs/permissions.md +143 -0
- package/docs/react-features.md +405 -0
- package/docs/remote.md +179 -0
- package/docs/scale.md +225 -0
- package/docs/security.md +132 -0
- package/docs/styling.md +1710 -0
- package/docs/system.md +519 -0
- package/docs/testing.md +494 -0
- package/docs/typescript.md +75 -0
- package/docs/uri-schemes.md +340 -0
- package/docs/wayland-backend.md +332 -0
- package/docs/windows-integrations.md +177 -0
- package/docs/windows.md +380 -0
- package/package.json +11 -3
- package/src/Reconciler.js +2 -2
- package/src/a11y.js +1 -1
- package/src/backend/context2d.js +3 -3
- package/src/bootstrap.js +1 -1
- package/src/bus.js +39 -26
- package/src/cli/create.d.ts +43 -0
- package/src/cli/create.js +305 -0
- package/src/cli/env.js +484 -0
- package/src/cli/index.js +29 -2
- package/src/cli/templates/AGENTS.template.md +195 -0
- package/src/cli/templates/DESIGN.template.md +269 -0
- package/src/cocoa/app.js +6 -6
- package/src/cocoa/bezels.js +1 -1
- package/src/cocoa/fonts.js +1 -1
- package/src/cocoa/globalmenu.js +1 -1
- package/src/cocoa/keymap.js +1 -1
- package/src/cocoa/panewindow.js +1 -1
- package/src/cocoa/player.js +1 -1
- package/src/cocoa/presenter.js +2 -2
- package/src/cocoa/promotion.js +1 -1
- package/src/cocoa/relaunch.js +1 -1
- package/src/cocoa/threaded.js +1 -1
- package/src/cocoa/video.js +1 -1
- package/src/cocoa/window.js +4 -4
- package/src/components/Menu.js +7 -2
- package/src/components/Switch.js +1 -1
- package/src/components/native.js +1 -1
- package/src/desktopintegration.js +2 -1
- package/src/desktopsettings.js +193 -24
- package/src/dnd.js +2 -2
- package/src/filedialog.js +3 -3
- package/src/glnodes.js +1 -1
- package/src/grid.js +1 -1
- package/src/keyboardstate.js +28 -0
- package/src/layouts.js +1 -1
- package/src/node.d.ts +10 -10
- package/src/nodes/animation.js +1 -1
- package/src/nodes/invalidate.js +1 -1
- package/src/nodes/layouthost.js +1 -1
- package/src/nodes/video.js +19 -8
- package/src/nodes/window/droptarget.js +1 -1
- package/src/pacing.js +1 -1
- package/src/portal.js +31 -0
- package/src/refresh/index.js +2 -1
- package/src/screencolor.js +5 -10
- package/src/types/system.d.ts +8 -6
- package/src/types/video.d.ts +5 -2
- package/src/videobackend.js +28 -0
- package/src/videoframes.js +29 -16
- package/src/wayland/app.js +86 -0
- package/src/wayland/clipboard.js +14 -7
- package/src/wayland/connection.js +3 -0
- package/src/wayland/context2d.js +129 -10
- package/src/wayland/protocols/index.json +6 -0
- package/src/wayland/protocols/xdg-foreign-unstable-v2.json +1 -0
- package/src/wayland/seat.js +28 -1
- package/src/wayland/swapchain.js +23 -0
- package/src/wayland/video.js +376 -0
- package/src/wayland/xkb.js +137 -0
- package/src/win32/a11y.js +1 -1
- package/src/win32/app.js +2 -2
- package/src/win32/fonts.js +1 -1
- package/src/win32/glarea.js +4 -4
- package/src/win32/overlay.js +1 -1
- package/src/win32/panewindow.js +3 -3
- package/src/win32/shell.js +1 -1
- package/src/win32/window.js +2 -2
- package/src/windowid.js +1 -1
- package/src/xvideo.js +189 -0
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
# Developer tooling
|
|
2
|
+
|
|
3
|
+
A react-x11 app is an ordinary Node process, so V8 and React tooling apply
|
|
4
|
+
unmodified; nothing here needs an adapter. Which tool answers which
|
|
5
|
+
question:
|
|
6
|
+
|
|
7
|
+
| Question | Tool |
|
|
8
|
+
| ---------------------------------------------- | ------------------------------------------------ |
|
|
9
|
+
| Which component rendered, and with what props? | [React DevTools](#react-devtools) |
|
|
10
|
+
| Was that render _wasted_? | [why-did-you-render](#why-did-you-render) |
|
|
11
|
+
| Where did the milliseconds go? | [`node --inspect`](#node-inspect), [`0x`](#0x) |
|
|
12
|
+
| Why is memory growing? | [`node --inspect`](#node-inspect) heap snapshots |
|
|
13
|
+
|
|
14
|
+
`react-scan` does not work: its documented entry point does not resolve
|
|
15
|
+
under bare Node, and its programmatic API silently never fires
|
|
16
|
+
([register](../ecosystem.md#silent-failures)). React's own `<Profiler>`
|
|
17
|
+
works unchanged and covers commit timings.
|
|
18
|
+
|
|
19
|
+
## React DevTools {#react-devtools}
|
|
20
|
+
|
|
21
|
+
**Out of the box — already wired behind an env var.** react-devtools-core@7.0.1.
|
|
22
|
+
|
|
23
|
+
`react-devtools-core` is the DevTools backend for a React tree outside a
|
|
24
|
+
browser page: it connects over WebSocket to the standalone DevTools UI and
|
|
25
|
+
streams the fiber tree, props, hooks and profiler data. It costs nothing
|
|
26
|
+
when off.
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
npm i -D react-devtools-core
|
|
30
|
+
|
|
31
|
+
# terminal 1 — standalone UI, listens on :8097
|
|
32
|
+
npx react-devtools
|
|
33
|
+
|
|
34
|
+
# terminal 2 — any app
|
|
35
|
+
REACT_X11_DEVTOOLS=1 npm run examples:dashboard
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`REACT_X11_DEVTOOLS` is read on the first `render()`/`createRoot()`, which
|
|
39
|
+
installs the DevTools hook before the first commit, so every mounted root is
|
|
40
|
+
visible. `REACT_X11_DEVTOOLS_HOST` and `REACT_X11_DEVTOOLS_PORT` point the
|
|
41
|
+
backend at a UI elsewhere — useful when the app runs on a headless box next
|
|
42
|
+
to the X server.
|
|
43
|
+
|
|
44
|
+
Working: the live component tree, props/hooks/state inspection, and
|
|
45
|
+
highlight-on-hover — hovering an element in the tree tints its rect in the
|
|
46
|
+
window. See [devtools](../devtools.md) for the full walkthrough.
|
|
47
|
+
|
|
48
|
+
- **Start the standalone UI first**, or wait for the backend's retry. The
|
|
49
|
+
app side retries; the UI does not attach to an already-running app unless
|
|
50
|
+
the hook was installed at startup, and `REACT_X11_DEVTOOLS` cannot be
|
|
51
|
+
turned on after the fact.
|
|
52
|
+
- The Profiler's commit-oriented views (flamegraph, ranked) work with the
|
|
53
|
+
dev reconciler. The **Timeline** view does not — it needs
|
|
54
|
+
`injectProfilingHooks`, which react-reconciler 0.33 does not expose.
|
|
55
|
+
- DevTools cannot map a host node back to its fiber
|
|
56
|
+
(`findFiberByHostInstance` is never registered); the tree, props and
|
|
57
|
+
highlight do not need it.
|
|
58
|
+
|
|
59
|
+
Design notes: [design/ecosystem/dev-tooling.md](../../design/ecosystem/dev-tooling.md).
|
|
60
|
+
|
|
61
|
+
## react-refresh {#react-refresh}
|
|
62
|
+
|
|
63
|
+
**Supported — `react-x11 dev` is the command, and `react-x11/refresh` the
|
|
64
|
+
entry point under it.** Fast Refresh without a bundler: edit a component and
|
|
65
|
+
the mounted window updates in place with hook state intact — the
|
|
66
|
+
connection, the window, half-typed text in an input all survive.
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
npx react-x11 dev app.jsx
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
A new app comes set up for it: `npm create react-x11@latest my-app` (or
|
|
73
|
+
`npx react-x11 create my-app`) writes `src/App.jsx`, an entry for a plain
|
|
74
|
+
run, and a `dev` script, with the toolchain below already in its
|
|
75
|
+
devDependencies. Beside them go `DESIGN.md`, the app's design brief, and
|
|
76
|
+
`AGENTS.md`, which tells a coding agent how react-x11 differs from
|
|
77
|
+
react-dom, where the docs installed with the package are, and how to check
|
|
78
|
+
its work by rendering the app to a PNG in light and dark
|
|
79
|
+
(`--no-agents-md` leaves it out).
|
|
80
|
+
|
|
81
|
+
### `react-x11 dev` {#react-x11-dev}
|
|
82
|
+
|
|
83
|
+
The package's `bin`, with nothing to set up. What it does that the bare
|
|
84
|
+
loader leaves to the app:
|
|
85
|
+
|
|
86
|
+
- **It is the entry, and imports the file.** The file named on node's
|
|
87
|
+
command line is the one module that never reloads (see below), so `dev`
|
|
88
|
+
imports yours instead, making it an ordinary hot module: an app in one
|
|
89
|
+
file reloads.
|
|
90
|
+
- **It mounts the file's default export.** A file that mounts a root of its
|
|
91
|
+
own while it is imported runs as it is — `dev` sees the root appear and
|
|
92
|
+
mounts nothing — and then the modules it imports reload while it does
|
|
93
|
+
not. A file that runs both ways mounts itself behind
|
|
94
|
+
`if (!import.meta.hot)`, which is false under `dev` and true under `node`,
|
|
95
|
+
`tsx` and `bun`; `examples/tasks.jsx` is written that way. One that mounts
|
|
96
|
+
itself unguarded _and_ exports only components is told, at start, that
|
|
97
|
+
every save re-runs its `createRoot` and opens another window.
|
|
98
|
+
- **It supplies a `<window>` while the tree renders none.** A drawn
|
|
99
|
+
element at the root throws an error with `code: 'NOT_IN_WINDOW'` before
|
|
100
|
+
anything commits, and `dev` then mounts the tree inside a window titled
|
|
101
|
+
after the file, sized to its content with a 320×200 floor. Decided again
|
|
102
|
+
on every reload: render a `<window>` and `dev`'s goes (the component
|
|
103
|
+
remounts, so its state resets — `dev` says so); lose it and it comes
|
|
104
|
+
back. A tree that renders a `<window>` _beside_ content in none stays
|
|
105
|
+
wrapped, with a note saying which content needs a window. Closing the
|
|
106
|
+
window ends the process.
|
|
107
|
+
- **It says when a save will not reload.** A module reloads in place only
|
|
108
|
+
when everything it exports is a component by React Refresh's rule — a
|
|
109
|
+
capitalized name — so `export default () => …` and a module exporting a
|
|
110
|
+
constant beside its component run, but saving them reloads nothing. `dev`
|
|
111
|
+
says so, and why, when it starts, instead of leaving the first save to
|
|
112
|
+
find out.
|
|
113
|
+
- **It asks before it installs the toolchain.** The four packages below are
|
|
114
|
+
optional peer dependencies, so an app's users never download them. `dev`
|
|
115
|
+
finds the missing ones, pins them to react-x11's ranges, and on a yes runs
|
|
116
|
+
the project's package manager (from its lockfile, else the one that
|
|
117
|
+
launched `dev`, else npm) and starts again. With no terminal to ask on, it
|
|
118
|
+
prints the command and exits.
|
|
119
|
+
- **It connects to [React DevTools](#react-devtools)** whenever
|
|
120
|
+
`npx react-devtools` is running, started before the app or after it —
|
|
121
|
+
wherever `react-devtools-core` is installed. Where it is not, and a
|
|
122
|
+
DevTools UI is listening when `dev` starts, `dev` prints the install
|
|
123
|
+
command. `REACT_X11_DEVTOOLS=0` turns it off.
|
|
124
|
+
- **It turns on [click-to-component](../click-to-component.md).**
|
|
125
|
+
Alt+Click (Option+Click on macOS) an element and its JSX line opens in
|
|
126
|
+
your editor. `REACT_X11_CLICK_TO_COMPONENT=0` turns it off.
|
|
127
|
+
- **It runs the project's react-x11.** A global or npx-cached `react-x11`
|
|
128
|
+
hands over to the copy the file imports (a second copy would be a root
|
|
129
|
+
nobody refreshes); a file that imports none is told to install it.
|
|
130
|
+
|
|
131
|
+
**Errors are shown in a window**, as well as printed:
|
|
132
|
+
|
|
133
|
+
- **A render error** takes the app's tree down and a window titled
|
|
134
|
+
`app.jsx — error` stands in for it: the message, the components it was
|
|
135
|
+
thrown under, the source around the line, and the stack's frames (React's,
|
|
136
|
+
Node's and react-x11's own counted rather than listed). Each save tries
|
|
137
|
+
the app again.
|
|
138
|
+
- **A save that does not compile** opens the same window beside the app,
|
|
139
|
+
which keeps running the module's last version with its state; it closes
|
|
140
|
+
once the file compiles, or when you close it.
|
|
141
|
+
- Clicking a line opens it in your editor, as
|
|
142
|
+
[click-to-component](../click-to-component.md) does, by
|
|
143
|
+
`REACT_X11_EDITOR` (`cursor` when unset).
|
|
144
|
+
- A throw from an event handler leaves the app running; it is only printed,
|
|
145
|
+
and dispatch continues.
|
|
146
|
+
|
|
147
|
+
Stack lines point at the source's own lines (`dev` turns source maps on).
|
|
148
|
+
Arguments after the file are the app's: `react-x11 dev app.jsx --size large`
|
|
149
|
+
gives the app the `process.argv` that `node app.jsx --size large` would.
|
|
150
|
+
`.jsx` files are compiled and hot; `.js` and `.mjs` run as Node runs them,
|
|
151
|
+
and the `.jsx` modules they import are hot. TypeScript is not supported.
|
|
152
|
+
|
|
153
|
+
### Under `--import` {#refresh-import}
|
|
154
|
+
|
|
155
|
+
A tool that hosts the loader itself — a workbench, a custom dev loop —
|
|
156
|
+
registers it with `--import`:
|
|
157
|
+
|
|
158
|
+
```sh
|
|
159
|
+
node --enable-source-maps --import react-x11/refresh/register main.jsx
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The entry — the file named on the command line — is **never reloaded**: a
|
|
163
|
+
module is watched once another module imports it, and nothing imports the
|
|
164
|
+
entry. So a one-file app does not reload at all, silently. Keep the entry to
|
|
165
|
+
mounting, and the components in modules it imports:
|
|
166
|
+
|
|
167
|
+
```jsx
|
|
168
|
+
// main.jsx — written once, never edited while the app runs
|
|
169
|
+
import React from 'react';
|
|
170
|
+
import ReactX11 from 'react-x11'; // default import: see the rules below
|
|
171
|
+
import App from './app.jsx'; // `export default function App() { ... }`
|
|
172
|
+
|
|
173
|
+
const root = await ReactX11.createRoot();
|
|
174
|
+
root.render(<App />);
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
A save that does not compile is only printed (the app keeps the module's
|
|
178
|
+
last version); a host that wants to show it subscribes to the loader:
|
|
179
|
+
|
|
180
|
+
```js
|
|
181
|
+
import { onTransform } from 'react-x11/refresh/loader';
|
|
182
|
+
|
|
183
|
+
onTransform(({ filename, error }) => {
|
|
184
|
+
// error is null when filename compiled
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
A tool that needs the loader's options writes its own two-line `--import`
|
|
189
|
+
module instead of `react-x11/refresh/register`:
|
|
190
|
+
|
|
191
|
+
```js
|
|
192
|
+
// tool-register.mjs, then: node --import ./tool-register.mjs app.jsx
|
|
193
|
+
import { registerRefresh } from 'react-x11/refresh/loader';
|
|
194
|
+
|
|
195
|
+
await registerRefresh({
|
|
196
|
+
extensions: ['.jsx'], // which files are hot modules
|
|
197
|
+
ignore: (path) => path.includes('/stores/'), // identity that must survive
|
|
198
|
+
prelude: ['globalThis.__TOOL__ = true'], // injected into every hot module
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### How it reloads {#refresh-rules}
|
|
203
|
+
|
|
204
|
+
Both routes run the same loader. A module whose exports are all components
|
|
205
|
+
is a **refresh boundary**: an edit to it (or to anything below it with no
|
|
206
|
+
boundary of its own) re-evaluates up to that module and re-renders the
|
|
207
|
+
edited components in place. A component whose hook signature changed
|
|
208
|
+
remounts alone. There are no accept handlers to write; the one optional
|
|
209
|
+
seam is the reload event:
|
|
210
|
+
|
|
211
|
+
```js
|
|
212
|
+
import { onReload } from 'react-x11/refresh';
|
|
213
|
+
|
|
214
|
+
onReload(({ urls, refreshed }) => {
|
|
215
|
+
console.log(`${urls.length} module(s) reloaded`);
|
|
216
|
+
});
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Node 22.15 or newer is required (for synchronous `module.registerHooks`),
|
|
220
|
+
plus four optional peer dependencies, which `dev` offers to install and
|
|
221
|
+
`--import` expects installed:
|
|
222
|
+
|
|
223
|
+
```sh
|
|
224
|
+
npm install --save-dev @babel/core @babel/plugin-transform-react-jsx react-refresh hot-module-replacement
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
`node_modules` and react-x11's own sources are outside the hot boundary,
|
|
228
|
+
so React, the renderer and `react-refresh/runtime` stay singletons. A stack
|
|
229
|
+
through a hot module points at the source's own line and column (what
|
|
230
|
+
click-to-component opens) when Node runs with `--enable-source-maps`, which
|
|
231
|
+
`dev` turns on itself; this needs hot-module-replacement 4.0.2 or newer.
|
|
232
|
+
|
|
233
|
+
These constraints are enforced as errors:
|
|
234
|
+
|
|
235
|
+
- **A named import called at module top level inside a hot module is a
|
|
236
|
+
transform error.** Named imports become live bindings initialized in a
|
|
237
|
+
microtask, so at module scope the value is still undefined. Use the
|
|
238
|
+
default import at top level (`React.createContext(...)`, not
|
|
239
|
+
`createContext(...)`); inside components anything goes. The entry is
|
|
240
|
+
transformed like the rest, so a named `createRoot` import followed by a
|
|
241
|
+
top-level `await createRoot()` is this error: write
|
|
242
|
+
`ReactX11.createRoot()` there.
|
|
243
|
+
- **Only the classic JSX runtime.** `jsxRuntime: 'automatic'` throws: the
|
|
244
|
+
automatic runtime appends its `react/jsx-runtime` import to the last
|
|
245
|
+
import's line, which the line-oriented import rewrite breaks.
|
|
246
|
+
- **One statement per prelude entry.** A `prelude` entry holding two
|
|
247
|
+
statements or a newline throws.
|
|
248
|
+
|
|
249
|
+
Not enforced:
|
|
250
|
+
|
|
251
|
+
- Under `--import`, an edit to the entry is dropped, whatever it exports;
|
|
252
|
+
editing it needs a restart. An edit elsewhere reaches the tree only if a
|
|
253
|
+
boundary sits on its import chain; a module whose importers reach the
|
|
254
|
+
entry without passing a component-only module is dropped the same way.
|
|
255
|
+
Neither says anything — `dev` reports the second, at start, for the file
|
|
256
|
+
it runs.
|
|
257
|
+
- **Module-scope side effects re-run on every reload** of the module and of
|
|
258
|
+
everything between it and its boundary. Keep hot modules' top level
|
|
259
|
+
idempotent, and keep identity that must survive (contexts, stores) outside
|
|
260
|
+
the hot graph — via the `ignore` seam or by extension. `registerElement`
|
|
261
|
+
at module scope is safe: during hot reload a duplicate registration
|
|
262
|
+
replaces silently instead of throwing
|
|
263
|
+
([re-registration policy](../extending.md#hot-reload)), and mounted nodes
|
|
264
|
+
keep the old prototype until they remount. The named-import rule applies:
|
|
265
|
+
call it as `Host.registerElement(...)` through a namespace or default
|
|
266
|
+
import.
|
|
267
|
+
- Fast Refresh needs the dev reconciler; `NODE_ENV=production` is a
|
|
268
|
+
registration error. There is no production hot path.
|
|
269
|
+
|
|
270
|
+
## `node --inspect` {#node-inspect}
|
|
271
|
+
|
|
272
|
+
**Out of the box.** Node built-in.
|
|
273
|
+
|
|
274
|
+
`node --inspect` exposes a debug port, and Chrome's `chrome://inspect` (or
|
|
275
|
+
VS Code's auto-attach) connects a full DevTools instance — sourcemap-aware
|
|
276
|
+
CPU profiler, heap snapshots, allocation timelines, live debugging.
|
|
277
|
+
Everything that costs time in a react-x11 app is ordinary JS frames, so this
|
|
278
|
+
is the recommended profiling path:
|
|
279
|
+
|
|
280
|
+
- **CPU profiles of commits.** Reconciler frames (`performUnitOfWork`,
|
|
281
|
+
`completeWork`), yoga layout (`calculateLayout`), style resolution and ntk
|
|
282
|
+
paint/flush show up in one call tree: "is it React, layout, or painting"
|
|
283
|
+
in one recording.
|
|
284
|
+
- **Heap snapshots of retained nodes.** Every drawn element is a node object
|
|
285
|
+
reachable from the ntk window's `_reactX11Node`, so leaked subtrees — a
|
|
286
|
+
`<popup>` that never unmounted, listeners held after close — are directly
|
|
287
|
+
countable in the constructor list. Two snapshots plus "Objects allocated
|
|
288
|
+
between…" isolates a leak per interaction.
|
|
289
|
+
|
|
290
|
+
```sh
|
|
291
|
+
# interactive: profile a running app from Chrome
|
|
292
|
+
node --inspect --import tsx examples/dashboard.jsx
|
|
293
|
+
# chrome://inspect -> Open dedicated DevTools for Node -> Performance / Memory
|
|
294
|
+
|
|
295
|
+
# capture-on-exit CPU profile, no UI needed
|
|
296
|
+
node --cpu-prof --cpu-prof-dir=./profiles --import tsx examples/dashboard.jsx
|
|
297
|
+
|
|
298
|
+
# heap snapshot of a live app without restarting it
|
|
299
|
+
kill -USR1 <pid> # inspector comes up on :9229, then attach and snapshot
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
- **Profile with `NODE_ENV=production` when chasing React costs.** The dev
|
|
303
|
+
reconciler's bookkeeping (`runWithFiberInDEV`) inflates commit times and
|
|
304
|
+
clutters the tree; for layout and paint costs it matters less.
|
|
305
|
+
- The inspector port is an arbitrary-code-execution door. Keep it on
|
|
306
|
+
`127.0.0.1` (the default); for remote boxes, tunnel it with
|
|
307
|
+
`ssh -L 9229:localhost:9229`.
|
|
308
|
+
- The React DevTools Profiler names the _components_ per commit; the V8
|
|
309
|
+
profiler names the _functions_.
|
|
310
|
+
|
|
311
|
+
## `0x` {#0x}
|
|
312
|
+
|
|
313
|
+
**Out of the box.** 0x@6.0.0.
|
|
314
|
+
|
|
315
|
+
`0x -- node app.js` runs the process under the V8 sampling profiler and, on
|
|
316
|
+
exit, writes a self-contained interactive `flamegraph.html`, with
|
|
317
|
+
`performUnitOfWork`/`completeWork`, `yoga` and your components searchable.
|
|
318
|
+
|
|
319
|
+
```sh
|
|
320
|
+
# profile an example, write the output into a throwaway dir
|
|
321
|
+
npx 0x --output-dir /tmp/rx11-prof -- node --import tsx examples/dashboard.jsx
|
|
322
|
+
# interact with the window until the slow thing happens, then Ctrl-C
|
|
323
|
+
# -> file:///tmp/rx11-prof/flamegraph.html
|
|
324
|
+
|
|
325
|
+
# tips inside the flamegraph UI:
|
|
326
|
+
# search "yoga" -> layout cost
|
|
327
|
+
# search "performUnitOfWork" -> react reconcile cost
|
|
328
|
+
# "Merge" view -> collapses recursion for a cleaner ranking
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
clinic.js's flame command wraps 0x; use 0x directly.
|
|
332
|
+
|
|
333
|
+
- Sampling profiler: sub-millisecond handlers need a longer capture, so
|
|
334
|
+
drive the interaction in a loop.
|
|
335
|
+
- Profile with `NODE_ENV=production` when measuring React itself.
|
|
336
|
+
- Under the tsx loader, frames from `.jsx` files show transformed function
|
|
337
|
+
names; component names usually survive.
|
|
338
|
+
- 0x is CPU-only and says nothing about _which commit_ or _why it
|
|
339
|
+
re-rendered_; pair it with the DevTools Profiler and why-did-you-render,
|
|
340
|
+
and use `node --inspect` for heap questions.
|
|
341
|
+
|
|
342
|
+
## why-did-you-render {#why-did-you-render}
|
|
343
|
+
|
|
344
|
+
**Out of the box.** @welldone-software/why-did-you-render@10.0.1.
|
|
345
|
+
|
|
346
|
+
Patches the `React` namespace so flagged components report _avoidable_
|
|
347
|
+
re-renders — props or hook state changed by reference but equal by value.
|
|
348
|
+
It hooks React, not `react-dom`, so it works under any renderer.
|
|
349
|
+
|
|
350
|
+
```jsx
|
|
351
|
+
import React from 'react';
|
|
352
|
+
import whyDidYouRender from '@welldone-software/why-did-you-render';
|
|
353
|
+
import { createRoot } from 'react-x11';
|
|
354
|
+
|
|
355
|
+
whyDidYouRender(React); // before anything renders
|
|
356
|
+
|
|
357
|
+
const Label = ({ user }) => <text>{`hi ${user.name}`}</text>;
|
|
358
|
+
Label.whyDidYouRender = true;
|
|
359
|
+
|
|
360
|
+
// Every render passes a fresh-but-equal object — the classic wasted render:
|
|
361
|
+
const App = () => (
|
|
362
|
+
<window width={300} height={200} title="wdyr">
|
|
363
|
+
<box style={{ flexGrow: 1 }}>
|
|
364
|
+
<Label user={{ name: 'ada' }} />
|
|
365
|
+
</box>
|
|
366
|
+
</window>
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
(await createRoot()).render(<App />);
|
|
370
|
+
// terminal: "Label … props.user: different objects that are equal by value."
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
- A re-render caused by a new-but-deep-equal object prop is reported with
|
|
374
|
+
`diffType: 'deepEquals'` and the offending path; hook tracking
|
|
375
|
+
(`trackHooks`, on by default) reports `setState` with an equal-by-value
|
|
376
|
+
object the same way.
|
|
377
|
+
- The default notifier prints `console.group` output in the terminal. A
|
|
378
|
+
custom `notifier` receives the structured
|
|
379
|
+
`{displayName, reason: {propsDifferences, hookDifferences}}` object — how
|
|
380
|
+
you assert on wasted renders in tests.
|
|
381
|
+
- Works with the automatic JSX runtime as compiled by tsx and esbuild; the
|
|
382
|
+
babel alias its docs describe for webpack is not needed.
|
|
383
|
+
- **Install it before components are defined and before the first render**
|
|
384
|
+
— it wraps `React.createElement` and the hook entry points on the
|
|
385
|
+
namespace.
|
|
386
|
+
- Dev-only: React's production build drops the internals it reads. Plain
|
|
387
|
+
`node`/`tsx` gives the dev build.
|
|
388
|
+
- Updates scheduled outside an event handler flush on a delayed task in
|
|
389
|
+
React 19, so in a scripted repro wait a macrotask before asserting the
|
|
390
|
+
notification fired.
|
|
391
|
+
|
|
392
|
+
DevTools tells you _that_ a component rendered; why-did-you-render tells you
|
|
393
|
+
the render was _wasted_ and which value to memoize.
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# Forms and validation
|
|
2
|
+
|
|
3
|
+
There is no `<form>` element, so nothing submits itself — wire
|
|
4
|
+
`handleSubmit` to a `Button`'s `onPress` or to `<textinput onSubmit>`.
|
|
5
|
+
|
|
6
|
+
Everything else follows from **one signature**. Every value control — the
|
|
7
|
+
host elements `<textinput>` and `<textarea>`, and the widgets `Checkbox`,
|
|
8
|
+
`Switch`, `RadioGroup`, `Select` and `Slider` — calls `onChange(ev)` with a
|
|
9
|
+
change event carrying `ev.value`, `ev.target.value`, `ev.target.name` and,
|
|
10
|
+
for a checkbox, `ev.target.checked`. That is the shape DOM form libraries
|
|
11
|
+
read, so a library's handler goes straight in, with no per-widget bridge:
|
|
12
|
+
|
|
13
|
+
```jsx
|
|
14
|
+
<textinput name="host" value={f.values.host} onChange={f.handleChange} />
|
|
15
|
+
<Checkbox name="agree" checked={f.values.agree} onChange={f.handleChange} />
|
|
16
|
+
<Select name="size" value={f.values.size} options={sizes} onChange={f.handleChange} />
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
| library | how it goes in |
|
|
20
|
+
| ---------------------- | --------------------------------------------------------- |
|
|
21
|
+
| `react-hook-form` | `{...register('host')}`, or `<Controller>` for a widget |
|
|
22
|
+
| `formik` | `onChange={handleChange}`, `getFieldProps`, or `<Field>` |
|
|
23
|
+
| `downshift` | `getInputProps()` spread onto a `<textinput>` |
|
|
24
|
+
| `@tanstack/react-form` | `onChange={(ev) => field.handleChange(ev.value)}` — below |
|
|
25
|
+
|
|
26
|
+
Each row was run headless against the renderer: react-hook-form@7.84.0,
|
|
27
|
+
formik@2.4.9, downshift@9.4.0, @tanstack/react-form@1.33.2.
|
|
28
|
+
|
|
29
|
+
**The one thing to know**: a library whose field contract is
|
|
30
|
+
value-in/value-out — TanStack Form here — needs the value taken off the
|
|
31
|
+
event; a bare `onChange={field.handleChange}` stores the _event object_ as
|
|
32
|
+
the field value with no error.
|
|
33
|
+
|
|
34
|
+
A control that takes a `name` is a form field and reports an event. `Tabs`,
|
|
35
|
+
`Table` and the menus are not, and keep their plain callbacks
|
|
36
|
+
([components](../components.md#the-change-event-and-name)).
|
|
37
|
+
|
|
38
|
+
## TanStack Form {#tanstack-form}
|
|
39
|
+
|
|
40
|
+
**Out of the box.** @tanstack/react-form@1.33.2 (form-core 1.33.2).
|
|
41
|
+
|
|
42
|
+
A headless form state manager: field values, dirty and touched tracking,
|
|
43
|
+
sync and async validation (Standard Schema, so zod/valibot/arktype plug in
|
|
44
|
+
directly), and submission. The field contract is `field.state.value` +
|
|
45
|
+
`field.handleChange(value)` — a **value, not an event** — so each field
|
|
46
|
+
takes the value off the control's event, the same expression a DOM app
|
|
47
|
+
writes, for a `<textinput>` and a `Checkbox` alike:
|
|
48
|
+
|
|
49
|
+
```jsx
|
|
50
|
+
onChange={(ev) => field.handleChange(ev.value)}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The bare `onChange={field.handleChange}` stores the event object as the
|
|
54
|
+
field's value — no error, no warning, and validation sees an object where it
|
|
55
|
+
expected a string.
|
|
56
|
+
|
|
57
|
+
```jsx
|
|
58
|
+
import React from 'react';
|
|
59
|
+
import { useForm } from '@tanstack/react-form';
|
|
60
|
+
import { Button } from 'react-x11';
|
|
61
|
+
import { z } from 'zod';
|
|
62
|
+
|
|
63
|
+
function ConnectForm({ connect }) {
|
|
64
|
+
const form = useForm({
|
|
65
|
+
defaultValues: { host: '', display: ':0' },
|
|
66
|
+
onSubmit: async ({ value }) => connect(value),
|
|
67
|
+
});
|
|
68
|
+
return (
|
|
69
|
+
<box style={{ flexDirection: 'column', gap: 8 }}>
|
|
70
|
+
<form.Field
|
|
71
|
+
name="host"
|
|
72
|
+
validators={{ onChange: z.string().min(3, 'too short') }}
|
|
73
|
+
>
|
|
74
|
+
{(field) => (
|
|
75
|
+
<box style={{ flexDirection: 'column' }}>
|
|
76
|
+
<textinput
|
|
77
|
+
value={field.state.value}
|
|
78
|
+
onChange={(ev) => field.handleChange(ev.value)}
|
|
79
|
+
/>
|
|
80
|
+
{field.state.meta.errors.length > 0 && (
|
|
81
|
+
<text style={{ color: '#cc4444' }}>
|
|
82
|
+
{field.state.meta.errors[0].message}
|
|
83
|
+
</text>
|
|
84
|
+
)}
|
|
85
|
+
</box>
|
|
86
|
+
)}
|
|
87
|
+
</form.Field>
|
|
88
|
+
<Button label="Connect" onPress={() => form.handleSubmit()} />
|
|
89
|
+
</box>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
- There is no submit-on-Enter for free. Wire `form.handleSubmit()` to a
|
|
95
|
+
button's `onPress` and/or a window-level Enter key handler.
|
|
96
|
+
- Validation errors from a zod schema are error _objects_
|
|
97
|
+
(`errors[0].message`); a plain string validator returns strings. A
|
|
98
|
+
`<text>` child must be a string, so render `.message`.
|
|
99
|
+
- The SSR and Next helpers live under subpaths; the root import pulls in no
|
|
100
|
+
react-dom.
|
|
101
|
+
|
|
102
|
+
## zod {#zod}
|
|
103
|
+
|
|
104
|
+
**Out of the box.** zod@4.4.3.
|
|
105
|
+
|
|
106
|
+
Schema validation, pure computation. Where it plugs in:
|
|
107
|
+
|
|
108
|
+
- **TanStack Form** — pass a schema straight into a field's
|
|
109
|
+
`validators: { onChange: z.string().min(3, 'too short') }`. Zod 4
|
|
110
|
+
implements Standard Schema, so no resolver package is needed.
|
|
111
|
+
- **React Hook Form** — via `@hookform/resolvers/zod`, with either
|
|
112
|
+
`register()` or `Controller`; see below.
|
|
113
|
+
- **Standalone** — validating a settings file or a config blob before
|
|
114
|
+
rendering is just `schema.safeParse(data)`.
|
|
115
|
+
|
|
116
|
+
```jsx
|
|
117
|
+
import { z } from 'zod';
|
|
118
|
+
|
|
119
|
+
const settings = z.object({
|
|
120
|
+
display: z.string().regex(/^:\d+$/, 'DISPLAY looks like :0'),
|
|
121
|
+
scale: z.coerce.number().min(0.5).max(3).default(1),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
function Settings({ raw }) {
|
|
125
|
+
const parsed = settings.safeParse(raw);
|
|
126
|
+
if (!parsed.success) {
|
|
127
|
+
// render the issues instead of crashing the app
|
|
128
|
+
return (
|
|
129
|
+
<box style={{ flexDirection: 'column' }}>
|
|
130
|
+
{parsed.error.issues.map((i) => (
|
|
131
|
+
<text
|
|
132
|
+
key={i.path.join('.')}
|
|
133
|
+
>{`${i.path.join('.')}: ${i.message}`}</text>
|
|
134
|
+
))}
|
|
135
|
+
</box>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return <text>display {parsed.data.display}</text>;
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
- Zod 4 field errors surface as issue objects; render `issue.message`, not
|
|
143
|
+
the issue itself.
|
|
144
|
+
- `valibot` and `arktype` are equally viable (also Standard Schema).
|
|
145
|
+
- Bundle-size arguments from the web do not apply — an X11 app ships no
|
|
146
|
+
bundle.
|
|
147
|
+
|
|
148
|
+
## React Hook Form {#react-hook-form}
|
|
149
|
+
|
|
150
|
+
**Out of the box, both APIs.** react-hook-form@7.84.0 with
|
|
151
|
+
@hookform/resolvers@5.5.7.
|
|
152
|
+
|
|
153
|
+
`register()` returns DOM-shaped props (`{name, ref, onChange(e),
|
|
154
|
+
onBlur(e)}`) and reads `e.target.value` off the event, which is what
|
|
155
|
+
`<textinput>` provides, so the spread works:
|
|
156
|
+
|
|
157
|
+
```jsx
|
|
158
|
+
const { register, handleSubmit } = useForm({ defaultValues: { host: '' } });
|
|
159
|
+
<textinput {...register('host')} />;
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The `ref` RHF attaches lands on a node whose `value` is writable (RHF
|
|
163
|
+
assigns `ref.value = ''` while registering). Through `register()`, typing
|
|
164
|
+
updates `getValues()`, `handleSubmit` receives the values,
|
|
165
|
+
`setValue(name, v)` writes through the ref so the field redraws, and
|
|
166
|
+
`setFocus(name)` calls the node's `.focus()`.
|
|
167
|
+
|
|
168
|
+
**`reset()` updates form state but does not clear an uncontrolled field's
|
|
169
|
+
text.** RHF writes values back through refs on reset only when its `isWeb`
|
|
170
|
+
probe (`typeof window !== 'undefined' && window.HTMLElement && document`)
|
|
171
|
+
passes, which it never does here; `setValue()` is not gated. To have
|
|
172
|
+
`reset()` clear the display, make the field controlled with `<Controller>`,
|
|
173
|
+
or assign `ref.current.value = ''` yourself.
|
|
174
|
+
|
|
175
|
+
`<Controller>` / `useController` is the choice for a controlled field.
|
|
176
|
+
`field.onChange` accepts a plain value or an event, so both are fine:
|
|
177
|
+
|
|
178
|
+
```jsx
|
|
179
|
+
onChange={(ev) => field.onChange(ev.target.value)} // explicit
|
|
180
|
+
onChange={field.onChange} // RHF unwraps .target
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
```jsx
|
|
184
|
+
import React from 'react';
|
|
185
|
+
import { useForm, Controller } from 'react-hook-form';
|
|
186
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
187
|
+
import { Button } from 'react-x11';
|
|
188
|
+
import { z } from 'zod';
|
|
189
|
+
|
|
190
|
+
const schema = z.object({
|
|
191
|
+
host: z.string().min(3, 'too short'),
|
|
192
|
+
display: z.string(),
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
function ConnectForm({ connect }) {
|
|
196
|
+
const { control, handleSubmit } = useForm({
|
|
197
|
+
defaultValues: { host: '', display: ':0' },
|
|
198
|
+
resolver: zodResolver(schema),
|
|
199
|
+
mode: 'onChange',
|
|
200
|
+
});
|
|
201
|
+
return (
|
|
202
|
+
<box style={{ flexDirection: 'column', gap: 8 }}>
|
|
203
|
+
<Controller
|
|
204
|
+
name="host"
|
|
205
|
+
control={control}
|
|
206
|
+
render={({ field, fieldState }) => (
|
|
207
|
+
<box style={{ flexDirection: 'column' }}>
|
|
208
|
+
<textinput
|
|
209
|
+
value={field.value}
|
|
210
|
+
onChange={field.onChange}
|
|
211
|
+
onBlur={field.onBlur}
|
|
212
|
+
/>
|
|
213
|
+
{fieldState.error && <text>{fieldState.error.message}</text>}
|
|
214
|
+
</box>
|
|
215
|
+
)}
|
|
216
|
+
/>
|
|
217
|
+
<Button label="Connect" onPress={handleSubmit(connect)} />
|
|
218
|
+
</box>
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
- A `Controller` field re-renders its subtree on every keystroke;
|
|
224
|
+
`register()` does not — the same performance argument RHF makes on the
|
|
225
|
+
web.
|
|
226
|
+
- `handleSubmit` returns a function. Call it: `handleSubmit(fn)` in an
|
|
227
|
+
`onPress` is the curried form, and there is no submit event to pass.
|
|
228
|
+
|
|
229
|
+
## Formik {#formik}
|
|
230
|
+
|
|
231
|
+
**Out of the box.** formik@2.4.9.
|
|
232
|
+
|
|
233
|
+
All three of Formik's field styles work:
|
|
234
|
+
|
|
235
|
+
```jsx
|
|
236
|
+
import { Formik, Field, useFormik } from 'formik';
|
|
237
|
+
|
|
238
|
+
// 1. the spread
|
|
239
|
+
<textinput {...formik.getFieldProps('host')} />
|
|
240
|
+
|
|
241
|
+
// 2. by hand
|
|
242
|
+
<textinput
|
|
243
|
+
name="host"
|
|
244
|
+
value={formik.values.host}
|
|
245
|
+
onChange={formik.handleChange}
|
|
246
|
+
onBlur={formik.handleBlur}
|
|
247
|
+
/>
|
|
248
|
+
|
|
249
|
+
// 3. <Field> with a render prop
|
|
250
|
+
<Field name="host">
|
|
251
|
+
{({ field }) => <textinput {...field} />}
|
|
252
|
+
</Field>
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
`handleChange` reads `target.name` to decide which field to write, so the
|
|
256
|
+
`name` prop is required in all of these. The widgets take the same handler:
|
|
257
|
+
|
|
258
|
+
```jsx
|
|
259
|
+
<Checkbox
|
|
260
|
+
name="agree"
|
|
261
|
+
checked={formik.values.agree}
|
|
262
|
+
onChange={formik.handleChange}
|
|
263
|
+
>
|
|
264
|
+
I agree
|
|
265
|
+
</Checkbox>
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
`ev.target` is `{ type: 'checkbox', name, value, checked }`, which is what
|
|
269
|
+
`handleChange` destructures — `type` is how it knows to take `checked`.
|
|
270
|
+
|
|
271
|
+
- `<Formik>`'s render-prop form and `useFormik` both work; there is no
|
|
272
|
+
`<form>` element, so call `submitForm()` (or `handleSubmit()`) yourself
|
|
273
|
+
from a `Button` or from `<textinput onSubmit>`.
|
|
274
|
+
- Passing `handleChange` a non-event object — a hand-rolled fake event with
|
|
275
|
+
no `target` — throws
|
|
276
|
+
`TypeError: Cannot read properties of undefined (reading 'type')` rather
|
|
277
|
+
than no-opping. Pass the real event.
|