Skip to content

Commit 503d866

Browse files
authored
feat: Add subscribe-ops documentation, include a Node.js 25+ compatibility fix for Next.js. (#1201)
1 parent 38f8d1b commit 503d866

4 files changed

Lines changed: 114 additions & 1 deletion

File tree

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
---
2+
title: 'subscribe-ops'
3+
section: 'API'
4+
subSection: 'Advanced'
5+
description: 'Fine-grained mutation tracking with operational transformations (Ops)'
6+
---
7+
8+
# Subscribe Ops
9+
10+
By default, Valtio's `subscribe` notify you that _something_ has changed in the state proxy. However, you can opt-in to receiving **Ops** (Operations), which are detailed descriptions of exactly what was modified.
11+
12+
## What are Ops?
13+
14+
Ops are granular mutation records. When a proxy is updated, Valtio can generate a description of the change as a tuple.
15+
16+
### Op Types
17+
18+
- **`set`**: `[op: 'set', path: Path, value: unknown, prevValue: unknown]`
19+
- Triggered when a property is assigned a new value.
20+
- **`delete`**: `[op: 'delete', path: Path, prevValue: unknown]`
21+
- Triggered when a property is deleted.
22+
- **`resolve`**: `[op: 'resolve', path: Path, value: unknown]`
23+
- Triggered when a promise in the state is fulfilled.
24+
- **`reject`**: `[op: 'reject', path: Path, error: unknown]`
25+
- Triggered when a promise in the state is rejected.
26+
27+
The `Path` is an array of strings or symbols representing the nested location of the property (e.g., `['user', 'profile', 'name']`).
28+
29+
## How to use Ops
30+
31+
The "ops" feature is an **opt-in** feature because it introduces a small performance overhead for tracking and allocating these operation objects.
32+
33+
### 1. Enabling Ops
34+
35+
You must explicitly enable op-tracking using the unstable API:
36+
37+
```javascript
38+
import { unstable_enableOp } from 'valtio'
39+
40+
// Enable globally
41+
unstable_enableOp(true)
42+
```
43+
44+
### 2. Receiving Ops in `subscribe`
45+
46+
Once enabled, the `subscribe` callback receives an array of these operations as its first argument.
47+
48+
```javascript
49+
import { proxy, subscribe, unstable_enableOp } from 'valtio'
50+
51+
unstable_enableOp(true)
52+
53+
const state = proxy({ count: 0, text: 'hello' })
54+
55+
subscribe(state, (ops) => {
56+
ops.forEach((op) => {
57+
const [action, path, value, prevValue] = op
58+
console.log(`Action: ${action} at ${path.join('.')}`)
59+
console.log(`New value:`, value)
60+
console.log(`Previous value:`, prevValue)
61+
})
62+
})
63+
64+
state.count++
65+
// Output:
66+
// Action: set at count
67+
// New value: 1
68+
// Previous value: 0
69+
```
70+
71+
> **Note**: If `unstable_enableOp(true)` is not called, the `ops` argument will be an empty array or `undefined`.
72+
73+
## Use Cases
74+
75+
While standard `subscribe` is sufficient for most React UI updates, Ops are useful for specific advanced scenarios:
76+
77+
1. **Network Synchronization**: Instead of sending the entire state over the wire, you can send only the `ops` (patches). This significantly reduces bandwidth consumption in distributed applications.
78+
2. **Undo/Redo History**: Use the `prevValue` provided in `set` and `delete` ops to easily revert state changes.
79+
3. **Audit Logs & Debugging**: Track a sequence of user-driven mutations for analytics or time-travel debugging.
80+
4. **Devtools Integration**: Powering custom development tools that need to visualize state transitions.
81+
82+
## Performance Considerations
83+
84+
Enabling Ops has a small overhead cost. For every mutation, Valtio must:
85+
86+
- Detect the change type.
87+
- Construct the `Path` array.
88+
- Allocate the `Op` tuple.
89+
90+
In high-frequency update scenarios (e.g., animations, canvas interactions moving hundreds of objects per frame), this can lead to:
91+
92+
- Increased garbage collection (GC) pressure due to object allocations.
93+
- A measurable drop in frame rates (FPS).
94+
95+
**Recommendation**: Only enable `unstable_enableOp` if your application actually consumes the granular `ops` data.

docs/api/advanced/subscribe.mdx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,9 @@ state.arr.push('world')
3838
## Codesandbox demo in VanillaJS
3939

4040
https://codesandbox.io/s/valtio-photo-booth-demo-forked-xp8hs?file=/src/main.js
41+
42+
## Advanced: Listening to "Ops"
43+
44+
The `subscribe` callback can also receive **Ops** (Operations), which are detailed records of exactly what changed (e.g., which property was set or deleted). This is useful for advanced scenarios like synchronization or undo/redo.
45+
46+
Because tracking these operations has a small performance cost, they are disabled by default. For more details on how to enable and use them, check out [Subscribe Ops](./subscribe-ops).

website/lib/mdx.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,12 @@ export function getDocsNav(): NavigationTree {
253253
],
254254
API: {
255255
Basic: [pages['proxy'], pages['useSnapshot']],
256-
Advanced: [pages['ref'], pages['subscribe'], pages['snapshot']],
256+
Advanced: [
257+
pages['ref'],
258+
pages['subscribe'],
259+
pages['subscribe-ops'],
260+
pages['snapshot'],
261+
],
257262
Utils: [
258263
pages['subscribeKey'],
259264
pages['watch'],

website/next.config.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
// Prevent crash on Node.js 25+ where SlowBuffer was removed.
2+
// This error originates from a compiled version of jsonwebtoken bundled inside Next.js.
3+
const buffer = require('buffer')
4+
if (!buffer.SlowBuffer) {
5+
buffer.SlowBuffer = buffer.Buffer
6+
}
7+
18
/** @type {import('next').NextConfig} */
29
module.exports = {
310
reactStrictMode: true,

0 commit comments

Comments
 (0)