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,206 @@
|
|
|
1
|
+
# Layout and positioning
|
|
2
|
+
|
|
3
|
+
Layout itself is yoga's job — see [styling](../styling.md). The two packages
|
|
4
|
+
here cover what yoga does not: placing a popup relative to a trigger across
|
|
5
|
+
screen edges, and rendering a long list without a node per row. Both are the
|
|
6
|
+
_core_ half of a library whose React wrapper is DOM-bound; the core is
|
|
7
|
+
published separately so non-DOM hosts can use it.
|
|
8
|
+
|
|
9
|
+
## `@floating-ui/core` {#floating-ui}
|
|
10
|
+
|
|
11
|
+
**Adapter, ~35 lines (a platform object).** @floating-ui/core@1.8.0.
|
|
12
|
+
|
|
13
|
+
Floating UI's positioning engine without DOM code. Hand `computePosition` a
|
|
14
|
+
`platform` object — `getElementRects`, `getDimensions`, `getClippingRect`,
|
|
15
|
+
`isRTL` — and get the full middleware pipeline (`offset`, `flip`, `shift`,
|
|
16
|
+
`size`, `arrow`, `autoPlacement`, `hide`) and its placement vocabulary.
|
|
17
|
+
|
|
18
|
+
As in the built-in `anchorRect()`, a drawn node's `node.abs` rect plus the
|
|
19
|
+
window's screen origin is the reference rect in screen coordinates,
|
|
20
|
+
`screenOf(node)` gives the clipping rect, and the popup-to-be is just
|
|
21
|
+
`{width, height}`. The result's `{x, y, placement}` drops straight into
|
|
22
|
+
`<popup x y>`.
|
|
23
|
+
|
|
24
|
+
```jsx
|
|
25
|
+
import { computePosition, offset, flip, shift } from '@floating-ui/core';
|
|
26
|
+
|
|
27
|
+
// The screen a node is on. Same three lines the built-in anchorRect uses;
|
|
28
|
+
// it is not exported from the package, so keep a copy.
|
|
29
|
+
function screenOf(node) {
|
|
30
|
+
const app = node?.app;
|
|
31
|
+
const screen = (app?.display ?? app?.X?.display)?.screen?.[0];
|
|
32
|
+
return screen?.pixel_width ? screen : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// reference = a drawn node (post-layout); floating = the popup's {width, height}
|
|
36
|
+
function screenRectOf(node) {
|
|
37
|
+
const win = node.root?.window;
|
|
38
|
+
const origin = win?._screenOrigin ?? { x: win?.x ?? 0, y: win?.y ?? 0 };
|
|
39
|
+
return {
|
|
40
|
+
x: origin.x + node.abs.x,
|
|
41
|
+
y: origin.y + node.abs.y,
|
|
42
|
+
width: node.abs.width,
|
|
43
|
+
height: node.abs.height,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const platform = {
|
|
48
|
+
getElementRects: ({ reference, floating }) => ({
|
|
49
|
+
reference: screenRectOf(reference),
|
|
50
|
+
floating: { x: 0, y: 0, width: floating.width, height: floating.height },
|
|
51
|
+
}),
|
|
52
|
+
getDimensions: (el) => el.abs ?? el, // node or plain {width, height}
|
|
53
|
+
getClippingRect: ({ element }) => {
|
|
54
|
+
const s = screenOf(element?.node ?? element);
|
|
55
|
+
return s
|
|
56
|
+
? { x: 0, y: 0, width: s.pixel_width, height: s.pixel_height }
|
|
57
|
+
: { x: 0, y: 0, width: Infinity, height: Infinity };
|
|
58
|
+
},
|
|
59
|
+
isElement: () => true,
|
|
60
|
+
isRTL: () => false,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const { x, y, placement } = await computePosition(
|
|
64
|
+
triggerNode, // from a ref on the trigger <box>
|
|
65
|
+
{ width: 200, height: 150, node: triggerNode },
|
|
66
|
+
{
|
|
67
|
+
placement: 'bottom-start',
|
|
68
|
+
middleware: [offset(4), flip(), shift({ padding: 8 })],
|
|
69
|
+
platform,
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
// -> <popup x={x} y={y}> ; placement tells you which side won
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Over `anchorRect` this adds middleware composition (`shift` with padding,
|
|
76
|
+
`size` to clamp long menus, `arrow`) and alignment fallback — `flip` with
|
|
77
|
+
`crossAxis: 'alignment'` switches `bottom-start` to `bottom-end` when that
|
|
78
|
+
fits. `anchorRect`'s `placement`, `align` and `offset` options map 1:1 onto
|
|
79
|
+
`placement` plus the `offset()` middleware.
|
|
80
|
+
|
|
81
|
+
- `computePosition` is async — `await` it, though this platform is
|
|
82
|
+
synchronous.
|
|
83
|
+
- `getClippingRect` above is screen 0's full rect, the same single-monitor
|
|
84
|
+
simplification as `screenOf`. On a dual-head X display, popups clamp to
|
|
85
|
+
the bounding box of both monitors.
|
|
86
|
+
- **`@floating-ui/dom` and `@floating-ui/react` are not usable** (DOM
|
|
87
|
+
platform and DOM interactions). Only `core` and its `@floating-ui/utils`
|
|
88
|
+
dependency belong in an X11 app.
|
|
89
|
+
- Safe-polygon hover lives in `@floating-ui/react`'s DOM-bound `useHover`,
|
|
90
|
+
not in `core`; keep react-x11's built-in one.
|
|
91
|
+
|
|
92
|
+
## `@tanstack/virtual-core` {#tanstack-virtual}
|
|
93
|
+
|
|
94
|
+
**Adapter, ~40 lines.** @tanstack/virtual-core@3.17.7.
|
|
95
|
+
|
|
96
|
+
The engine behind `useVirtualizer`: from a count, an `estimateSize`, the
|
|
97
|
+
viewport rect and the scroll offset it computes the visible items
|
|
98
|
+
(`getVirtualItems()`), the total size (`getTotalSize()`), and offsets for
|
|
99
|
+
`scrollToIndex`. Every DOM touchpoint is a constructor option.
|
|
100
|
+
|
|
101
|
+
**Do not import `@tanstack/react-virtual`.** Its first line is
|
|
102
|
+
`import { flushSync } from "react-dom"`: without react-dom it fails to
|
|
103
|
+
resolve; _with_ it, it silently flushes into the wrong reconciler
|
|
104
|
+
([silent failures](../ecosystem.md#silent-failures)). The hook below is
|
|
105
|
+
everything the React wrapper adds.
|
|
106
|
+
|
|
107
|
+
Three seams wire to a scrolling `<box>`:
|
|
108
|
+
|
|
109
|
+
- `observeElementRect` → `<box onViewport>`, which fires from layout
|
|
110
|
+
with `{width, height, contentWidth, contentHeight}` — including before any
|
|
111
|
+
scroll, which is what a list needs on mount;
|
|
112
|
+
- `observeElementOffset` → `<box onScroll>` (`scrollY`);
|
|
113
|
+
- `scrollToFn` → `ref.scrollTo(offset)`.
|
|
114
|
+
|
|
115
|
+
`getScrollElement()` must return something with
|
|
116
|
+
`scrollHeight`/`clientHeight` (virtual-core duck-types Element vs Window
|
|
117
|
+
with `'scrollHeight' in el`); a getter shim over the node's
|
|
118
|
+
`contentHeight`/`abs.height` satisfies it.
|
|
119
|
+
|
|
120
|
+
```jsx
|
|
121
|
+
import React from 'react';
|
|
122
|
+
import { Virtualizer } from '@tanstack/virtual-core';
|
|
123
|
+
|
|
124
|
+
function useX11Virtualizer({ count, estimateSize, overscan = 2 }) {
|
|
125
|
+
const ref = React.useRef(null);
|
|
126
|
+
const obs = React.useRef({});
|
|
127
|
+
const [, rerender] = React.useReducer((n) => n + 1, 0);
|
|
128
|
+
const [element] = React.useState(() => ({
|
|
129
|
+
// element-shaped view of the node
|
|
130
|
+
get scrollHeight() {
|
|
131
|
+
return ref.current?.contentHeight ?? 0;
|
|
132
|
+
},
|
|
133
|
+
get clientHeight() {
|
|
134
|
+
return ref.current?.abs.height ?? 0;
|
|
135
|
+
},
|
|
136
|
+
get scrollWidth() {
|
|
137
|
+
return ref.current?.contentWidth ?? 0;
|
|
138
|
+
},
|
|
139
|
+
get clientWidth() {
|
|
140
|
+
return ref.current?.abs.width ?? 0;
|
|
141
|
+
},
|
|
142
|
+
}));
|
|
143
|
+
const [v] = React.useState(
|
|
144
|
+
() =>
|
|
145
|
+
new Virtualizer({
|
|
146
|
+
count,
|
|
147
|
+
estimateSize,
|
|
148
|
+
overscan,
|
|
149
|
+
getScrollElement: () => (ref.current ? element : null),
|
|
150
|
+
observeElementRect: (_i, cb) => {
|
|
151
|
+
obs.current.rect = cb;
|
|
152
|
+
},
|
|
153
|
+
observeElementOffset: (_i, cb) => {
|
|
154
|
+
obs.current.offset = cb;
|
|
155
|
+
cb(0, false);
|
|
156
|
+
},
|
|
157
|
+
scrollToFn: (offset) => ref.current?.scrollTo(offset),
|
|
158
|
+
onChange: () => rerender(),
|
|
159
|
+
}),
|
|
160
|
+
);
|
|
161
|
+
v.setOptions({ ...v.options, count, onChange: () => rerender() });
|
|
162
|
+
React.useEffect(() => v._didMount(), []);
|
|
163
|
+
React.useEffect(() => v._willUpdate());
|
|
164
|
+
return {
|
|
165
|
+
virtualizer: v,
|
|
166
|
+
scrollProps: {
|
|
167
|
+
ref,
|
|
168
|
+
onViewport: (r) => obs.current.rect?.(r),
|
|
169
|
+
onScroll: (s) => obs.current.offset?.(s.scrollY, true),
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
```jsx
|
|
176
|
+
function BigList() {
|
|
177
|
+
const { virtualizer, scrollProps } = useX11Virtualizer({
|
|
178
|
+
count: 10000,
|
|
179
|
+
estimateSize: () => 24,
|
|
180
|
+
});
|
|
181
|
+
return (
|
|
182
|
+
<box {...scrollProps} style={{ flexGrow: 1, overflow: 'scroll' }}>
|
|
183
|
+
<box style={{ height: virtualizer.getTotalSize() }}>
|
|
184
|
+
{virtualizer.getVirtualItems().map((item) => (
|
|
185
|
+
<text
|
|
186
|
+
key={item.key}
|
|
187
|
+
style={{ position: 'absolute', top: item.start, height: item.size }}
|
|
188
|
+
>
|
|
189
|
+
{`row ${item.index}`}
|
|
190
|
+
</text>
|
|
191
|
+
))}
|
|
192
|
+
</box>
|
|
193
|
+
</box>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
- **First paint shows zero rows.** Layout runs on the frame clock after the
|
|
199
|
+
mount commit, `onViewport` reports a tick later, and the list fills in on
|
|
200
|
+
the next frame. Fine interactively; tests must tick a few times.
|
|
201
|
+
- `estimateSize` is the only measurement; `measureElement` is built on
|
|
202
|
+
`ResizeObserver` and `getBoundingClientRect`. For variable-height rows
|
|
203
|
+
pass exact sizes: `estimateSize: (i) => heights[i]`.
|
|
204
|
+
- `_didMount`/`_willUpdate` are underscored but are the documented pattern
|
|
205
|
+
for custom adapters — the official React, Vue and Svelte adapters call
|
|
206
|
+
exactly these.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Routing
|
|
2
|
+
|
|
3
|
+
There is no URL bar and no `history` object, so both routers here run on
|
|
4
|
+
their first-party in-memory location. What does not survive is `<Link>`,
|
|
5
|
+
which renders an `<a>` host in both; navigate from a `<box onClick>`
|
|
6
|
+
instead. These libraries give you matching, not a back stack — build stack
|
|
7
|
+
or tab navigation on top if you need it.
|
|
8
|
+
|
|
9
|
+
## wouter {#wouter}
|
|
10
|
+
|
|
11
|
+
**Out of the box — with `wouter/memory-location`.** wouter@3.10.0.
|
|
12
|
+
|
|
13
|
+
A 2 kB router: `<Router>`, `<Route>`, `<Switch>`, `useLocation`, pattern
|
|
14
|
+
params, and a pluggable location hook with a non-browser one shipped.
|
|
15
|
+
|
|
16
|
+
```jsx
|
|
17
|
+
import React from 'react';
|
|
18
|
+
import { createRoot } from 'react-x11';
|
|
19
|
+
import { Router, Route, Switch } from 'wouter';
|
|
20
|
+
import { memoryLocation } from 'wouter/memory-location';
|
|
21
|
+
|
|
22
|
+
const { hook, navigate } = memoryLocation({ path: '/' });
|
|
23
|
+
|
|
24
|
+
// the react-x11 replacement for <Link>
|
|
25
|
+
const NavText = ({ href, children }) => (
|
|
26
|
+
<box style={{ height: 20 }} onClick={() => navigate(href)}>
|
|
27
|
+
<text style={{ color: '#08c' }}>{children}</text>
|
|
28
|
+
</box>
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const App = () => (
|
|
32
|
+
<window width={400} height={300} title="routed">
|
|
33
|
+
<box style={{ flexGrow: 1, flexDirection: 'column' }}>
|
|
34
|
+
<Router hook={hook}>
|
|
35
|
+
<box style={{ flexDirection: 'row', gap: 8, height: 20 }}>
|
|
36
|
+
<NavText href="/">home</NavText>
|
|
37
|
+
<NavText href="/about">about</NavText>
|
|
38
|
+
</box>
|
|
39
|
+
<Switch>
|
|
40
|
+
<Route path="/">
|
|
41
|
+
<text>home page</text>
|
|
42
|
+
</Route>
|
|
43
|
+
<Route path="/about">
|
|
44
|
+
<text>about page</text>
|
|
45
|
+
</Route>
|
|
46
|
+
<Route>
|
|
47
|
+
<text>not found</text>
|
|
48
|
+
</Route>
|
|
49
|
+
</Switch>
|
|
50
|
+
</Router>
|
|
51
|
+
</box>
|
|
52
|
+
</window>
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
(await createRoot()).render(<App />);
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
- **Always mount the memory hook at the top.** A `<Route>` outside a
|
|
59
|
+
`<Router hook={...}>` falls back to `useBrowserLocation` and crashes
|
|
60
|
+
immediately with `ReferenceError: location is not defined`.
|
|
61
|
+
- **`<Link>` does not work**: it renders an `<a>`. `<Link asChild>` avoids
|
|
62
|
+
the `<a>` but does not navigate, because its click guard checks
|
|
63
|
+
`event.button !== 0` and react-x11's `MouseEvent.button` is the X button
|
|
64
|
+
number, `1` for left.
|
|
65
|
+
- `useLocation()[1]` is the same `navigate` from inside components; the
|
|
66
|
+
module-level one is handier with a single router.
|
|
67
|
+
- `useHashLocation` and `useBrowserLocation` are browser-only. Everything
|
|
68
|
+
else — matching, params, nesting, `useRoute` — is pure.
|
|
69
|
+
- `memoryLocation({ record: true })` keeps a `history` array, which is the
|
|
70
|
+
raw material for a back stack and handy to assert on in tests.
|
|
71
|
+
|
|
72
|
+
## react-router {#react-router}
|
|
73
|
+
|
|
74
|
+
**Out of the box — `MemoryRouter`, and none of the DOM components.**
|
|
75
|
+
react-router 7.18.2.
|
|
76
|
+
|
|
77
|
+
The router core — `useRoutes`, `useNavigate`, `useParams`, `useLocation`,
|
|
78
|
+
loaders, actions — is DOM-free. Import from `react-router`, not
|
|
79
|
+
`react-router-dom`, whose browser entry points are what fails.
|
|
80
|
+
|
|
81
|
+
```jsx
|
|
82
|
+
import React from 'react';
|
|
83
|
+
import { createRoot } from 'react-x11';
|
|
84
|
+
import {
|
|
85
|
+
MemoryRouter,
|
|
86
|
+
Routes,
|
|
87
|
+
Route,
|
|
88
|
+
useNavigate,
|
|
89
|
+
useLocation,
|
|
90
|
+
} from 'react-router';
|
|
91
|
+
|
|
92
|
+
function Home() {
|
|
93
|
+
const navigate = useNavigate();
|
|
94
|
+
return (
|
|
95
|
+
<box style={{ flexDirection: 'column' }}>
|
|
96
|
+
<text>home page</text>
|
|
97
|
+
<box style={{ height: 20 }} onClick={() => navigate('/about')}>
|
|
98
|
+
<text>go to about</text>
|
|
99
|
+
</box>
|
|
100
|
+
</box>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function About() {
|
|
105
|
+
const loc = useLocation();
|
|
106
|
+
return <text>{`about page (${loc.pathname})`}</text>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
(await createRoot()).render(
|
|
110
|
+
<window width={300} height={200} title="routed">
|
|
111
|
+
<box style={{ flexGrow: 1 }}>
|
|
112
|
+
<MemoryRouter initialEntries={['/']}>
|
|
113
|
+
<Routes>
|
|
114
|
+
<Route path="/" element={<Home />} />
|
|
115
|
+
<Route path="/about" element={<About />} />
|
|
116
|
+
<Route path="*" element={<text>not found</text>} />
|
|
117
|
+
</Routes>
|
|
118
|
+
</MemoryRouter>
|
|
119
|
+
</box>
|
|
120
|
+
</window>,
|
|
121
|
+
);
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
- **`BrowserRouter` and `HashRouter` fail** — `createBrowserHistory` needs
|
|
125
|
+
`window.history`. The error is
|
|
126
|
+
`react-x11: uncaught error ReferenceError: document is not defined` at
|
|
127
|
+
`getUrlBasedHistory`; `render()` does not throw, react-x11 logs it and the
|
|
128
|
+
window count stays 0.
|
|
129
|
+
- **`<Link>`, `<NavLink>` and `<Form>` all render `<a>`/`<form>` hosts** and
|
|
130
|
+
cannot work. Use `useNavigate()` on a `<box onClick>`.
|
|
131
|
+
- A `<Link>` with a bare string child hits the raw-text error before the
|
|
132
|
+
element error: `react-x11: raw text "go" must be wrapped in a <text>
|
|
133
|
+
element`. Both are in the
|
|
134
|
+
[register](../ecosystem.md#render-time).
|
|
135
|
+
- Without loaders and actions, wouter is the much smaller dependency.
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
# State management
|
|
2
|
+
|
|
3
|
+
Every store here subscribes through React's own `useSyncExternalStore` and
|
|
4
|
+
never touches a host environment. Their out-of-React surface matters more in
|
|
5
|
+
a desktop app than on the web: an X event callback, an ntk timer or a D-Bus
|
|
6
|
+
signal handler can write to the store directly, and every subscribed
|
|
7
|
+
component under every `<window>` re-renders.
|
|
8
|
+
|
|
9
|
+
Versions are the ones tested; newer releases in the same major are expected
|
|
10
|
+
to behave the same.
|
|
11
|
+
|
|
12
|
+
## zustand {#zustand}
|
|
13
|
+
|
|
14
|
+
**Out of the box.** zustand@5.0.14.
|
|
15
|
+
|
|
16
|
+
A small store: `create((set) => ({...}))` returns a hook that components
|
|
17
|
+
call with a selector, and `useStore.getState()` / `useStore.subscribe()`
|
|
18
|
+
work outside React. The default recommendation for app state.
|
|
19
|
+
|
|
20
|
+
```jsx
|
|
21
|
+
import React from 'react';
|
|
22
|
+
import { createRoot } from 'react-x11';
|
|
23
|
+
import { create } from 'zustand';
|
|
24
|
+
|
|
25
|
+
const useStore = create((set) => ({
|
|
26
|
+
n: 0,
|
|
27
|
+
inc: () => set((s) => ({ n: s.n + 1 })),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
function App() {
|
|
31
|
+
const n = useStore((s) => s.n);
|
|
32
|
+
return (
|
|
33
|
+
<window width={200} height={100} title="counter">
|
|
34
|
+
<box style={{ padding: 8 }} onClick={() => useStore.getState().inc()}>
|
|
35
|
+
<text>count: {n}</text>
|
|
36
|
+
</box>
|
|
37
|
+
</window>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
(await createRoot()).render(<App />);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The store is module-level, so several windows share it.
|
|
45
|
+
|
|
46
|
+
- `zustand/middleware`'s `persist` defaults to `localStorage`; pass a
|
|
47
|
+
`storage` backed by `node:fs` (`createJSONStorage(() => fileStorage)`).
|
|
48
|
+
- `zustand/traditional` (`createWithEqualityFn`) also works; the
|
|
49
|
+
shallow-compare helper is pure.
|
|
50
|
+
|
|
51
|
+
## jotai {#jotai}
|
|
52
|
+
|
|
53
|
+
**Out of the box.** jotai@2.20.2.
|
|
54
|
+
|
|
55
|
+
Atomic state: `atom(initial)` defines a unit, `useAtom`/`useAtomValue`/
|
|
56
|
+
`useSetAtom` read and write it, and derived atoms recompute from their
|
|
57
|
+
dependencies. In a desktop app use an explicit store: create it at startup,
|
|
58
|
+
hand it to `<Provider>`, and let X event handlers or background work call
|
|
59
|
+
`store.set(...)`. One store shared by several `<window>` trees keeps them in
|
|
60
|
+
sync.
|
|
61
|
+
|
|
62
|
+
```jsx
|
|
63
|
+
import React from 'react';
|
|
64
|
+
import { createRoot } from 'react-x11';
|
|
65
|
+
import { atom, createStore, Provider, useAtomValue } from 'jotai';
|
|
66
|
+
|
|
67
|
+
const countAtom = atom(0);
|
|
68
|
+
const doubledAtom = atom((get) => get(countAtom) * 2);
|
|
69
|
+
const store = createStore();
|
|
70
|
+
|
|
71
|
+
function App() {
|
|
72
|
+
const doubled = useAtomValue(doubledAtom);
|
|
73
|
+
return (
|
|
74
|
+
<window width={200} height={100} title="jotai">
|
|
75
|
+
<text>doubled: {doubled}</text>
|
|
76
|
+
</window>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
(await createRoot()).render(
|
|
81
|
+
<Provider store={store}>
|
|
82
|
+
<App />
|
|
83
|
+
</Provider>,
|
|
84
|
+
);
|
|
85
|
+
setInterval(() => store.set(countAtom, (n) => n + 1), 1000);
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
- `jotai/utils`' `atomWithStorage` defaults to `localStorage`; supply a
|
|
89
|
+
custom storage (sync or async).
|
|
90
|
+
- Async atoms suspend; wrap consumers in `<React.Suspense>` (react-x11
|
|
91
|
+
renders fallbacks like any React 19 host) or use `loadable()`.
|
|
92
|
+
|
|
93
|
+
## valtio {#valtio}
|
|
94
|
+
|
|
95
|
+
**Out of the box.** valtio@2.3.2.
|
|
96
|
+
|
|
97
|
+
Proxy-based state: `proxy(obj)` returns an object you mutate directly;
|
|
98
|
+
`useSnapshot(state)` returns an immutable snapshot and re-renders only when
|
|
99
|
+
the properties read change. Mutation from anywhere suits X event handlers
|
|
100
|
+
and ntk callbacks; `subscribe(state, cb)` pushes state into non-React things
|
|
101
|
+
— a selection onto the clipboard, a window title set imperatively.
|
|
102
|
+
|
|
103
|
+
```jsx
|
|
104
|
+
import React from 'react';
|
|
105
|
+
import { createRoot } from 'react-x11';
|
|
106
|
+
import { proxy, useSnapshot } from 'valtio';
|
|
107
|
+
|
|
108
|
+
const state = proxy({ n: 0, log: [] });
|
|
109
|
+
|
|
110
|
+
function App() {
|
|
111
|
+
const snap = useSnapshot(state);
|
|
112
|
+
return (
|
|
113
|
+
<window width={220} height={120} title="valtio">
|
|
114
|
+
<box
|
|
115
|
+
style={{ padding: 8 }}
|
|
116
|
+
onClick={() => {
|
|
117
|
+
state.n += 1;
|
|
118
|
+
state.log.push(Date.now());
|
|
119
|
+
}}
|
|
120
|
+
>
|
|
121
|
+
<text>clicked {snap.n} times</text>
|
|
122
|
+
</box>
|
|
123
|
+
</window>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
(await createRoot()).render(<App />);
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
- Valtio 2 no longer deep-clones snapshots in dev the way v1 did; read the
|
|
131
|
+
v2 migration notes if you port v1 examples.
|
|
132
|
+
- `valtio/utils`' `devtools()` targets the Redux DevTools browser extension
|
|
133
|
+
and is a no-op here.
|
|
134
|
+
|
|
135
|
+
## XState {#xstate}
|
|
136
|
+
|
|
137
|
+
**Out of the box.** xstate@5.32.5, @xstate/react@6.1.0.
|
|
138
|
+
|
|
139
|
+
`createMachine` describes states, events, guards and actions;
|
|
140
|
+
`createActor` interprets one; `@xstate/react` provides `useMachine`,
|
|
141
|
+
`useActor`, `useActorRef` and `useSelector`. Statecharts suit desktop
|
|
142
|
+
chrome: drag-resize, modal and focus flows, a connection lifecycle
|
|
143
|
+
(`connecting → connected → retrying`), cancellable jobs. An actor started at
|
|
144
|
+
app level can outlive any single `<window>` and drive several.
|
|
145
|
+
|
|
146
|
+
```jsx
|
|
147
|
+
import React from 'react';
|
|
148
|
+
import { createRoot } from 'react-x11';
|
|
149
|
+
import { createMachine, createActor } from 'xstate';
|
|
150
|
+
import { useSelector } from '@xstate/react';
|
|
151
|
+
|
|
152
|
+
const toggle = createMachine({
|
|
153
|
+
id: 'toggle',
|
|
154
|
+
initial: 'off',
|
|
155
|
+
states: {
|
|
156
|
+
off: { on: { TOGGLE: 'on' } },
|
|
157
|
+
on: { on: { TOGGLE: 'off' } },
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
const actor = createActor(toggle).start();
|
|
161
|
+
|
|
162
|
+
function App() {
|
|
163
|
+
const value = useSelector(actor, (s) => s.value);
|
|
164
|
+
return (
|
|
165
|
+
<window width={220} height={120} title="xstate">
|
|
166
|
+
<box
|
|
167
|
+
style={{ padding: 8 }}
|
|
168
|
+
onClick={() => actor.send({ type: 'TOGGLE' })}
|
|
169
|
+
>
|
|
170
|
+
<text>state: {value}</text>
|
|
171
|
+
</box>
|
|
172
|
+
</window>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
(await createRoot()).render(<App />);
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
- `useMachine(machine)` inside a component gives a component-scoped actor;
|
|
180
|
+
the external actor above is better for machines shared across windows.
|
|
181
|
+
- `@statelyai/inspect` connects over WebSocket to Stately's editor, which
|
|
182
|
+
works from Node; the iframe inspector expects a browser.
|
|
183
|
+
|
|
184
|
+
## Redux {#redux}
|
|
185
|
+
|
|
186
|
+
**Out of the box — pin `react-redux@>=9`.** react-redux@9.3.0,
|
|
187
|
+
@reduxjs/toolkit@2.12.0, redux@5.0.1.
|
|
188
|
+
|
|
189
|
+
react-redux 9 no longer imports `react-dom`/`react-native` for
|
|
190
|
+
`unstable_batchedUpdates` and subscribes through `useSyncExternalStore`, so
|
|
191
|
+
it needs no shim. redux core and RTK, listener and observable middleware
|
|
192
|
+
included, are pure JS.
|
|
193
|
+
|
|
194
|
+
```jsx
|
|
195
|
+
import React from 'react';
|
|
196
|
+
import { createRoot } from 'react-x11';
|
|
197
|
+
import { configureStore, createSlice } from '@reduxjs/toolkit';
|
|
198
|
+
import { Provider, useSelector, useDispatch } from 'react-redux';
|
|
199
|
+
|
|
200
|
+
const counter = createSlice({
|
|
201
|
+
name: 'counter',
|
|
202
|
+
initialState: { n: 0 },
|
|
203
|
+
reducers: {
|
|
204
|
+
inc: (s) => {
|
|
205
|
+
s.n += 1;
|
|
206
|
+
}, // immer draft — mutation is fine
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
const store = configureStore({ reducer: { counter: counter.reducer } });
|
|
210
|
+
|
|
211
|
+
function Counter() {
|
|
212
|
+
const n = useSelector((s) => s.counter.n);
|
|
213
|
+
const dispatch = useDispatch();
|
|
214
|
+
return (
|
|
215
|
+
<box style={{ padding: 8 }} onClick={() => dispatch(counter.actions.inc())}>
|
|
216
|
+
<text>count: {n}</text>
|
|
217
|
+
</box>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
(await createRoot()).render(
|
|
222
|
+
<Provider store={store}>
|
|
223
|
+
<window width={220} height={120} title="redux">
|
|
224
|
+
<Counter />
|
|
225
|
+
</window>
|
|
226
|
+
</Provider>,
|
|
227
|
+
);
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
- **The version pin is the one thing you must know.** react-redux 8 imports
|
|
231
|
+
`react-dom` unconditionally: npm refuses it with `ERESOLVE` on React 19,
|
|
232
|
+
and behind `--legacy-peer-deps` the app fails at startup with
|
|
233
|
+
`Cannot find module 'react-dom'`. Both are in the
|
|
234
|
+
[negative-results register](../ecosystem.md#install-and-import-time).
|
|
235
|
+
- RTK's `devTools: true` (the default) looks for the Redux DevTools
|
|
236
|
+
extension global and is harmless when absent; `@redux-devtools/cli` is a
|
|
237
|
+
remote devtools server if you want inspection.
|
|
238
|
+
- RTK Query works, but `setupListeners()` wires
|
|
239
|
+
`window.addEventListener('focus'|'online')`, absent under Node. Pass
|
|
240
|
+
`setupListeners(dispatch, customHandler)` a handler wired to
|
|
241
|
+
`<window onFocus>`, as in the
|
|
242
|
+
[TanStack Query adapter](data-fetching.md#tanstack-query).
|
|
243
|
+
|
|
244
|
+
## Immer {#immer}
|
|
245
|
+
|
|
246
|
+
**Out of the box.** immer@11.1.15.
|
|
247
|
+
|
|
248
|
+
`produce(base, recipe)` runs a mutating recipe against a Proxy draft and
|
|
249
|
+
returns a structurally-shared immutable result; curried `produce(recipe)` is
|
|
250
|
+
a drop-in `setState` updater. It is already inside `@reduxjs/toolkit`'s
|
|
251
|
+
`createSlice` reducers and available as zustand middleware
|
|
252
|
+
(`zustand/middleware/immer`).
|
|
253
|
+
|
|
254
|
+
```jsx
|
|
255
|
+
import React from 'react';
|
|
256
|
+
import { createRoot } from 'react-x11';
|
|
257
|
+
import { produce } from 'immer';
|
|
258
|
+
|
|
259
|
+
function App() {
|
|
260
|
+
const [state, setState] = React.useState({
|
|
261
|
+
items: [{ label: 'a', done: false }],
|
|
262
|
+
});
|
|
263
|
+
const toggle = (i) =>
|
|
264
|
+
setState(
|
|
265
|
+
produce((draft) => {
|
|
266
|
+
draft.items[i].done = !draft.items[i].done;
|
|
267
|
+
}),
|
|
268
|
+
);
|
|
269
|
+
return (
|
|
270
|
+
<window width={260} height={160} title="todo">
|
|
271
|
+
{state.items.map((it, i) => (
|
|
272
|
+
<box key={it.label} style={{ padding: 4 }} onClick={() => toggle(i)}>
|
|
273
|
+
<text>
|
|
274
|
+
{it.done ? '[x] ' : '[ ] '}
|
|
275
|
+
{it.label}
|
|
276
|
+
</text>
|
|
277
|
+
</box>
|
|
278
|
+
))}
|
|
279
|
+
</window>
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
(await createRoot()).render(<App />);
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Useful directly with `useState`/`useReducer` for the nested state desktop
|
|
287
|
+
apps accumulate — tree views, multi-pane layouts.
|
|
288
|
+
|
|
289
|
+
## MobX {#mobx}
|
|
290
|
+
|
|
291
|
+
**Adapter — no code, but `react-dom` must be installed.** mobx@6.16.1,
|
|
292
|
+
mobx-react-lite@4.1.1.
|
|
293
|
+
|
|
294
|
+
Observable state via `makeAutoObservable`/`observable`; `mobx-react-lite`'s
|
|
295
|
+
`observer()` re-renders a component when the observables it dereferences
|
|
296
|
+
change. mobx core is pure JS.
|
|
297
|
+
|
|
298
|
+
**Every build of mobx-react-lite does
|
|
299
|
+
`import { unstable_batchedUpdates } from "react-dom"`** at module load, to
|
|
300
|
+
install React's batcher as mobx's reaction scheduler. Without react-dom the
|
|
301
|
+
app fails at startup with `Cannot find module 'react-dom'`.
|
|
302
|
+
|
|
303
|
+
The fix is `npm install react-dom`. Its top-level entry is Node-safe (the
|
|
304
|
+
one SSR uses), react-x11 never renders through it, and React 19's
|
|
305
|
+
`unstable_batchedUpdates` batches correctly for any renderer. With it
|
|
306
|
+
present, `observer` subscribes through `use-sync-external-store` and
|
|
307
|
+
mutations from X event handlers or timers re-render the tree.
|
|
308
|
+
|
|
309
|
+
```jsx
|
|
310
|
+
// prerequisite: npm install react-dom (satisfies mobx-react-lite's import;
|
|
311
|
+
// react-x11 does not render through it)
|
|
312
|
+
import React from 'react';
|
|
313
|
+
import { createRoot } from 'react-x11';
|
|
314
|
+
import { makeAutoObservable, runInAction } from 'mobx';
|
|
315
|
+
import { observer } from 'mobx-react-lite';
|
|
316
|
+
|
|
317
|
+
const store = makeAutoObservable({ n: 0 });
|
|
318
|
+
|
|
319
|
+
const App = observer(function App() {
|
|
320
|
+
return (
|
|
321
|
+
<window width={220} height={120} title="mobx">
|
|
322
|
+
<box
|
|
323
|
+
style={{ padding: 8 }}
|
|
324
|
+
onClick={() =>
|
|
325
|
+
runInAction(() => {
|
|
326
|
+
store.n += 1;
|
|
327
|
+
})
|
|
328
|
+
}
|
|
329
|
+
>
|
|
330
|
+
<text>count: {store.n}</text>
|
|
331
|
+
</box>
|
|
332
|
+
</window>
|
|
333
|
+
);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
(await createRoot()).render(<App />);
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
- Without react-dom the failure is at import time, not render time, and the
|
|
340
|
+
require stack names mobx-react-lite.
|
|
341
|
+
- `mobx-react` layers on mobx-react-lite, so the same applies. Prefer
|
|
342
|
+
`-lite`.
|
|
343
|
+
- Do not import from `react-dom` yourself; it is a compatibility shim here,
|
|
344
|
+
nothing more.
|