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,360 @@
|
|
|
1
|
+
# Animation
|
|
2
|
+
|
|
3
|
+
react-x11's built-in `transition` style property has a fixed easing curve
|
|
4
|
+
([styling](../styling.md#transitions)). This page covers what it cannot
|
|
5
|
+
express: springs with velocity carry-over, interruption, arbitrary easing
|
|
6
|
+
curves, shape morphing, and keeping an element mounted while it animates
|
|
7
|
+
out.
|
|
8
|
+
|
|
9
|
+
**There is no `opacity` style property**, and `validateStyle` throws on
|
|
10
|
+
unknown properties, so the classic fade does not port; animate a colour, a
|
|
11
|
+
size or a position instead.
|
|
12
|
+
|
|
13
|
+
Two patterns recur. The cheap one is `onUpdate → setState → style prop`:
|
|
14
|
+
each frame is a React render plus a commit, which react-x11 turns into a
|
|
15
|
+
partial repaint of the changed node. Keep the animated component a leaf — a
|
|
16
|
+
`<box>`, not the window root — and it holds spring frame rates. The other is
|
|
17
|
+
the [react-spring host](#react-spring): more setup, but it applies frames
|
|
18
|
+
imperatively and bypasses React.
|
|
19
|
+
|
|
20
|
+
Node has no global `requestAnimationFrame`. In an app, use the ntk window's
|
|
21
|
+
`requestAnimationFrame` (through a ref on `<window>`), which paces frames to
|
|
22
|
+
X connection latency; a 16 ms `setInterval` works everywhere else.
|
|
23
|
+
|
|
24
|
+
`framer-motion` fails at both its component and its value layer — see the
|
|
25
|
+
[negative-results register](../ecosystem.md#render-time).
|
|
26
|
+
|
|
27
|
+
## popmotion {#popmotion}
|
|
28
|
+
|
|
29
|
+
**Out of the box.** popmotion@11.0.5.
|
|
30
|
+
|
|
31
|
+
`animate()` tweens or springs a plain JS value and calls `onUpdate(v)`
|
|
32
|
+
every frame. The smallest way to get real springs, with velocity and
|
|
33
|
+
overshoot, without the react-spring host.
|
|
34
|
+
|
|
35
|
+
```jsx
|
|
36
|
+
import React, { useEffect, useState } from 'react';
|
|
37
|
+
import { animate } from 'popmotion';
|
|
38
|
+
|
|
39
|
+
function Bar() {
|
|
40
|
+
const [w, setW] = useState(20);
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
const anim = animate({
|
|
43
|
+
from: 20,
|
|
44
|
+
to: 200,
|
|
45
|
+
stiffness: 300,
|
|
46
|
+
damping: 20,
|
|
47
|
+
onUpdate: setW,
|
|
48
|
+
});
|
|
49
|
+
return () => anim.stop();
|
|
50
|
+
}, []);
|
|
51
|
+
return <box style={{ width: w, height: 30, backgroundColor: '#3366ff' }} />;
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
With no `requestAnimationFrame`, popmotion falls back to a 16.7 ms
|
|
56
|
+
`setTimeout`, so it needs no configuration. For latency-adaptive pacing,
|
|
57
|
+
pass `animate()` a custom `driver` backed by the ntk window's clock.
|
|
58
|
+
|
|
59
|
+
- Springs on layout properties (width, margin) cost a yoga layout pass per
|
|
60
|
+
frame, as react-x11's own layout transitions do.
|
|
61
|
+
- `animate` interpolates colour strings (`'#f00' → '#00f'`), and the
|
|
62
|
+
`rgba(...)` strings it emits paint as-is.
|
|
63
|
+
- Use only the value APIs — `animate`, `spring`, `decay`, `mix` — not
|
|
64
|
+
`styler` or the DOM helpers.
|
|
65
|
+
|
|
66
|
+
## d3-ease {#d3-ease}
|
|
67
|
+
|
|
68
|
+
**Out of the box.** d3-ease@3.0.1.
|
|
69
|
+
|
|
70
|
+
Pure easing functions — `ease(t)` maps normalized time to progress — with
|
|
71
|
+
thirty-plus curves (cubic, elastic, bounce, back) the built-in `transition`
|
|
72
|
+
lacks. Compute `t` from wall time, pass `ease(t)` to an interpolator, push
|
|
73
|
+
the result into a style prop.
|
|
74
|
+
|
|
75
|
+
```jsx
|
|
76
|
+
import React, { useEffect, useState } from 'react';
|
|
77
|
+
import { easeCubicOut } from 'd3-ease';
|
|
78
|
+
import { interpolateNumber } from 'd3-interpolate';
|
|
79
|
+
|
|
80
|
+
function useTimeline(duration, ease, onFrame) {
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
const start = Date.now();
|
|
83
|
+
const timer = setInterval(() => {
|
|
84
|
+
const t = Math.min(1, (Date.now() - start) / duration);
|
|
85
|
+
onFrame(ease(t));
|
|
86
|
+
if (t >= 1) clearInterval(timer);
|
|
87
|
+
}, 16);
|
|
88
|
+
return () => clearInterval(timer);
|
|
89
|
+
}, []);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const width = interpolateNumber(10, 200);
|
|
93
|
+
|
|
94
|
+
function Gauge() {
|
|
95
|
+
const [f, setF] = useState(0);
|
|
96
|
+
useTimeline(250, easeCubicOut, setF);
|
|
97
|
+
return (
|
|
98
|
+
<box style={{ width: width(f), height: 24, backgroundColor: '#204080' }} />
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
- ESM-only since v3 — `import`, not `require()`.
|
|
104
|
+
- It is only the curve; you own the clock and the interpolation.
|
|
105
|
+
- Overshooting curves (elastic, back) leave the from/to range; clamp before
|
|
106
|
+
feeding anything react-x11 validates as non-negative.
|
|
107
|
+
|
|
108
|
+
## d3-interpolate {#d3-interpolate}
|
|
109
|
+
|
|
110
|
+
**Out of the box.** d3-interpolate@3.0.1.
|
|
111
|
+
|
|
112
|
+
`interpolate(a, b)` returns `t => value` for numbers, colours in any CSS
|
|
113
|
+
space, strings with embedded numbers, dates, arrays and nested objects.
|
|
114
|
+
`interpolateRgb` emits `'rgb(r, g, b)'` strings, which paint as-is. (The
|
|
115
|
+
interpolator behind react-x11's `transition` is not exported.)
|
|
116
|
+
|
|
117
|
+
```jsx
|
|
118
|
+
import { easeCubicOut } from 'd3-ease';
|
|
119
|
+
import { interpolateNumber, interpolateRgb } from 'd3-interpolate';
|
|
120
|
+
|
|
121
|
+
const width = interpolateNumber(10, 200);
|
|
122
|
+
const color = interpolateRgb('#204080', '#e04040');
|
|
123
|
+
|
|
124
|
+
function Gauge() {
|
|
125
|
+
const [f, setF] = useState(0);
|
|
126
|
+
useTimeline(250, easeCubicOut, setF); // see d3-ease above
|
|
127
|
+
return (
|
|
128
|
+
<box style={{ width: width(f), height: 24, backgroundColor: color(f) }} />
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
- ESM-only since v3.
|
|
134
|
+
- Generic `interpolate(a, b)` on objects interpolates _every_ numeric-looking
|
|
135
|
+
key, giving fractional values to properties that want integers.
|
|
136
|
+
Interpolate only the properties you mean.
|
|
137
|
+
- There is no `opacity`; to fade, interpolate a colour's alpha into
|
|
138
|
+
`backgroundColor: 'rgba(...)'`.
|
|
139
|
+
- `interpolateArray` over polygon points feeding `<canvas onDraw>` is a good
|
|
140
|
+
use of the general form.
|
|
141
|
+
|
|
142
|
+
## `@tweenjs/tween.js` {#tweenjs}
|
|
143
|
+
|
|
144
|
+
**Out of the box.** @tweenjs/tween.js@25.0.0.
|
|
145
|
+
|
|
146
|
+
`new Tween(obj).to(target, ms).easing(...).onUpdate(...).start()`, then
|
|
147
|
+
_you_ call `tween.update(time)` on your own clock — there is no implicit
|
|
148
|
+
global loop. In an app, pump from the ntk window's `requestAnimationFrame`;
|
|
149
|
+
with many tweens, one `Group` per window lets `group.update(now)` step them
|
|
150
|
+
all from the same tick.
|
|
151
|
+
|
|
152
|
+
```jsx
|
|
153
|
+
import React, { useEffect, useState } from 'react';
|
|
154
|
+
import { Tween, Easing } from '@tweenjs/tween.js';
|
|
155
|
+
|
|
156
|
+
function Panel() {
|
|
157
|
+
const [pos, setPos] = useState({ x: 0, h: 10 });
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
const state = { x: 0, h: 10 };
|
|
160
|
+
const tween = new Tween(state)
|
|
161
|
+
.to({ x: 150, h: 60 }, 300)
|
|
162
|
+
.easing(Easing.Cubic.Out)
|
|
163
|
+
.onUpdate(() => setPos({ ...state }))
|
|
164
|
+
.start();
|
|
165
|
+
const timer = setInterval(() => {
|
|
166
|
+
if (!tween.update()) clearInterval(timer); // update() is false when done
|
|
167
|
+
}, 16);
|
|
168
|
+
return () => clearInterval(timer);
|
|
169
|
+
}, []);
|
|
170
|
+
return (
|
|
171
|
+
<box
|
|
172
|
+
style={{
|
|
173
|
+
marginLeft: pos.x,
|
|
174
|
+
width: 40,
|
|
175
|
+
height: pos.h,
|
|
176
|
+
backgroundColor: '#00aa55',
|
|
177
|
+
}}
|
|
178
|
+
/>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
- Numbers only, including nested in objects and arrays. For colours use
|
|
184
|
+
d3-interpolate or popmotion.
|
|
185
|
+
- Nothing animates until something calls `update()` — the classic first
|
|
186
|
+
bug.
|
|
187
|
+
- `onUpdate` mutates the source object, so spread it into fresh state
|
|
188
|
+
(`setPos({ ...state })`) or React skips the re-render on identity.
|
|
189
|
+
|
|
190
|
+
## flubber {#flubber}
|
|
191
|
+
|
|
192
|
+
**Out of the box.** flubber@0.4.2.
|
|
193
|
+
|
|
194
|
+
Shape morphing for SVG paths: `interpolate(from, to)` returns
|
|
195
|
+
`t => d-string`, handling ring alignment, point insertion and topology
|
|
196
|
+
changes so intermediate frames do not self-intersect.
|
|
197
|
+
|
|
198
|
+
```jsx
|
|
199
|
+
import React, { useEffect, useState } from 'react';
|
|
200
|
+
import flubber from 'flubber'; // CJS: default import, no named ESM exports
|
|
201
|
+
const { interpolate } = flubber;
|
|
202
|
+
|
|
203
|
+
const morph = interpolate(
|
|
204
|
+
'M50,10 L90,90 L10,90 Z',
|
|
205
|
+
'M10,10 L90,10 L90,90 L10,90 Z',
|
|
206
|
+
);
|
|
207
|
+
const svgFor = (d) =>
|
|
208
|
+
`<svg viewBox="0 0 100 100"><path d="${d}" fill="#cc3355"/></svg>`;
|
|
209
|
+
|
|
210
|
+
function Morph() {
|
|
211
|
+
const [t, setT] = useState(0);
|
|
212
|
+
useEffect(() => {
|
|
213
|
+
const start = Date.now();
|
|
214
|
+
const timer = setInterval(() => {
|
|
215
|
+
const p = Math.min(1, (Date.now() - start) / 200);
|
|
216
|
+
setT(p);
|
|
217
|
+
if (p >= 1) clearInterval(timer);
|
|
218
|
+
}, 16);
|
|
219
|
+
return () => clearInterval(timer);
|
|
220
|
+
}, []);
|
|
221
|
+
return <svg source={svgFor(morph(t))} style={{ width: 100, height: 100 }} />;
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
- `import { interpolate } from 'flubber'` throws — it is a CJS package, so
|
|
226
|
+
use the default-import form above.
|
|
227
|
+
- Build the interpolator once (module scope or `useMemo`); construction is
|
|
228
|
+
the expensive part.
|
|
229
|
+
- Every frame is a full SVG parse and rasterize of the `source` string. Fine
|
|
230
|
+
for icons and small figures; for large scenes draw the interpolated points
|
|
231
|
+
in `<canvas onDraw>`.
|
|
232
|
+
- 0.4.2 since 2017 — finished rather than unmaintained.
|
|
233
|
+
|
|
234
|
+
## react-transition-group {#react-transition-group}
|
|
235
|
+
|
|
236
|
+
**Out of the box — with `nodeRef`.** react-transition-group@4.4.5.
|
|
237
|
+
|
|
238
|
+
A transition _state machine_, not an animator: `<Transition in={bool}
|
|
239
|
+
timeout={ms}>` walks its child through `entering`/`entered`/`exiting`/
|
|
240
|
+
`exited` on a timer and keeps it mounted during exit — the way to keep an
|
|
241
|
+
element mounted while it animates out, which react-x11 has no built-in for.
|
|
242
|
+
|
|
243
|
+
Always pass `nodeRef` — without it, v4 falls back to `ReactDOM.findDOMNode`,
|
|
244
|
+
which cannot handle a react-x11 node. Map states to styles yourself, with the
|
|
245
|
+
built-in `transition` style so the flip _tweens_ instead of snapping.
|
|
246
|
+
|
|
247
|
+
```jsx
|
|
248
|
+
import React, { useRef } from 'react';
|
|
249
|
+
import { Transition } from 'react-transition-group';
|
|
250
|
+
|
|
251
|
+
function Panel({ shown }) {
|
|
252
|
+
const nodeRef = useRef(null);
|
|
253
|
+
return (
|
|
254
|
+
<Transition
|
|
255
|
+
nodeRef={nodeRef}
|
|
256
|
+
in={shown}
|
|
257
|
+
timeout={150}
|
|
258
|
+
mountOnEnter
|
|
259
|
+
unmountOnExit
|
|
260
|
+
>
|
|
261
|
+
{(state) => (
|
|
262
|
+
<box
|
|
263
|
+
ref={nodeRef}
|
|
264
|
+
style={{
|
|
265
|
+
transition: 150, // let react-x11 tween the flip
|
|
266
|
+
width: 100,
|
|
267
|
+
height: 50,
|
|
268
|
+
backgroundColor: state === 'entered' ? '#ff0000' : '#0000ff',
|
|
269
|
+
}}
|
|
270
|
+
/>
|
|
271
|
+
)}
|
|
272
|
+
</Transition>
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
- **`react-dom` must be in `node_modules`.** `cjs/Transition.js` requires it
|
|
278
|
+
at module scope for the `findDOMNode` fallback, even when `nodeRef` means
|
|
279
|
+
it is never called. Install the peer; nothing renders through it.
|
|
280
|
+
- `CSSTransition` toggles class names, and there are none. Use plain
|
|
281
|
+
`Transition`.
|
|
282
|
+
- `timeout` and your transition duration are independent; keep them equal,
|
|
283
|
+
or the state flips mid-tween.
|
|
284
|
+
- `addEndListener` works; call the provided `done` yourself.
|
|
285
|
+
|
|
286
|
+
## `@react-spring/core` {#react-spring}
|
|
287
|
+
|
|
288
|
+
**Adapter, ~20 lines.** @react-spring/core@10.1.2,
|
|
289
|
+
@react-spring/animated@10.1.2.
|
|
290
|
+
|
|
291
|
+
The platform-independent half of react-spring: `useSpring`,
|
|
292
|
+
`useTransition`, `useTrail`, `SpringValue`, `Controller` — springs,
|
|
293
|
+
interruption with velocity carry-over, `api.start()`.
|
|
294
|
+
`@react-spring/animated` provides `createHost`, the documented seam for a
|
|
295
|
+
non-DOM target (how `@react-spring/native` and `/three` exist). Two public
|
|
296
|
+
seams do the work:
|
|
297
|
+
|
|
298
|
+
- `Globals.assign({ now, requestAnimationFrame })` points the frame loop at
|
|
299
|
+
a clock.
|
|
300
|
+
- `createHost(['box', …], { applyAnimatedValues })` produces `animated.box`.
|
|
301
|
+
Each frame, react-spring calls `applyAnimatedValues(instance, props)` with
|
|
302
|
+
the react-x11 node, and `node.applyProps(next, prev)` is the imperative
|
|
303
|
+
path the reconciler's `commitUpdate` uses. A component renders once while
|
|
304
|
+
hundreds of frames apply.
|
|
305
|
+
|
|
306
|
+
```jsx
|
|
307
|
+
// spring-x11.js — the adapter
|
|
308
|
+
import { Globals } from '@react-spring/core';
|
|
309
|
+
import { createHost } from '@react-spring/animated';
|
|
310
|
+
|
|
311
|
+
Globals.assign({
|
|
312
|
+
now: Date.now,
|
|
313
|
+
// real app: cb => ntkWindow.requestAnimationFrame(cb) (ref on <window>)
|
|
314
|
+
requestAnimationFrame: (cb) => setTimeout(() => cb(Date.now()), 16),
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const host = createHost(['box', 'text', 'window'], {
|
|
318
|
+
applyAnimatedValues(instance, props) {
|
|
319
|
+
if (!instance || typeof instance.applyProps !== 'function') return false;
|
|
320
|
+
const prev = instance.props;
|
|
321
|
+
const next = { ...prev, ...props };
|
|
322
|
+
// frames carry only animated leaves — keep the static style keys
|
|
323
|
+
if (props.style) next.style = { ...prev.style, ...props.style };
|
|
324
|
+
instance.applyProps(next, prev);
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
export const animated = host.animated;
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
```jsx
|
|
332
|
+
import React, { useEffect } from 'react';
|
|
333
|
+
import { useSpring } from '@react-spring/core';
|
|
334
|
+
import { animated } from './spring-x11.js';
|
|
335
|
+
|
|
336
|
+
function Bar() {
|
|
337
|
+
const [style, api] = useSpring(() => ({
|
|
338
|
+
from: { width: 20, backgroundColor: '#204080' },
|
|
339
|
+
}));
|
|
340
|
+
useEffect(() => {
|
|
341
|
+
api.start({ width: 180, backgroundColor: '#e04040' });
|
|
342
|
+
}, []);
|
|
343
|
+
return <animated.box style={{ ...style, height: 30 }} />;
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
- **Merge `style` one level deep in `applyAnimatedValues`** — the
|
|
348
|
+
number-one silent failure. Frames carry _animated leaves only_
|
|
349
|
+
(`props.getValue(true)`), so static keys (`height: 30` beside an animated
|
|
350
|
+
`width`) are absent, and a naive `{...prev, ...props}` merge wipes them:
|
|
351
|
+
the box paints nothing, with no error.
|
|
352
|
+
- Hex, `rgb()` and `rgba()` strings interpolate; named CSS colours are not
|
|
353
|
+
enabled by default in `@react-spring/shared`.
|
|
354
|
+
- Do not use `useScroll`, `useResize`, `useInView` or `useReducedMotion` from
|
|
355
|
+
core; they observe the DOM `window` and `matchMedia`.
|
|
356
|
+
`Globals.assign({ skipAnimation })` is the manual reduced-motion switch.
|
|
357
|
+
- Style arrays (`style={[a, b]}`) defeat the shallow merge. Flatten first —
|
|
358
|
+
react-x11 exports `flattenStyle`.
|
|
359
|
+
- A `setImmediate` clock runs frames as fast as the event loop turns — fine
|
|
360
|
+
for tests, wasteful for apps. Use the ntk window clock or a 16 ms timeout.
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# Data fetching
|
|
2
|
+
|
|
3
|
+
A react-x11 app is an ordinary Node process, so `fetch`, undici, `ky` and
|
|
4
|
+
`graphql-request` work unmodified. The _cache_ layer on top needs care:
|
|
5
|
+
stale-while-revalidate libraries decide whether they are "on a server" by
|
|
6
|
+
looking for a global `window`, and a react-x11 process has none. Get it
|
|
7
|
+
wrong and nothing throws; the app just stops refetching.
|
|
8
|
+
|
|
9
|
+
## TanStack Query {#tanstack-query}
|
|
10
|
+
|
|
11
|
+
**Adapter, ~12 lines.** @tanstack/react-query@5.101.4 (query-core 5.101.4).
|
|
12
|
+
|
|
13
|
+
`useQuery` deduplicates, caches, retries and background-refetches;
|
|
14
|
+
`useMutation` handles writes with invalidation. The environment coupling
|
|
15
|
+
lives in three replaceable managers — `focusManager`, `onlineManager`,
|
|
16
|
+
`environmentManager`. The trap is query-core's probe:
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
// @tanstack/query-core src/utils.ts
|
|
20
|
+
export const isServer = typeof window === 'undefined' || 'Deno' in globalThis;
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
With no `window`, query-core decides it is doing SSR:
|
|
24
|
+
|
|
25
|
+
- `retry` defaults to **0** instead of 3;
|
|
26
|
+
- stale timers are never scheduled and `refetchInterval` is disabled;
|
|
27
|
+
- `gcTime` defaults to **Infinity** instead of 5 minutes — the cache never
|
|
28
|
+
collects;
|
|
29
|
+
- the default `focusManager` setup wants
|
|
30
|
+
`window.addEventListener('visibilitychange')`, so focus refetching is
|
|
31
|
+
inert.
|
|
32
|
+
|
|
33
|
+
The fix is `environmentManager.setIsServer()`, re-exported from
|
|
34
|
+
`@tanstack/react-query`, paired with react-x11's window focus events:
|
|
35
|
+
|
|
36
|
+
```jsx
|
|
37
|
+
// query-x11.js — the whole adapter
|
|
38
|
+
import {
|
|
39
|
+
environmentManager,
|
|
40
|
+
focusManager,
|
|
41
|
+
onlineManager,
|
|
42
|
+
} from '@tanstack/react-query';
|
|
43
|
+
|
|
44
|
+
environmentManager.setIsServer(() => false); // we are a client, not SSR
|
|
45
|
+
focusManager.setEventListener(() => () => {}); // drop the visibilitychange probe
|
|
46
|
+
onlineManager.setEventListener(() => () => {}); // assume online (or probe for real)
|
|
47
|
+
|
|
48
|
+
export const windowFocusProps = {
|
|
49
|
+
onFocus: () => focusManager.setFocused(true),
|
|
50
|
+
onBlur: () => focusManager.setFocused(false),
|
|
51
|
+
};
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```jsx
|
|
55
|
+
import React from 'react';
|
|
56
|
+
import { createRoot } from 'react-x11';
|
|
57
|
+
import {
|
|
58
|
+
QueryClient,
|
|
59
|
+
QueryClientProvider,
|
|
60
|
+
useQuery,
|
|
61
|
+
} from '@tanstack/react-query';
|
|
62
|
+
import { windowFocusProps } from './query-x11.js';
|
|
63
|
+
|
|
64
|
+
const client = new QueryClient();
|
|
65
|
+
|
|
66
|
+
function Weather() {
|
|
67
|
+
const { data, status } = useQuery({
|
|
68
|
+
queryKey: ['weather'],
|
|
69
|
+
queryFn: () => fetch('https://wttr.in/?format=3').then((r) => r.text()),
|
|
70
|
+
});
|
|
71
|
+
return <text>{status === 'pending' ? 'loading…' : data}</text>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
(await createRoot()).render(
|
|
75
|
+
<window width={360} height={120} title="weather" {...windowFocusProps}>
|
|
76
|
+
<QueryClientProvider client={client}>
|
|
77
|
+
<Weather />
|
|
78
|
+
</QueryClientProvider>
|
|
79
|
+
</window>,
|
|
80
|
+
);
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Focusing the window now refetches stale queries, like refocusing a browser
|
|
84
|
+
tab.
|
|
85
|
+
|
|
86
|
+
- **Call `setIsServer` before creating the `QueryClient`** — the managers
|
|
87
|
+
are module singletons.
|
|
88
|
+
- Switching between two of your own windows fires blur-then-focus, which
|
|
89
|
+
refetches stale queries; raise `staleTime` if that is too eager.
|
|
90
|
+
- `onlineManager` defaults to online under Node (no `navigator.onLine`).
|
|
91
|
+
Wire it to NetworkManager over D-Bus for real offline handling.
|
|
92
|
+
- `@tanstack/react-query-devtools` is DOM-rendered and not usable.
|
|
93
|
+
|
|
94
|
+
## SWR {#swr}
|
|
95
|
+
|
|
96
|
+
**Adapter, 1 line — but read the dead end first.** swr@2.4.2.
|
|
97
|
+
|
|
98
|
+
`useSWR` fetches on mount, caches, dedupes and re-renders unmodified. Focus
|
|
99
|
+
and reconnect revalidation fail silently: SWR computes its environment once
|
|
100
|
+
at import time, with no override:
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
// swr/dist/_internal/config-context-*.mjs
|
|
104
|
+
const isWindowDefined = typeof window != 'undefined';
|
|
105
|
+
const IS_SERVER = !isWindowDefined || isLegacyDeno;
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Revalidation event sources are registered only `if (!IS_SERVER)` —
|
|
109
|
+
_including_ a custom `initFocus`/`initReconnect` passed through the
|
|
110
|
+
cache-provider config, so the provider approach from SWR's docs is dead code
|
|
111
|
+
under Node and swr registers zero listeners. There is no `setIsServer`.
|
|
112
|
+
|
|
113
|
+
Two ways out. The simple one: drive revalidation with `mutate`, whose key
|
|
114
|
+
filter `() => true` revalidates every cached key, from `<window onFocus>`:
|
|
115
|
+
|
|
116
|
+
```jsx
|
|
117
|
+
import React from 'react';
|
|
118
|
+
import { createRoot } from 'react-x11';
|
|
119
|
+
import useSWR, { mutate } from 'swr';
|
|
120
|
+
|
|
121
|
+
const fetcher = (url) => fetch(url).then((r) => r.text());
|
|
122
|
+
|
|
123
|
+
function Weather() {
|
|
124
|
+
const { data } = useSWR('https://wttr.in/?format=3', fetcher);
|
|
125
|
+
return <text>{data ?? 'loading…'}</text>;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
(await createRoot()).render(
|
|
129
|
+
// the adapter is this one prop: revalidate everything on window focus
|
|
130
|
+
<window
|
|
131
|
+
width={360}
|
|
132
|
+
height={120}
|
|
133
|
+
title="weather"
|
|
134
|
+
onFocus={() => mutate(() => true)}
|
|
135
|
+
>
|
|
136
|
+
<Weather />
|
|
137
|
+
</window>,
|
|
138
|
+
);
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The thorough one: make `IS_SERVER` false, which turns swr's own machinery
|
|
142
|
+
back on. It has to happen **before the first import of swr**:
|
|
143
|
+
|
|
144
|
+
```js
|
|
145
|
+
// swr-env.js — imported first, before anything that imports swr
|
|
146
|
+
const listeners = new Map();
|
|
147
|
+
const target = {
|
|
148
|
+
addEventListener: (t, fn) =>
|
|
149
|
+
listeners.set(t, [...(listeners.get(t) ?? []), fn]),
|
|
150
|
+
removeEventListener: (t, fn) =>
|
|
151
|
+
listeners.set(
|
|
152
|
+
t,
|
|
153
|
+
(listeners.get(t) ?? []).filter((f) => f !== fn),
|
|
154
|
+
),
|
|
155
|
+
};
|
|
156
|
+
globalThis.window ??= target;
|
|
157
|
+
globalThis.document ??= target;
|
|
158
|
+
|
|
159
|
+
/** Call from your <window onFocus> handler. */
|
|
160
|
+
export const fireFocus = () =>
|
|
161
|
+
(listeners.get('focus') ?? []).forEach((fn) => fn());
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
swr then registers `visibilitychange`, `focus`, `online` and `offline`
|
|
165
|
+
listeners and `revalidateOnFocus` behaves. Two gotchas:
|
|
166
|
+
|
|
167
|
+
- Fire only `'focus'`. An `'offline'` event sets swr's internal online flag
|
|
168
|
+
false and silently suppresses the deferred revalidation.
|
|
169
|
+
- `focusThrottleInterval` defaults to 5000 ms, so a focus within five
|
|
170
|
+
seconds of the last revalidation is dropped — which looks exactly like the
|
|
171
|
+
shim not working. Lower it while testing the wiring.
|
|
172
|
+
|
|
173
|
+
Other caveats:
|
|
174
|
+
|
|
175
|
+
- **`revalidateOnFocus` and `revalidateOnReconnect` are inert** unless you
|
|
176
|
+
define the globals. Nothing warns you.
|
|
177
|
+
- The global `mutate` targets the _default_ cache, so with a custom
|
|
178
|
+
`provider` via `<SWRConfig>` it silently misses; use
|
|
179
|
+
`useSWRConfig().mutate` from inside the tree.
|
|
180
|
+
- Suspense mode throws
|
|
181
|
+
`Fallback data is required when using Suspense in SSR.` because SWR
|
|
182
|
+
believes it is on a server. Provide `fallbackData`, or avoid
|
|
183
|
+
`suspense: true`.
|
|
184
|
+
- `preload()` is an SSR-gated no-op under Node.
|
|
185
|
+
- For focus revalidation with retries and cache GC done properly, prefer
|
|
186
|
+
TanStack Query — it ships a supported environment override.
|