react-x11 2.51.0 → 2.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +10 -3
- 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,146 @@
|
|
|
1
|
+
# Routing
|
|
2
|
+
|
|
3
|
+
There is no URL bar and no `history` object, so every router on this page
|
|
4
|
+
runs on an in-memory location. That is not a workaround — both libraries ship
|
|
5
|
+
a first-party memory backend, because both already support React Native and
|
|
6
|
+
testing.
|
|
7
|
+
|
|
8
|
+
What consistently does not survive is the _link_ element. `<Link>` renders an
|
|
9
|
+
`<a>` host in both routers, and there is no `<a>` here. Navigate imperatively
|
|
10
|
+
from a `<box onClick>` instead; that is two lines and it is all you lose.
|
|
11
|
+
|
|
12
|
+
Also worth saying up front: a desktop app usually wants stack or tab
|
|
13
|
+
navigation semantics rather than URL paths. These libraries give you the
|
|
14
|
+
matching machinery, not a back stack — build that on top if you need it.
|
|
15
|
+
|
|
16
|
+
## wouter {#wouter}
|
|
17
|
+
|
|
18
|
+
**Out of the box — with `wouter/memory-location`.** wouter@3.10.0.
|
|
19
|
+
|
|
20
|
+
A 2 kB router: `<Router>`, `<Route>`, `<Switch>`, `useLocation`, pattern
|
|
21
|
+
params. Location sources are pluggable hooks and the package ships a
|
|
22
|
+
non-browser one, so there is no adapter to write.
|
|
23
|
+
|
|
24
|
+
```jsx
|
|
25
|
+
import React from 'react';
|
|
26
|
+
import { createRoot } from 'react-x11';
|
|
27
|
+
import { Router, Route, Switch } from 'wouter';
|
|
28
|
+
import { memoryLocation } from 'wouter/memory-location';
|
|
29
|
+
|
|
30
|
+
const { hook, navigate } = memoryLocation({ path: '/' });
|
|
31
|
+
|
|
32
|
+
// the react-x11 replacement for <Link>
|
|
33
|
+
const NavText = ({ href, children }) => (
|
|
34
|
+
<box style={{ height: 20 }} onClick={() => navigate(href)}>
|
|
35
|
+
<text style={{ color: '#08c' }}>{children}</text>
|
|
36
|
+
</box>
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const App = () => (
|
|
40
|
+
<window width={400} height={300} title="routed">
|
|
41
|
+
<box style={{ flexGrow: 1, flexDirection: 'column' }}>
|
|
42
|
+
<Router hook={hook}>
|
|
43
|
+
<box style={{ flexDirection: 'row', gap: 8, height: 20 }}>
|
|
44
|
+
<NavText href="/">home</NavText>
|
|
45
|
+
<NavText href="/about">about</NavText>
|
|
46
|
+
</box>
|
|
47
|
+
<Switch>
|
|
48
|
+
<Route path="/">
|
|
49
|
+
<text>home page</text>
|
|
50
|
+
</Route>
|
|
51
|
+
<Route path="/about">
|
|
52
|
+
<text>about page</text>
|
|
53
|
+
</Route>
|
|
54
|
+
<Route>
|
|
55
|
+
<text>not found</text>
|
|
56
|
+
</Route>
|
|
57
|
+
</Switch>
|
|
58
|
+
</Router>
|
|
59
|
+
</box>
|
|
60
|
+
</window>
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
(await createRoot()).render(<App />);
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
- **Always mount the memory hook at the top.** A `<Route>` outside a
|
|
67
|
+
`<Router hook={...}>` falls back to `useBrowserLocation` and crashes
|
|
68
|
+
immediately with `ReferenceError: location is not defined`.
|
|
69
|
+
- **`<Link>` does not work.** It renders an `<a>` host element.
|
|
70
|
+
`<Link asChild>` avoids the `<a>` but still does not navigate: its click
|
|
71
|
+
guard checks `event.button !== 0`, and react-x11's synthetic
|
|
72
|
+
`MouseEvent.button` is the X button number, which is `1` for left.
|
|
73
|
+
- `useLocation()[1]` gives you the same `navigate` from inside components;
|
|
74
|
+
the module-level one from `memoryLocation` is just more convenient with a
|
|
75
|
+
single router.
|
|
76
|
+
- `useHashLocation` and `useBrowserLocation` are browser-only. Everything
|
|
77
|
+
else — matching, params, nesting, `useRoute` — is pure.
|
|
78
|
+
- `memoryLocation({ record: true })` keeps a `history` array, which is the
|
|
79
|
+
raw material for a back stack and handy to assert on in tests.
|
|
80
|
+
|
|
81
|
+
## react-router {#react-router}
|
|
82
|
+
|
|
83
|
+
**Out of the box — `MemoryRouter`, and none of the DOM components.**
|
|
84
|
+
react-router 7.18.2.
|
|
85
|
+
|
|
86
|
+
The router core — `useRoutes`, `useNavigate`, `useParams`, `useLocation`,
|
|
87
|
+
loaders, actions — is DOM-free. Import from `react-router`, not
|
|
88
|
+
`react-router-dom`; the DOM package re-exports the core plus the browser
|
|
89
|
+
entry points, and the browser entry points are exactly what fails.
|
|
90
|
+
|
|
91
|
+
```jsx
|
|
92
|
+
import React from 'react';
|
|
93
|
+
import { createRoot } from 'react-x11';
|
|
94
|
+
import {
|
|
95
|
+
MemoryRouter,
|
|
96
|
+
Routes,
|
|
97
|
+
Route,
|
|
98
|
+
useNavigate,
|
|
99
|
+
useLocation,
|
|
100
|
+
} from 'react-router';
|
|
101
|
+
|
|
102
|
+
function Home() {
|
|
103
|
+
const navigate = useNavigate();
|
|
104
|
+
return (
|
|
105
|
+
<box style={{ flexDirection: 'column' }}>
|
|
106
|
+
<text>home page</text>
|
|
107
|
+
<box style={{ height: 20 }} onClick={() => navigate('/about')}>
|
|
108
|
+
<text>go to about</text>
|
|
109
|
+
</box>
|
|
110
|
+
</box>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function About() {
|
|
115
|
+
const loc = useLocation();
|
|
116
|
+
return <text>{`about page (${loc.pathname})`}</text>;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
(await createRoot()).render(
|
|
120
|
+
<window width={300} height={200} title="routed">
|
|
121
|
+
<box style={{ flexGrow: 1 }}>
|
|
122
|
+
<MemoryRouter initialEntries={['/']}>
|
|
123
|
+
<Routes>
|
|
124
|
+
<Route path="/" element={<Home />} />
|
|
125
|
+
<Route path="/about" element={<About />} />
|
|
126
|
+
<Route path="*" element={<text>not found</text>} />
|
|
127
|
+
</Routes>
|
|
128
|
+
</MemoryRouter>
|
|
129
|
+
</box>
|
|
130
|
+
</window>,
|
|
131
|
+
);
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
- **`BrowserRouter` and `HashRouter` both fail** — they call
|
|
135
|
+
`createBrowserHistory`, which needs `window.history`. The error is
|
|
136
|
+
`react-x11: uncaught error ReferenceError: document is not defined` at
|
|
137
|
+
`getUrlBasedHistory`, and note that `render()` does not throw: react-x11
|
|
138
|
+
logs it and the window count stays 0.
|
|
139
|
+
- **`<Link>`, `<NavLink>` and `<Form>` all render `<a>`/`<form>` hosts** and
|
|
140
|
+
cannot work. Use `useNavigate()` on a `<box onClick>`.
|
|
141
|
+
- A `<Link>` with a bare string child hits the raw-text error before the
|
|
142
|
+
element error: `react-x11: raw text "go" must be wrapped in a <text>
|
|
143
|
+
element`. Both are in the
|
|
144
|
+
[register](../ecosystem.md#render-time).
|
|
145
|
+
- If you do not need loaders and actions, wouter above is the smaller
|
|
146
|
+
dependency by a wide margin.
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
# State management
|
|
2
|
+
|
|
3
|
+
Every store here is renderer-agnostic: they subscribe through React's own
|
|
4
|
+
`useSyncExternalStore` and never touch a host environment. The out-of-React
|
|
5
|
+
surface matters more in a desktop app than on the web — an X event callback,
|
|
6
|
+
an ntk timer or a D-Bus signal handler can write to the store directly, and
|
|
7
|
+
every subscribed component under every `<window>` re-renders.
|
|
8
|
+
|
|
9
|
+
Versions below are what was tested; anything newer in the same major is
|
|
10
|
+
expected to behave the same.
|
|
11
|
+
|
|
12
|
+
## zustand {#zustand}
|
|
13
|
+
|
|
14
|
+
**Out of the box.** zustand@5.0.14.
|
|
15
|
+
|
|
16
|
+
A small unopinionated store: `create((set) => ({...}))` returns a hook that
|
|
17
|
+
components call with a selector. Subscription goes through
|
|
18
|
+
`useSyncExternalStore` imported from `react` itself, and the store is usable
|
|
19
|
+
outside React via `useStore.getState()` / `useStore.subscribe()`. The
|
|
20
|
+
default recommendation for app state: zero DOM, zero adapter.
|
|
21
|
+
|
|
22
|
+
```jsx
|
|
23
|
+
import React from 'react';
|
|
24
|
+
import { createRoot } from 'react-x11';
|
|
25
|
+
import { create } from 'zustand';
|
|
26
|
+
|
|
27
|
+
const useStore = create((set) => ({
|
|
28
|
+
n: 0,
|
|
29
|
+
inc: () => set((s) => ({ n: s.n + 1 })),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
function App() {
|
|
33
|
+
const n = useStore((s) => s.n);
|
|
34
|
+
return (
|
|
35
|
+
<window width={200} height={100} title="counter">
|
|
36
|
+
<box style={{ padding: 8 }} onClick={() => useStore.getState().inc()}>
|
|
37
|
+
<text>count: {n}</text>
|
|
38
|
+
</box>
|
|
39
|
+
</window>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
(await createRoot()).render(<App />);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Because the store is module-level rather than tree-level, several windows
|
|
47
|
+
share it for free.
|
|
48
|
+
|
|
49
|
+
- `zustand/middleware`'s `persist` defaults to `localStorage`; pass a
|
|
50
|
+
`storage` backed by `node:fs` (`createJSONStorage(() => fileStorage)`).
|
|
51
|
+
- `zustand/traditional` (`createWithEqualityFn`) also works; the
|
|
52
|
+
shallow-compare helper is pure.
|
|
53
|
+
|
|
54
|
+
## jotai {#jotai}
|
|
55
|
+
|
|
56
|
+
**Out of the box.** jotai@2.20.2.
|
|
57
|
+
|
|
58
|
+
Bottom-up atomic state: `atom(initial)` defines a unit of state,
|
|
59
|
+
`useAtom`/`useAtomValue`/`useSetAtom` read and write it, and derived atoms
|
|
60
|
+
recompute from their dependencies. Atoms are pure and the React bindings
|
|
61
|
+
import only `react`.
|
|
62
|
+
|
|
63
|
+
The explicit-store pattern is the right one for a desktop app: create the
|
|
64
|
+
store at startup, hand it to the tree, and let X event handlers or
|
|
65
|
+
background work call `store.set(...)`. One store shared by several
|
|
66
|
+
`<window>` trees keeps them in sync automatically.
|
|
67
|
+
|
|
68
|
+
```jsx
|
|
69
|
+
import React from 'react';
|
|
70
|
+
import { createRoot } from 'react-x11';
|
|
71
|
+
import { atom, createStore, Provider, useAtomValue } from 'jotai';
|
|
72
|
+
|
|
73
|
+
const countAtom = atom(0);
|
|
74
|
+
const doubledAtom = atom((get) => get(countAtom) * 2);
|
|
75
|
+
const store = createStore();
|
|
76
|
+
|
|
77
|
+
function App() {
|
|
78
|
+
const doubled = useAtomValue(doubledAtom);
|
|
79
|
+
return (
|
|
80
|
+
<window width={200} height={100} title="jotai">
|
|
81
|
+
<text>doubled: {doubled}</text>
|
|
82
|
+
</window>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
(await createRoot()).render(
|
|
87
|
+
<Provider store={store}>
|
|
88
|
+
<App />
|
|
89
|
+
</Provider>,
|
|
90
|
+
);
|
|
91
|
+
setInterval(() => store.set(countAtom, (n) => n + 1), 1000);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
- `jotai/utils`' `atomWithStorage` defaults to `localStorage`; supply a
|
|
95
|
+
custom storage (sync or async).
|
|
96
|
+
- Async atoms suspend; wrap consumers in `<React.Suspense>` — react-x11
|
|
97
|
+
renders fallbacks like any React 19 host — or use `loadable()`.
|
|
98
|
+
|
|
99
|
+
## valtio {#valtio}
|
|
100
|
+
|
|
101
|
+
**Out of the box.** valtio@2.3.2.
|
|
102
|
+
|
|
103
|
+
Proxy-based state: `proxy(obj)` returns an object you mutate directly;
|
|
104
|
+
`useSnapshot(state)` returns an immutable snapshot and tracks which
|
|
105
|
+
properties were read, re-rendering only when those change.
|
|
106
|
+
|
|
107
|
+
Mutation-from-anywhere maps well onto X event handlers and ntk callbacks.
|
|
108
|
+
`subscribe(state, cb)` is handy for pushing state into non-React things —
|
|
109
|
+
writing a selection to the X clipboard, updating a window title
|
|
110
|
+
imperatively.
|
|
111
|
+
|
|
112
|
+
```jsx
|
|
113
|
+
import React from 'react';
|
|
114
|
+
import { createRoot } from 'react-x11';
|
|
115
|
+
import { proxy, useSnapshot } from 'valtio';
|
|
116
|
+
|
|
117
|
+
const state = proxy({ n: 0, log: [] });
|
|
118
|
+
|
|
119
|
+
function App() {
|
|
120
|
+
const snap = useSnapshot(state);
|
|
121
|
+
return (
|
|
122
|
+
<window width={220} height={120} title="valtio">
|
|
123
|
+
<box
|
|
124
|
+
style={{ padding: 8 }}
|
|
125
|
+
onClick={() => {
|
|
126
|
+
state.n += 1;
|
|
127
|
+
state.log.push(Date.now());
|
|
128
|
+
}}
|
|
129
|
+
>
|
|
130
|
+
<text>clicked {snap.n} times</text>
|
|
131
|
+
</box>
|
|
132
|
+
</window>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
(await createRoot()).render(<App />);
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
- Valtio 2 no longer deep-clones snapshots the way v1 did in dev; read the
|
|
140
|
+
v2 migration notes if you are coming from v1 examples.
|
|
141
|
+
- `valtio/utils`' `devtools()` targets the Redux DevTools browser extension.
|
|
142
|
+
It is a no-op without one, and there is nothing to connect to under X11.
|
|
143
|
+
|
|
144
|
+
## XState {#xstate}
|
|
145
|
+
|
|
146
|
+
**Out of the box.** xstate@5.32.5, @xstate/react@6.1.0.
|
|
147
|
+
|
|
148
|
+
`createMachine` describes states, events, guards and actions; `createActor`
|
|
149
|
+
interprets one; `@xstate/react` provides `useMachine`, `useActor`,
|
|
150
|
+
`useActorRef` and `useSelector`. The interpreter has no environment coupling
|
|
151
|
+
and the React bindings subscribe via `useSyncExternalStore`.
|
|
152
|
+
|
|
153
|
+
Desktop chrome is where statecharts earn their keep: drag-resize state,
|
|
154
|
+
modal and focus flows, a connection lifecycle (`connecting → connected →
|
|
155
|
+
retrying`), long-running jobs with cancellation. An actor started at app
|
|
156
|
+
level can outlive any single `<window>` and drive several of them.
|
|
157
|
+
|
|
158
|
+
```jsx
|
|
159
|
+
import React from 'react';
|
|
160
|
+
import { createRoot } from 'react-x11';
|
|
161
|
+
import { createMachine, createActor } from 'xstate';
|
|
162
|
+
import { useSelector } from '@xstate/react';
|
|
163
|
+
|
|
164
|
+
const toggle = createMachine({
|
|
165
|
+
id: 'toggle',
|
|
166
|
+
initial: 'off',
|
|
167
|
+
states: {
|
|
168
|
+
off: { on: { TOGGLE: 'on' } },
|
|
169
|
+
on: { on: { TOGGLE: 'off' } },
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
const actor = createActor(toggle).start();
|
|
173
|
+
|
|
174
|
+
function App() {
|
|
175
|
+
const value = useSelector(actor, (s) => s.value);
|
|
176
|
+
return (
|
|
177
|
+
<window width={220} height={120} title="xstate">
|
|
178
|
+
<box
|
|
179
|
+
style={{ padding: 8 }}
|
|
180
|
+
onClick={() => actor.send({ type: 'TOGGLE' })}
|
|
181
|
+
>
|
|
182
|
+
<text>state: {value}</text>
|
|
183
|
+
</box>
|
|
184
|
+
</window>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
(await createRoot()).render(<App />);
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
- `useMachine(machine)` inside a component works too and gives you a
|
|
192
|
+
component-scoped actor; the external-actor pattern above is better for
|
|
193
|
+
machines shared across windows.
|
|
194
|
+
- `@statelyai/inspect` connects over WebSocket to Stately's editor. The
|
|
195
|
+
transport works from Node; the iframe inspector expects a browser.
|
|
196
|
+
|
|
197
|
+
## Redux {#redux}
|
|
198
|
+
|
|
199
|
+
**Out of the box — pin `react-redux@>=9`.** react-redux@9.3.0,
|
|
200
|
+
@reduxjs/toolkit@2.12.0, redux@5.0.1.
|
|
201
|
+
|
|
202
|
+
react-redux 9 dropped the `react-dom`/`react-native` imports it used for
|
|
203
|
+
`unstable_batchedUpdates` and subscribes through React's own
|
|
204
|
+
`useSyncExternalStore`, so it is renderer-agnostic and needs no shim here.
|
|
205
|
+
redux core and RTK are pure JS throughout, listener and observable
|
|
206
|
+
middleware included.
|
|
207
|
+
|
|
208
|
+
```jsx
|
|
209
|
+
import React from 'react';
|
|
210
|
+
import { createRoot } from 'react-x11';
|
|
211
|
+
import { configureStore, createSlice } from '@reduxjs/toolkit';
|
|
212
|
+
import { Provider, useSelector, useDispatch } from 'react-redux';
|
|
213
|
+
|
|
214
|
+
const counter = createSlice({
|
|
215
|
+
name: 'counter',
|
|
216
|
+
initialState: { n: 0 },
|
|
217
|
+
reducers: {
|
|
218
|
+
inc: (s) => {
|
|
219
|
+
s.n += 1;
|
|
220
|
+
}, // immer draft — mutation is fine
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
const store = configureStore({ reducer: { counter: counter.reducer } });
|
|
224
|
+
|
|
225
|
+
function Counter() {
|
|
226
|
+
const n = useSelector((s) => s.counter.n);
|
|
227
|
+
const dispatch = useDispatch();
|
|
228
|
+
return (
|
|
229
|
+
<box style={{ padding: 8 }} onClick={() => dispatch(counter.actions.inc())}>
|
|
230
|
+
<text>count: {n}</text>
|
|
231
|
+
</box>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
(await createRoot()).render(
|
|
236
|
+
<Provider store={store}>
|
|
237
|
+
<window width={220} height={120} title="redux">
|
|
238
|
+
<Counter />
|
|
239
|
+
</window>
|
|
240
|
+
</Provider>,
|
|
241
|
+
);
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
- **The version pin is the one thing you must know.** react-redux 8 imports
|
|
245
|
+
`react-dom` unconditionally, and the failure is a module-resolution error
|
|
246
|
+
at startup, not a runtime warning. Both of its failure modes — the npm
|
|
247
|
+
`ERESOLVE` on React 19 and the `Cannot find module 'react-dom'` behind
|
|
248
|
+
`--legacy-peer-deps` — are in the
|
|
249
|
+
[negative-results register](../ecosystem.md#install-and-import-time).
|
|
250
|
+
- RTK's `devTools: true` (the default) looks for the Redux DevTools
|
|
251
|
+
extension global; harmless when absent. `@redux-devtools/cli` is a remote
|
|
252
|
+
devtools server if you want inspection.
|
|
253
|
+
- RTK Query works, but `setupListeners()` wires
|
|
254
|
+
`window.addEventListener('focus'|'online')`, which are absent under Node.
|
|
255
|
+
Pass a custom handler to `setupListeners(dispatch, customHandler)` wired
|
|
256
|
+
to `<window onFocus>`, the same way as the
|
|
257
|
+
[TanStack Query adapter](data-fetching.md#tanstack-query).
|
|
258
|
+
|
|
259
|
+
## Immer {#immer}
|
|
260
|
+
|
|
261
|
+
**Out of the box.** immer@11.1.15.
|
|
262
|
+
|
|
263
|
+
`produce(base, recipe)` runs a mutating recipe against a Proxy draft and
|
|
264
|
+
returns a structurally-shared immutable result. Curried `produce(recipe)` is
|
|
265
|
+
a drop-in `setState` updater. Pure JS, no environment assumptions — and it
|
|
266
|
+
is already inside `@reduxjs/toolkit`'s `createSlice` reducers and available
|
|
267
|
+
as zustand middleware (`zustand/middleware/immer`).
|
|
268
|
+
|
|
269
|
+
```jsx
|
|
270
|
+
import React from 'react';
|
|
271
|
+
import { createRoot } from 'react-x11';
|
|
272
|
+
import { produce } from 'immer';
|
|
273
|
+
|
|
274
|
+
function App() {
|
|
275
|
+
const [state, setState] = React.useState({
|
|
276
|
+
items: [{ label: 'a', done: false }],
|
|
277
|
+
});
|
|
278
|
+
const toggle = (i) =>
|
|
279
|
+
setState(
|
|
280
|
+
produce((draft) => {
|
|
281
|
+
draft.items[i].done = !draft.items[i].done;
|
|
282
|
+
}),
|
|
283
|
+
);
|
|
284
|
+
return (
|
|
285
|
+
<window width={260} height={160} title="todo">
|
|
286
|
+
{state.items.map((it, i) => (
|
|
287
|
+
<box key={it.label} style={{ padding: 4 }} onClick={() => toggle(i)}>
|
|
288
|
+
<text>
|
|
289
|
+
{it.done ? '[x] ' : '[ ] '}
|
|
290
|
+
{it.label}
|
|
291
|
+
</text>
|
|
292
|
+
</box>
|
|
293
|
+
))}
|
|
294
|
+
</window>
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
(await createRoot()).render(<App />);
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Nothing to adapt. Useful directly with `useState`/`useReducer` for the
|
|
302
|
+
nested state desktop apps accumulate — tree views, multi-pane layouts.
|
|
303
|
+
|
|
304
|
+
## MobX {#mobx}
|
|
305
|
+
|
|
306
|
+
**Adapter — no code, but `react-dom` must be installed.** mobx@6.16.1,
|
|
307
|
+
mobx-react-lite@4.1.1.
|
|
308
|
+
|
|
309
|
+
Observable state via `makeAutoObservable`/`observable`, with
|
|
310
|
+
`mobx-react-lite`'s `observer()` HOC re-rendering components when the
|
|
311
|
+
observables they dereference change. mobx core is pure JS.
|
|
312
|
+
|
|
313
|
+
The wrinkle: **every build of mobx-react-lite unconditionally does
|
|
314
|
+
`import { unstable_batchedUpdates } from "react-dom"`** at module load, to
|
|
315
|
+
install React's batcher as mobx's reaction scheduler. In a react-x11 app
|
|
316
|
+
react-dom is not otherwise present, so the import fails at startup with
|
|
317
|
+
`Cannot find module 'react-dom'`.
|
|
318
|
+
|
|
319
|
+
The fix is `npm install react-dom`. Its top-level entry is Node-safe (it is
|
|
320
|
+
the same entry SSR uses), react-x11 never renders through it, and React 19's
|
|
321
|
+
`unstable_batchedUpdates` is a thin wrapper that batches correctly for any
|
|
322
|
+
renderer. With react-dom present, `observer` subscribes through
|
|
323
|
+
`use-sync-external-store` and mutations from X event handlers or timers
|
|
324
|
+
re-render the tree.
|
|
325
|
+
|
|
326
|
+
```jsx
|
|
327
|
+
// prerequisite: npm install react-dom (satisfies mobx-react-lite's import;
|
|
328
|
+
// react-x11 does not render through it)
|
|
329
|
+
import React from 'react';
|
|
330
|
+
import { createRoot } from 'react-x11';
|
|
331
|
+
import { makeAutoObservable, runInAction } from 'mobx';
|
|
332
|
+
import { observer } from 'mobx-react-lite';
|
|
333
|
+
|
|
334
|
+
const store = makeAutoObservable({ n: 0 });
|
|
335
|
+
|
|
336
|
+
const App = observer(function App() {
|
|
337
|
+
return (
|
|
338
|
+
<window width={220} height={120} title="mobx">
|
|
339
|
+
<box
|
|
340
|
+
style={{ padding: 8 }}
|
|
341
|
+
onClick={() =>
|
|
342
|
+
runInAction(() => {
|
|
343
|
+
store.n += 1;
|
|
344
|
+
})
|
|
345
|
+
}
|
|
346
|
+
>
|
|
347
|
+
<text>count: {store.n}</text>
|
|
348
|
+
</box>
|
|
349
|
+
</window>
|
|
350
|
+
);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
(await createRoot()).render(<App />);
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
- Without react-dom the app fails at import time, not render time; the error
|
|
357
|
+
names mobx-react-lite in its require stack.
|
|
358
|
+
- `mobx-react` (the bigger sibling) layers on mobx-react-lite, so the same
|
|
359
|
+
note applies. Prefer `-lite`.
|
|
360
|
+
- Do not import from `react-dom` yourself. It is a compatibility shim here,
|
|
361
|
+
nothing more.
|