react-x11 2.51.1 → 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.
- package/README.md +87 -54
- package/docs/README.md +410 -0
- package/docs/accessibility.md +628 -0
- package/docs/appearance.md +416 -0
- package/docs/architecture/accessibility.md +470 -0
- package/docs/architecture/animation.md +707 -0
- package/docs/architecture/container-queries.md +525 -0
- package/docs/architecture/custom-layout.md +334 -0
- package/docs/architecture/drag-and-drop.md +941 -0
- package/docs/architecture/element-layer-contents.md +622 -0
- package/docs/architecture/frame-pacing.md +255 -0
- package/docs/architecture/grid-layout.md +358 -0
- package/docs/architecture/protocol-efficiency.md +357 -0
- package/docs/architecture/video.md +1149 -0
- package/docs/architecture/windowed-regions.md +854 -0
- package/docs/click-to-component.md +125 -0
- package/docs/clipboard.md +281 -0
- package/docs/components.md +1749 -0
- package/docs/dbus.md +277 -0
- package/docs/debugging.md +464 -0
- package/docs/desktop-calendar.md +284 -0
- package/docs/desktop.md +648 -0
- package/docs/devtools.md +250 -0
- package/docs/drag-and-drop.md +663 -0
- package/docs/ecosystem/animation.md +394 -0
- package/docs/ecosystem/data-fetching.md +201 -0
- package/docs/ecosystem/dev-tooling.md +436 -0
- package/docs/ecosystem/forms.md +313 -0
- package/docs/ecosystem/headless.md +219 -0
- package/docs/ecosystem/i18n.md +89 -0
- package/docs/ecosystem/icons.md +410 -0
- package/docs/ecosystem/layout.md +216 -0
- package/docs/ecosystem/routing.md +146 -0
- package/docs/ecosystem/state.md +361 -0
- package/docs/ecosystem/testing.md +500 -0
- package/docs/ecosystem/theming.md +540 -0
- package/docs/ecosystem.md +473 -0
- package/docs/elements.md +2762 -0
- package/docs/embedding.md +288 -0
- package/docs/events.md +1025 -0
- package/docs/extending.md +2242 -0
- package/docs/eyedropper.md +223 -0
- package/docs/filedialog.md +258 -0
- package/docs/frame.md +580 -0
- package/docs/gl.md +170 -0
- package/docs/globalmenu.md +278 -0
- package/docs/glx.md +137 -0
- package/docs/macos.md +3295 -0
- package/docs/notifications.md +164 -0
- package/docs/packaging.md +611 -0
- package/docs/permissions.md +148 -0
- package/docs/react-features.md +430 -0
- package/docs/remote.md +223 -0
- package/docs/scale.md +339 -0
- package/docs/security.md +132 -0
- package/docs/styling.md +1928 -0
- package/docs/system.md +599 -0
- package/docs/testing.md +514 -0
- package/docs/typescript.md +117 -0
- package/docs/uri-schemes.md +380 -0
- package/docs/wayland-backend.md +591 -0
- package/docs/wayland.md +545 -0
- package/docs/windows-embedding.md +271 -0
- package/docs/windows-gl.md +221 -0
- package/docs/windows-integrations.md +233 -0
- package/docs/windows.md +1404 -0
- package/package.json +9 -2
- 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/desktopintegration.js +2 -1
- package/src/desktopsettings.js +193 -24
- package/src/filedialog.js +3 -3
- package/src/keyboardstate.js +28 -0
- package/src/node.d.ts +10 -10
- package/src/portal.js +31 -0
- package/src/screencolor.js +5 -10
- package/src/types/system.d.ts +8 -6
- package/src/wayland/app.js +59 -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/xkb.js +137 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Permissions
|
|
2
|
+
|
|
3
|
+
May this app use the camera, the microphone, the screen, the accessibility
|
|
4
|
+
APIs — and how does it ask?
|
|
5
|
+
|
|
6
|
+
```jsx
|
|
7
|
+
import { usePermission } from 'react-x11';
|
|
8
|
+
|
|
9
|
+
function RecordButton() {
|
|
10
|
+
const mic = usePermission('microphone');
|
|
11
|
+
|
|
12
|
+
if (!mic.available) return <Button label="Record" onPress={record} />;
|
|
13
|
+
if (mic.status === 'granted')
|
|
14
|
+
return <Button label="Record" onPress={record} />;
|
|
15
|
+
return (
|
|
16
|
+
<Button
|
|
17
|
+
label={mic.status === 'prompt' ? 'Allow microphone' : 'Open Settings'}
|
|
18
|
+
onPress={() =>
|
|
19
|
+
mic.status === 'prompt' ? mic.request() : mic.openSettings()
|
|
20
|
+
}
|
|
21
|
+
/>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## The ladder
|
|
27
|
+
|
|
28
|
+
The [file dialog](filedialog.md)'s shape again, with one rung that answers and
|
|
29
|
+
one that only points:
|
|
30
|
+
|
|
31
|
+
| | | |
|
|
32
|
+
| --- | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
33
|
+
| 1 | **the platform's authorization** | The [cocoa backend](macos.md): macOS's privacy (TCC) status per kind, and the system's own prompt where a framework offers one — `AVCaptureDevice` for the camera and microphone, `CoreLocation`, and the rest. |
|
|
34
|
+
| 2 | **the Settings pane** | `open x-apple.systempreferences:…` — any Mac, bridge or not. It cannot read a status or raise a prompt; it puts the user in front of the switch, which is all `openPrivacySettings` promises. |
|
|
35
|
+
|
|
36
|
+
**On Linux nothing answers yet.** The freedesktop counterparts are the
|
|
37
|
+
per-device portals (`org.freedesktop.portal.Camera`, `.Location`, …), which
|
|
38
|
+
need no bridge and are
|
|
39
|
+
[#466](https://github.com/sidorares/react-x11/issues/466)'s remaining half.
|
|
40
|
+
Until then a status query says `'unknown'` and a request rejects with a
|
|
41
|
+
**typed** `NoPermissionServiceError` — the signal to keep the feature behind
|
|
42
|
+
the answer, not a crash. `usePermission().available` is that branch as render
|
|
43
|
+
state.
|
|
44
|
+
|
|
45
|
+
The top rung is never chosen by naming a backend: the call asks the connection
|
|
46
|
+
the tree renders through whether it has an authorization API, and the cocoa
|
|
47
|
+
backend says so by carrying one.
|
|
48
|
+
|
|
49
|
+
## The vocabulary
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
type PermissionKind =
|
|
53
|
+
| 'camera'
|
|
54
|
+
| 'microphone'
|
|
55
|
+
| 'screen-recording'
|
|
56
|
+
| 'accessibility'
|
|
57
|
+
| 'input-monitoring'
|
|
58
|
+
| 'automation'
|
|
59
|
+
| 'location'
|
|
60
|
+
| 'calendars'
|
|
61
|
+
| 'reminders';
|
|
62
|
+
|
|
63
|
+
type PermissionStatus =
|
|
64
|
+
'granted' | 'denied' | 'restricted' | 'prompt' | 'write-only' | 'unknown';
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
A status is one of six words, and two of them are not the platform's:
|
|
68
|
+
|
|
69
|
+
| | |
|
|
70
|
+
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
71
|
+
| `'granted'` | use it |
|
|
72
|
+
| `'denied'` | the user said no; `openPrivacySettings()` is the way back |
|
|
73
|
+
| `'restricted'` | MDM or parental controls — the user **cannot** grant it, so no Settings button |
|
|
74
|
+
| `'prompt'` | not decided yet — `request()` would ask |
|
|
75
|
+
| `'write-only'` | macOS 14's partial grant for `calendars` and `reminders`: the app may **save** an item it cannot read. A grant to a writer and a refusal to a reader, so it crosses as its own word rather than being flattened into one of them |
|
|
76
|
+
| `'unknown'` | nothing here can say. A fact about the machine, not the permission, and the reason a status query never throws |
|
|
77
|
+
|
|
78
|
+
A request answers with the **status after the user answered**, never a bare
|
|
79
|
+
boolean, because `'denied'` and `'restricted'` want different UI.
|
|
80
|
+
|
|
81
|
+
## `usePermission()`
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
const { status, available, request, refresh, openSettings } = usePermission(kind, { target? });
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`status` starts `'unknown'` and settles once read. `request()` resolves with
|
|
88
|
+
the status after the user answered and updates `status` with it; a second call
|
|
89
|
+
while one is in flight returns the same promise, so a double-clicked button
|
|
90
|
+
asks once. There is **no change notification** behind a status on any
|
|
91
|
+
platform — the status is what was last read, on mount and after each
|
|
92
|
+
`request()`; `refresh()` re-reads it on your own cue, a window coming back to
|
|
93
|
+
the front being the usual one.
|
|
94
|
+
|
|
95
|
+
## The bare functions
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
import {
|
|
99
|
+
permissionStatus,
|
|
100
|
+
requestPermission,
|
|
101
|
+
openPrivacySettings,
|
|
102
|
+
permissionBackend,
|
|
103
|
+
} from 'react-x11';
|
|
104
|
+
|
|
105
|
+
await permissionStatus('camera'); // never rejects for anything about the machine
|
|
106
|
+
await requestPermission('camera'); // rejects with NoPermissionServiceError where nothing can ask
|
|
107
|
+
await openPrivacySettings('camera'); // true if something opened; false off macOS
|
|
108
|
+
permissionBackend(); // 'cocoa' | null, synchronously
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## What each kind actually does on macOS
|
|
112
|
+
|
|
113
|
+
Not every kind has a prompt of its own, and the shape of the request follows
|
|
114
|
+
the framework's:
|
|
115
|
+
|
|
116
|
+
| kind | status | the "prompt" |
|
|
117
|
+
| ---------------------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
118
|
+
| `camera`, `microphone` | all five | the system's in-process dialog; the answer arrives when the user clicks |
|
|
119
|
+
| `screen-recording` | never `'prompt'` | the system's go-to-Settings dialog, shown once; the request resolves at once, and a new grant needs a **restart** |
|
|
120
|
+
| `accessibility` | never `'prompt'` | the go-to-Settings dialog; resolves at once, and the grant applies live |
|
|
121
|
+
| `input-monitoring` | `'prompt'` while open | posts the prompt and resolves at once |
|
|
122
|
+
| `automation` | per **target** | `{ target: 'com.apple.finder' }` — the bundle id of a **running** app, or the call throws; asking blocks until answered |
|
|
123
|
+
| `location` | all five | `requestWhenInUseAuthorization`, answered through the run loop |
|
|
124
|
+
| `calendars` | plus `'write-only'` | EventKit's own prompt. `{ access: 'write-only' }` asks the narrower one; the grant is what [desktop-calendar.md](desktop-calendar.md) reads through |
|
|
125
|
+
| `reminders` | plus `'write-only'` | the same store, a separate grant. `{ access: 'write-only' }` is a `TypeError` here: reminders have no such grant |
|
|
126
|
+
| `files-and-folders` | — | no API: reading the folder _is_ the prompt and `EPERM` the denial; `openPrivacySettings` reaches the pane |
|
|
127
|
+
| `full-disk-access` | — | no API; `openPrivacySettings('full-disk-access')` |
|
|
128
|
+
|
|
129
|
+
**Attribution is the thing to know.** A bare `node` process is attributed to
|
|
130
|
+
its _responsible process_ — the terminal, an IDE — or to `node` itself, and
|
|
131
|
+
prompts with no usage-description strings. A bundled app must carry the keys
|
|
132
|
+
(`NSCameraUsageDescription`, `NSMicrophoneUsageDescription`,
|
|
133
|
+
`NSLocationUsageDescription`, `NSAppleEventsUsageDescription`,
|
|
134
|
+
`NSCalendarsFullAccessUsageDescription`,
|
|
135
|
+
`NSCalendarsWriteOnlyAccessUsageDescription`,
|
|
136
|
+
`NSRemindersFullAccessUsageDescription`) in its `Info.plist` — without them
|
|
137
|
+
the request never prompts, or TCC ends the process. See
|
|
138
|
+
[packaging.md](packaging.md).
|
|
139
|
+
|
|
140
|
+
## What differs between the rungs
|
|
141
|
+
|
|
142
|
+
- **The Settings rung reads nothing.** `openPrivacySettings` opens the pane
|
|
143
|
+
on any Mac — through the bridge on the cocoa backend, by `open` under
|
|
144
|
+
XQuartz — and that is all it does; the status stays `'unknown'` on the X11
|
|
145
|
+
backend because nothing there can read TCC.
|
|
146
|
+
- **`'unknown'` is not `'prompt'`.** An app that treats them alike will show
|
|
147
|
+
an "Allow" button on Linux that rejects when pressed. Branch on `available`
|
|
148
|
+
first.
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
# React features
|
|
2
|
+
|
|
3
|
+
You already know React. This page is about the parts where that knowledge
|
|
4
|
+
transfers unchanged, the parts where it transfers with a caveat, and the
|
|
5
|
+
four places where a habit from the DOM will quietly not work.
|
|
6
|
+
|
|
7
|
+
The short version: **everything in React core behaves exactly as documented.**
|
|
8
|
+
Hooks, context, error boundaries, `<Suspense>`, `lazy`, `use`, transitions,
|
|
9
|
+
`<Profiler>`, `<StrictMode>` — all of it. react-x11 is a normal React 19
|
|
10
|
+
renderer, and where React's behaviour does not depend on the DOM, it is the
|
|
11
|
+
same behaviour.
|
|
12
|
+
|
|
13
|
+
Everything on this page is also something you can drive by hand — in the
|
|
14
|
+
apps that depend on it rather than in a panel of its own:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npm run examples:monitor # priority: a filter over ~780 live rows
|
|
18
|
+
npm run examples:chat # <Activity>, useOptimistic, <Suspense>
|
|
19
|
+
npm run examples:timer # error boundaries, and where a crash is logged
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Those replaced an `examples/react-features.jsx` that had a panel per feature.
|
|
23
|
+
A panel proves the API exists; it cannot show you the choice, because nothing
|
|
24
|
+
in it costs anything. `monitor`'s field is expensive because a process list
|
|
25
|
+
is expensive, and that is the only condition under which deferring it is a
|
|
26
|
+
decision rather than a demonstration.
|
|
27
|
+
|
|
28
|
+
What differs is anything the DOM used to define for you:
|
|
29
|
+
|
|
30
|
+
- **Paint timing.** A commit does not put pixels on screen. Frames go out on
|
|
31
|
+
a clock, so "before paint" is a real, observable boundary.
|
|
32
|
+
- **Measuring.** There is no `getBoundingClientRect()` that forces layout.
|
|
33
|
+
Layout has already happened, or it has not.
|
|
34
|
+
- **Hiding.** `<Suspense>` and `<Activity>` hide a `<box>` cheaply, but
|
|
35
|
+
hiding a `<window>` genuinely unmaps it, and the window manager notices.
|
|
36
|
+
- **Portal containers.** There is no document to portal into.
|
|
37
|
+
|
|
38
|
+
## At a glance
|
|
39
|
+
|
|
40
|
+
| | |
|
|
41
|
+
| -------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
|
42
|
+
| `useState`, `useReducer`, `useContext`, `useMemo`, `useCallback`, `useRef` | unchanged |
|
|
43
|
+
| `useImperativeHandle`, `forwardRef`, `ref` as a prop, ref cleanup | unchanged |
|
|
44
|
+
| `use(promise)`, `use(context)`, `React.lazy` | unchanged |
|
|
45
|
+
| `useOptimistic`, `useActionState`, `<Profiler>`, custom hooks | unchanged |
|
|
46
|
+
| `useSyncExternalStore` | unchanged, and the best way to bridge non-React code |
|
|
47
|
+
| `useTransition`, `startTransition`, `useDeferredValue` | work, and really do stay responsive |
|
|
48
|
+
| `useLayoutEffect` vs `useEffect` | the difference is real — one paint versus two |
|
|
49
|
+
| `<Suspense>` | works; read [Suspense and Activity](#suspense-and-activity) for `<window>` |
|
|
50
|
+
| `<Activity>` | works; `mode="hidden"` around a `<window>` unmaps it |
|
|
51
|
+
| Error boundaries | work — but **where you put one** decides if the window survives |
|
|
52
|
+
| `<StrictMode>` | safe to use; effects are not double-invoked on the first mount |
|
|
53
|
+
| `useId` | works; there is very little here to use it for |
|
|
54
|
+
| `createPortal` | use [`<popup>`](elements.md) instead — see [Portals](#portals) |
|
|
55
|
+
| `flushSync` | not exported; you almost certainly do not need it |
|
|
56
|
+
| `useFormStatus` | not wired up |
|
|
57
|
+
| React Server Components | not available |
|
|
58
|
+
| anything imported from `react-dom` | not available — see [Not available](#not-available) |
|
|
59
|
+
|
|
60
|
+
## Effects, and the paint boundary
|
|
61
|
+
|
|
62
|
+
Put an effect that **changes what the user sees** in `useLayoutEffect`. Put
|
|
63
|
+
everything else in `useEffect`. That is the same rule as the DOM, and here
|
|
64
|
+
it has a directly observable consequence:
|
|
65
|
+
|
|
66
|
+
| you call `setState` from | what the user sees |
|
|
67
|
+
| ------------------------ | ------------------------------------------------------- |
|
|
68
|
+
| `useLayoutEffect` | one frame, with the corrected value |
|
|
69
|
+
| `useEffect` | one frame with the old value, then a frame with the new |
|
|
70
|
+
|
|
71
|
+
So an adjustment made in `useEffect` flickers. This is not a timing race you
|
|
72
|
+
can get lucky with — updates from a layout effect are folded into the frame
|
|
73
|
+
being prepared, and updates from a passive effect are not.
|
|
74
|
+
|
|
75
|
+
That holds for a correction computed from React state. One computed from
|
|
76
|
+
**geometry** is a layout behind: a layout effect runs after the commit and
|
|
77
|
+
before the layout pass, so `abs` there is still the previous frame's rect
|
|
78
|
+
(zeros, on mount), and anything measured from it describes the frame
|
|
79
|
+
before. For a size, let the element report it with `onLayout`
|
|
80
|
+
([below](#measuring-a-node)); for a style that depends on one, write a
|
|
81
|
+
container query ([styling.md](styling.md#container-queries)), which the
|
|
82
|
+
pass answers in the same frame. `scrollIntoView` measures nothing when you
|
|
83
|
+
call it, and neither does the part of a `scrollTo` past the extent the last
|
|
84
|
+
pass measured, which is what `scrollTo(Infinity)` asks for when it follows
|
|
85
|
+
rows this commit added. The pass answers both against the geometry it
|
|
86
|
+
produces, so they are safe here ([elements.md](elements.md#scrolling)).
|
|
87
|
+
|
|
88
|
+
The same boundary is why an event handler feels instant: react-x11 lands the
|
|
89
|
+
React update caused by a click or a keystroke **before** the frame goes out,
|
|
90
|
+
so the response and the default action paint together rather than a frame
|
|
91
|
+
apart.
|
|
92
|
+
|
|
93
|
+
```jsx
|
|
94
|
+
// good — `tooTall` and `fits` are React state, so the corrected rows paint
|
|
95
|
+
// in the same frame as the state that asked for them
|
|
96
|
+
useLayoutEffect(() => {
|
|
97
|
+
if (tooTall) setRows(fits);
|
|
98
|
+
}, [tooTall, fits]);
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Measuring a node
|
|
102
|
+
|
|
103
|
+
**There is no forced synchronous layout.** Nothing you can call will make
|
|
104
|
+
layout run so you can read the result. This is the DOM habit most likely to
|
|
105
|
+
break on the way over, because in a browser `getBoundingClientRect()` both
|
|
106
|
+
reads _and_ triggers.
|
|
107
|
+
|
|
108
|
+
A node's `abs` rect (and `getClientRects()`) reports what the **last** layout
|
|
109
|
+
produced. On the very first render of a node, that is nothing yet.
|
|
110
|
+
|
|
111
|
+
What to do instead:
|
|
112
|
+
|
|
113
|
+
- **Let the element tell you.** `onLayout` fires on any element after a
|
|
114
|
+
layout pass moved or resized it, with `width`/`height` (and `x`/`y`
|
|
115
|
+
within the parent) in logical pixels — once after the first layout, then
|
|
116
|
+
on change. `<box onViewport>` adds a scroller's content size, and
|
|
117
|
+
`<window onResize>` fires when the window is resized. These are the
|
|
118
|
+
supported way to react to a size.
|
|
119
|
+
- **Read `abs` from a ref in a handler or a later effect**, not during the
|
|
120
|
+
render that created the node.
|
|
121
|
+
- **Take a getter, not a value.** `useWindowId(ref)` and `useAnchor()` return
|
|
122
|
+
getters for exactly this reason — a value captured during render would be
|
|
123
|
+
`null` on the render where you needed it.
|
|
124
|
+
|
|
125
|
+
```jsx
|
|
126
|
+
const [cols, setCols] = useState(1);
|
|
127
|
+
|
|
128
|
+
<box onLayout={({ width }) => setCols(Math.max(1, (width / 200) | 0))}>
|
|
129
|
+
{items.map(…)}
|
|
130
|
+
</box>;
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
When what depends on the size is a _style_ rather than a decision about what
|
|
134
|
+
to render, write it as a container query block instead
|
|
135
|
+
([styling.md](styling.md#container-queries)): it applies in the same frame,
|
|
136
|
+
where a state set from `onLayout` re-renders a frame later.
|
|
137
|
+
|
|
138
|
+
If you are porting a component that measures itself and adjusts, this is the
|
|
139
|
+
part to rewrite. See [elements.md](elements.md) for `onLayout`, `onViewport`
|
|
140
|
+
and `onResize`, and [components.md](components.md) for `useAnchor`.
|
|
141
|
+
|
|
142
|
+
## What a ref gives you
|
|
143
|
+
|
|
144
|
+
Two different kinds of object, depending on the element:
|
|
145
|
+
|
|
146
|
+
- **`<window>` and `<popup>`** hand back the live **window object** of
|
|
147
|
+
whichever backend is drawing — on X11 the ntk window, which is what
|
|
148
|
+
[`windowIdOf()`](elements.md) resolves to an XID. Both spell
|
|
149
|
+
`getContext('2d')` and `requestAnimationFrame` the same way; anything
|
|
150
|
+
past that is the backend's own surface, so a component that reaches
|
|
151
|
+
further should say which backend it is for.
|
|
152
|
+
- **every other element** hands back its **node**: `focus()`, `blur()`,
|
|
153
|
+
`focused`, `hitTest()`, `containsPoint()`, `getClientRects()`, and `abs`
|
|
154
|
+
for its position and size. `<textinput>`/`<textarea>` additionally match
|
|
155
|
+
the DOM closely enough that libraries like react-hook-form drive them
|
|
156
|
+
through a ref.
|
|
157
|
+
|
|
158
|
+
A window ref is `null` until that window exists, and refs attach after the
|
|
159
|
+
commit — so read them in effects and handlers, never during render.
|
|
160
|
+
|
|
161
|
+
For `<canvas>`, note that the **node is not a drawing surface**. There is no
|
|
162
|
+
`getContext()` on it; you draw in `onDraw`, which the renderer calls with a
|
|
163
|
+
context whenever the canvas needs repainting:
|
|
164
|
+
|
|
165
|
+
```jsx
|
|
166
|
+
<canvas onDraw={(ctx, { width, height }) => { …}} />
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
To make it repaint, change something React can see — a prop, state, or the
|
|
170
|
+
`cacheKey`. That is the supported path, and it is usually what you want.
|
|
171
|
+
There is an imperative escape hatch for element authors in
|
|
172
|
+
[extending.md](extending.md), but note that it lives on the **owning
|
|
173
|
+
window**, not on the drawn node, despite what the bundled types currently
|
|
174
|
+
say (see [Known gaps](#known-gaps)).
|
|
175
|
+
|
|
176
|
+
## Transitions, and what stays responsive
|
|
177
|
+
|
|
178
|
+
`useTransition`, `startTransition` and `useDeferredValue` all work, and they
|
|
179
|
+
really do keep the UI responsive — React will interrupt a transition render
|
|
180
|
+
to handle an incoming click or keystroke.
|
|
181
|
+
|
|
182
|
+
What is worth knowing is that **transitions are the mechanism that does
|
|
183
|
+
this**. An ordinary `setState` — from a handler, a timer, a socket callback —
|
|
184
|
+
renders in one uninterruptible pass, however much work it is. If a render is
|
|
185
|
+
big enough to drop a frame, wrapping it in `startTransition` (or deferring
|
|
186
|
+
its input with `useDeferredValue`) is not a micro-optimisation, it is the
|
|
187
|
+
difference between a responsive window and a frozen one.
|
|
188
|
+
|
|
189
|
+
```jsx
|
|
190
|
+
const [pending, startTransition] = useTransition();
|
|
191
|
+
|
|
192
|
+
// typing stays instant; the expensive list catches up
|
|
193
|
+
const onChange = (e) => {
|
|
194
|
+
setQuery(e.value);
|
|
195
|
+
startTransition(() => setResults(search(e.value)));
|
|
196
|
+
};
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Two smaller notes:
|
|
200
|
+
|
|
201
|
+
- `root.render()` is **synchronous** — the tree is committed before the call
|
|
202
|
+
returns. Wrapping it in `startTransition` does nothing.
|
|
203
|
+
- A `startTransition` inside a click handler is still a transition. It does
|
|
204
|
+
not inherit the handler's urgency.
|
|
205
|
+
|
|
206
|
+
react-x11 already classifies X11 input for you: presses, releases, keys,
|
|
207
|
+
focus changes, window close and drops are urgent; pointer motion and
|
|
208
|
+
drag-over are not, which is what keeps a motion burst from flooding the
|
|
209
|
+
renderer. You do not have to do anything to get this.
|
|
210
|
+
|
|
211
|
+
[`examples/monitor.jsx`](../examples/monitor.jsx) is this paragraph with a
|
|
212
|
+
keyboard on it: a filter over hundreds of live rows, where one keystroke
|
|
213
|
+
changes every row on screen. Without the deferral the caret waits for the
|
|
214
|
+
table; with it the field stays under the fingers and the rows arrive a beat
|
|
215
|
+
later. Two things there are worth copying, and both are one-line mistakes in
|
|
216
|
+
the other direction: the expensive subtree is
|
|
217
|
+
`memo`ised (or the urgent render rebuilds it anyway, and deferring buys
|
|
218
|
+
nothing), and no prop that changes on every update — not even a "stale"
|
|
219
|
+
border — is passed into it.
|
|
220
|
+
|
|
221
|
+
### Bridging non-React code
|
|
222
|
+
|
|
223
|
+
`useSyncExternalStore` is the right tool for anything that lives outside
|
|
224
|
+
React — a timer, a D-Bus signal, a socket, an ntk event. It is also faster
|
|
225
|
+
here than a hand-rolled `setState` bridge: a store notification is applied
|
|
226
|
+
urgently and lands in the next microtask, where the equivalent `setState`
|
|
227
|
+
waits a turn longer.
|
|
228
|
+
|
|
229
|
+
Every mainstream store (zustand, jotai, valtio, redux, XState) works with no
|
|
230
|
+
adapter for this reason. The [window-manager
|
|
231
|
+
example](../examples/wm.jsx) is the worked case: the WM core is a plain
|
|
232
|
+
store, and the UI subscribes to it.
|
|
233
|
+
|
|
234
|
+
## Suspense and Activity
|
|
235
|
+
|
|
236
|
+
`<Suspense>` works. The one thing to plan for is layout: a hidden subtree
|
|
237
|
+
gives up its space entirely, so the fallback lays out as if the real content
|
|
238
|
+
were not there. **Size your fallback deliberately**, or the window collapses
|
|
239
|
+
to the fallback and jumps back on reveal.
|
|
240
|
+
|
|
241
|
+
```jsx
|
|
242
|
+
<Suspense fallback={<box style={{ height: 240 }} />}>
|
|
243
|
+
<Report />
|
|
244
|
+
</Suspense>
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
**Focus goes with the hidden tree and comes back with it.** A field inside
|
|
248
|
+
the boundary gives up the keyboard when the fallback appears — nothing
|
|
249
|
+
invisible collects keystrokes — and gets it back on the reveal unless
|
|
250
|
+
something else took focus meanwhile, so a boundary that re-suspends
|
|
251
|
+
mid-edit does not drop the user out of what they were typing. See
|
|
252
|
+
[events.md](events.md#focus-and-visibility) for the rules and the way out.
|
|
253
|
+
|
|
254
|
+
**Scroll positions come back too.** What a hidden boundary holds is not
|
|
255
|
+
placed while it is hidden — the same as under `display: 'none'` — so a scroll
|
|
256
|
+
pane in it is not clamped to the start of content it no longer lays out,
|
|
257
|
+
reports no `onViewport` or `onScroll` until it is shown, and is shown at the
|
|
258
|
+
offset it was left at. A tab kept mounted under `<Activity>` reopens on the
|
|
259
|
+
row the user was reading.
|
|
260
|
+
|
|
261
|
+
If a `<window>` is inside the boundary, hiding it **unmaps the real window**.
|
|
262
|
+
The window manager treats the reveal as a new window: it may re-place it,
|
|
263
|
+
restack it, or apply its own geometry, and anything the user did to it can be
|
|
264
|
+
lost. Prefer suspending _inside_ a window over suspending the window itself.
|
|
265
|
+
|
|
266
|
+
`React.lazy` works. If you ship a single-file bundle, note that dynamic
|
|
267
|
+
`import()` gets inlined — you keep lazy _evaluation_ but get no code
|
|
268
|
+
splitting; see [packaging.md](packaging.md).
|
|
269
|
+
|
|
270
|
+
`<Activity>` works, and `mode="hidden"` around a toplevel `<window>` reads
|
|
271
|
+
the same way `<Suspense>` does: the window is unmapped, and one mounted
|
|
272
|
+
hidden is never mapped at all — it is created, laid out and kept, but the
|
|
273
|
+
window manager only ever hears about it on the reveal. The same caveat
|
|
274
|
+
applies: what the user did to the window before it was hidden is the window
|
|
275
|
+
manager's to remember, and it may not.
|
|
276
|
+
|
|
277
|
+
**Testing Suspense**: a promise that settles outside `act()` is not always
|
|
278
|
+
picked up by an `await act()`, because React throttles the commit when a
|
|
279
|
+
fallback was shown very recently. Use `waitFor` from `react-x11/test` —
|
|
280
|
+
see [testing.md](testing.md).
|
|
281
|
+
|
|
282
|
+
## Portals
|
|
283
|
+
|
|
284
|
+
**Use `<popup>`.** It is what you want in almost every case where the DOM
|
|
285
|
+
would reach for `createPortal`: a `<popup>` is written as an ordinary child
|
|
286
|
+
in your JSX — so it sees context, state and props exactly where you wrote it
|
|
287
|
+
— but it is a real top-level X window, so it escapes its parent's clipping
|
|
288
|
+
and can extend past the window edge.
|
|
289
|
+
|
|
290
|
+
```jsx
|
|
291
|
+
<box>
|
|
292
|
+
<Button onClick={(e) => setAt({ x: e.screenX, y: e.screenY })}>
|
|
293
|
+
Options…
|
|
294
|
+
</Button>
|
|
295
|
+
{at && (
|
|
296
|
+
<popup
|
|
297
|
+
x={at.x}
|
|
298
|
+
y={at.y}
|
|
299
|
+
width={180}
|
|
300
|
+
height={120}
|
|
301
|
+
grab
|
|
302
|
+
onDismiss={() => setAt(null)}
|
|
303
|
+
>
|
|
304
|
+
<Menu />
|
|
305
|
+
</popup>
|
|
306
|
+
)}
|
|
307
|
+
</box>
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Its position in the JSX has no effect on where it appears — you place it in
|
|
311
|
+
screen coordinates, and `useAnchor()` does that math for you when you are
|
|
312
|
+
anchoring to another node.
|
|
313
|
+
|
|
314
|
+
`Dialog`, `Tooltip`, `ContextMenu`, `MenuBar` and `Select` are all built this
|
|
315
|
+
way — see [components.md](components.md).
|
|
316
|
+
|
|
317
|
+
There is no `createPortal` export. The reconciler's own is reachable through
|
|
318
|
+
the `Renderer` escape hatch, but it is not a supported surface: the container
|
|
319
|
+
has to be an X connection or an X window rather than a node, and only
|
|
320
|
+
`<window>`/`<popup>` can be the portal's immediate child. If you find
|
|
321
|
+
yourself wanting it, you almost certainly want a second `<window>` or a
|
|
322
|
+
`<popup>`.
|
|
323
|
+
|
|
324
|
+
## Where to put an error boundary
|
|
325
|
+
|
|
326
|
+
Boundaries work exactly as documented. The X11-specific decision is
|
|
327
|
+
**placement**, and it decides whether the user's window survives:
|
|
328
|
+
|
|
329
|
+
```jsx
|
|
330
|
+
// good — the window and everything the WM knows about it survives
|
|
331
|
+
<window title="Editor">
|
|
332
|
+
<ErrorBoundary fallback={<text>Something broke</text>}>
|
|
333
|
+
<Document />
|
|
334
|
+
</ErrorBoundary>
|
|
335
|
+
</window>
|
|
336
|
+
|
|
337
|
+
// the window itself is destroyed and recreated on a throw
|
|
338
|
+
<ErrorBoundary fallback={<window title="Editor">…</window>}>
|
|
339
|
+
<window title="Editor">
|
|
340
|
+
<Document />
|
|
341
|
+
</window>
|
|
342
|
+
</ErrorBoundary>
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
A recreated window is a **new** window: its position, size, stacking,
|
|
346
|
+
maximized state and anything else the user or the window manager did to it
|
|
347
|
+
are gone. Put the boundary inside the window unless you genuinely want the
|
|
348
|
+
window replaced.
|
|
349
|
+
|
|
350
|
+
An **uncaught** error unmounts the whole tree, which here means every window
|
|
351
|
+
disappears while the process stays alive. `createRoot` takes
|
|
352
|
+
`onUncaughtError`, `onCaughtError` and `onRecoverableError` so you can decide
|
|
353
|
+
what that should mean for your app — the default logs and sets a failing exit
|
|
354
|
+
code. See [events.md](events.md).
|
|
355
|
+
|
|
356
|
+
One thing React does not cover: **a throw inside an event handler never
|
|
357
|
+
reaches a boundary.** (That is true in React DOM too.) react-x11 routes those
|
|
358
|
+
to `onUncaughtError` instead, so they are at least reportable rather than
|
|
359
|
+
silent.
|
|
360
|
+
|
|
361
|
+
## Smaller notes
|
|
362
|
+
|
|
363
|
+
**`useId`** works, but there is little use for it: no element takes an `id`,
|
|
364
|
+
there is no hydration, and labels are associated structurally (`<Checkbox
|
|
365
|
+
label>`, `<Radio>children</Radio>`). If you want a window's real identity,
|
|
366
|
+
that is `useWindowId()`. Avoid putting an `id` prop on `<window>` — unknown
|
|
367
|
+
props there are forwarded to the underlying window as creation attributes.
|
|
368
|
+
|
|
369
|
+
**`<StrictMode>`** is safe. A discarded render costs no X11 traffic, so
|
|
370
|
+
double-rendering never produces double windows. Be aware that effects are
|
|
371
|
+
**not** double-invoked on the initial mount, so it will not catch a missing
|
|
372
|
+
cleanup at mount the way it does in React DOM.
|
|
373
|
+
|
|
374
|
+
**`memo` / `useMemo`** save CPU, not bandwidth. Rebuilding a `style` object
|
|
375
|
+
or array every render is free — styles are compared by value, so an
|
|
376
|
+
identical style produces no layout work, no repaint and no frame. Passing
|
|
377
|
+
`style={[base, active && on]}` inline is the documented idiom, not a leak.
|
|
378
|
+
See [styling.md](styling.md).
|
|
379
|
+
|
|
380
|
+
**Keys** matter more for `<window>` children than for drawn ones. Reordering
|
|
381
|
+
`<box>` children is cheap. Reordering sibling `<window>`s restacks real
|
|
382
|
+
windows, and a missing key destroys and recreates them — with the same loss
|
|
383
|
+
of window-manager state described above. To keep a window on top, use
|
|
384
|
+
`alwaysOnTop` rather than ordering.
|
|
385
|
+
|
|
386
|
+
**DevTools** works — set `REACT_X11_DEVTOOLS=1` and the component tree,
|
|
387
|
+
props and highlight-on-hover all behave normally, with the highlight drawn
|
|
388
|
+
into the real window. The Profiler **Timeline** tab is unavailable; the
|
|
389
|
+
commit list and flamegraph work. See [devtools.md](devtools.md).
|
|
390
|
+
|
|
391
|
+
**Fast Refresh** works without a bundler: `npx react-x11 dev app.jsx` runs
|
|
392
|
+
a file with its default export mounted and every save applied in place, over
|
|
393
|
+
the `react-x11/refresh` entry point, which a tool that hosts the loader
|
|
394
|
+
itself registers with `--import`. Keep anything
|
|
395
|
+
whose identity must survive a reload — contexts, stores — in a module you
|
|
396
|
+
are not editing. See [dev tooling](ecosystem/dev-tooling.md#react-refresh)
|
|
397
|
+
and the [hot-reload example](../examples/tasks.jsx), which is
|
|
398
|
+
`npm run examples:tasks:hot`.
|
|
399
|
+
|
|
400
|
+
## Not available
|
|
401
|
+
|
|
402
|
+
**`react-dom`.** It is not a dependency, so `ReactDOM.createPortal`,
|
|
403
|
+
`flushSync`, `findDOMNode`, `hydrateRoot` and `unstable_batchedUpdates` are
|
|
404
|
+
not part of react-x11.
|
|
405
|
+
|
|
406
|
+
The trap is not the import error — it is that `react-dom` is a required peer
|
|
407
|
+
of some libraries, so it can end up installed anyway. Then the import
|
|
408
|
+
resolves, and `flushSync()` runs your callback **without flushing anything**,
|
|
409
|
+
because it is talking to a renderer you are not using. If a library depends
|
|
410
|
+
on synchronous flushing, check `npm ls react-dom`. The
|
|
411
|
+
[ecosystem register](ecosystem.md) records which packages hit this.
|
|
412
|
+
|
|
413
|
+
**Server Components** are not available: there is no Flight client wired up.
|
|
414
|
+
|
|
415
|
+
**`useFormStatus`** is not wired up. `useOptimistic` and `useActionState` are
|
|
416
|
+
unaffected and work normally.
|
|
417
|
+
|
|
418
|
+
## Known gaps
|
|
419
|
+
|
|
420
|
+
Open bugs where a React feature does not yet mean here what it should:
|
|
421
|
+
|
|
422
|
+
- A `<popup>` written inside a subtree that `<Suspense>` or `<Activity>`
|
|
423
|
+
hides stays on screen: React hides the topmost host instance of the branch
|
|
424
|
+
and the popup is its own X window below it, so nothing unmaps it. Focus
|
|
425
|
+
follows what is actually visible, so the popup keeps the keyboard — but
|
|
426
|
+
the popup itself has to be conditional in your JSX rather than left to the
|
|
427
|
+
boundary.
|
|
428
|
+
- The bundled types declare `invalidate()` on every node; at runtime only the
|
|
429
|
+
owning `<window>` has it. Reach it as `ref.current.root` — or better, drive
|
|
430
|
+
repaints through props and `cacheKey`.
|