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.
Files changed (121) hide show
  1. package/README.md +94 -61
  2. package/docs/README.md +256 -0
  3. package/docs/accessibility.md +575 -0
  4. package/docs/appearance.md +317 -0
  5. package/docs/click-to-component.md +92 -0
  6. package/docs/clipboard.md +262 -0
  7. package/docs/components.md +1231 -0
  8. package/docs/dbus.md +205 -0
  9. package/docs/debugging.md +371 -0
  10. package/docs/desktop-calendar.md +247 -0
  11. package/docs/desktop.md +523 -0
  12. package/docs/devtools.md +133 -0
  13. package/docs/drag-and-drop.md +585 -0
  14. package/docs/ecosystem/animation.md +360 -0
  15. package/docs/ecosystem/data-fetching.md +186 -0
  16. package/docs/ecosystem/dev-tooling.md +393 -0
  17. package/docs/ecosystem/forms.md +277 -0
  18. package/docs/ecosystem/headless.md +194 -0
  19. package/docs/ecosystem/i18n.md +86 -0
  20. package/docs/ecosystem/icons.md +377 -0
  21. package/docs/ecosystem/layout.md +206 -0
  22. package/docs/ecosystem/routing.md +135 -0
  23. package/docs/ecosystem/state.md +344 -0
  24. package/docs/ecosystem/testing.md +474 -0
  25. package/docs/ecosystem/theming.md +494 -0
  26. package/docs/ecosystem.md +439 -0
  27. package/docs/elements.md +2079 -0
  28. package/docs/embedding.md +239 -0
  29. package/docs/events.md +742 -0
  30. package/docs/extending.md +1700 -0
  31. package/docs/eyedropper.md +164 -0
  32. package/docs/filedialog.md +222 -0
  33. package/docs/frame.md +436 -0
  34. package/docs/gl.md +173 -0
  35. package/docs/globalmenu.md +195 -0
  36. package/docs/macos.md +592 -0
  37. package/docs/notifications.md +150 -0
  38. package/docs/packaging.md +468 -0
  39. package/docs/permissions.md +143 -0
  40. package/docs/react-features.md +405 -0
  41. package/docs/remote.md +179 -0
  42. package/docs/scale.md +225 -0
  43. package/docs/security.md +132 -0
  44. package/docs/styling.md +1710 -0
  45. package/docs/system.md +519 -0
  46. package/docs/testing.md +494 -0
  47. package/docs/typescript.md +75 -0
  48. package/docs/uri-schemes.md +340 -0
  49. package/docs/wayland-backend.md +332 -0
  50. package/docs/windows-integrations.md +177 -0
  51. package/docs/windows.md +380 -0
  52. package/package.json +11 -3
  53. package/src/Reconciler.js +2 -2
  54. package/src/a11y.js +1 -1
  55. package/src/backend/context2d.js +3 -3
  56. package/src/bootstrap.js +1 -1
  57. package/src/bus.js +39 -26
  58. package/src/cli/create.d.ts +43 -0
  59. package/src/cli/create.js +305 -0
  60. package/src/cli/env.js +484 -0
  61. package/src/cli/index.js +29 -2
  62. package/src/cli/templates/AGENTS.template.md +195 -0
  63. package/src/cli/templates/DESIGN.template.md +269 -0
  64. package/src/cocoa/app.js +6 -6
  65. package/src/cocoa/bezels.js +1 -1
  66. package/src/cocoa/fonts.js +1 -1
  67. package/src/cocoa/globalmenu.js +1 -1
  68. package/src/cocoa/keymap.js +1 -1
  69. package/src/cocoa/panewindow.js +1 -1
  70. package/src/cocoa/player.js +1 -1
  71. package/src/cocoa/presenter.js +2 -2
  72. package/src/cocoa/promotion.js +1 -1
  73. package/src/cocoa/relaunch.js +1 -1
  74. package/src/cocoa/threaded.js +1 -1
  75. package/src/cocoa/video.js +1 -1
  76. package/src/cocoa/window.js +4 -4
  77. package/src/components/Menu.js +7 -2
  78. package/src/components/Switch.js +1 -1
  79. package/src/components/native.js +1 -1
  80. package/src/desktopintegration.js +2 -1
  81. package/src/desktopsettings.js +193 -24
  82. package/src/dnd.js +2 -2
  83. package/src/filedialog.js +3 -3
  84. package/src/glnodes.js +1 -1
  85. package/src/grid.js +1 -1
  86. package/src/keyboardstate.js +28 -0
  87. package/src/layouts.js +1 -1
  88. package/src/node.d.ts +10 -10
  89. package/src/nodes/animation.js +1 -1
  90. package/src/nodes/invalidate.js +1 -1
  91. package/src/nodes/layouthost.js +1 -1
  92. package/src/nodes/video.js +19 -8
  93. package/src/nodes/window/droptarget.js +1 -1
  94. package/src/pacing.js +1 -1
  95. package/src/portal.js +31 -0
  96. package/src/refresh/index.js +2 -1
  97. package/src/screencolor.js +5 -10
  98. package/src/types/system.d.ts +8 -6
  99. package/src/types/video.d.ts +5 -2
  100. package/src/videobackend.js +28 -0
  101. package/src/videoframes.js +29 -16
  102. package/src/wayland/app.js +86 -0
  103. package/src/wayland/clipboard.js +14 -7
  104. package/src/wayland/connection.js +3 -0
  105. package/src/wayland/context2d.js +129 -10
  106. package/src/wayland/protocols/index.json +6 -0
  107. package/src/wayland/protocols/xdg-foreign-unstable-v2.json +1 -0
  108. package/src/wayland/seat.js +28 -1
  109. package/src/wayland/swapchain.js +23 -0
  110. package/src/wayland/video.js +376 -0
  111. package/src/wayland/xkb.js +137 -0
  112. package/src/win32/a11y.js +1 -1
  113. package/src/win32/app.js +2 -2
  114. package/src/win32/fonts.js +1 -1
  115. package/src/win32/glarea.js +4 -4
  116. package/src/win32/overlay.js +1 -1
  117. package/src/win32/panewindow.js +3 -3
  118. package/src/win32/shell.js +1 -1
  119. package/src/win32/window.js +2 -2
  120. package/src/windowid.js +1 -1
  121. 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.