0
# Patches System
1
2
Advanced patch tracking system for implementing undo/redo, debugging, and state synchronization features. The patches system allows you to track, serialize, and replay state changes with fine-grained detail.
3
4
## Capabilities
5
6
### enablePatches
7
8
Enables the patches plugin, which is required for all patch-related functionality.
9
10
```typescript { .api }
11
/**
12
* Enable the patches plugin for tracking changes as patches
13
* Must be called before using patch-related functions
14
*/
15
function enablePatches(): void;
16
```
17
18
This function must be called once before using `produceWithPatches`, `applyPatches`, or patch listeners with `produce`.
19
20
### applyPatches
21
22
Applies an array of Immer patches to the first argument, creating a new state with the patches applied.
23
24
```typescript { .api }
25
/**
26
* Apply an array of Immer patches to the first argument
27
* @param base - The object to apply patches to
28
* @param patches - Array of patch objects describing changes
29
* @returns New state with patches applied
30
*/
31
function applyPatches<T>(base: T, patches: readonly Patch[]): T;
32
```
33
34
**Usage Examples:**
35
36
```typescript
37
import { enablePatches, produceWithPatches, applyPatches } from "immer";
38
39
// Enable patches functionality
40
enablePatches();
41
42
const baseState = {
43
user: { name: "John", age: 30 },
44
todos: ["Learn Immer", "Use patches"]
45
};
46
47
// Generate patches
48
const [nextState, patches, inversePatches] = produceWithPatches(baseState, draft => {
49
draft.user.age = 31;
50
draft.todos.push("Master patches");
51
draft.user.email = "john@example.com";
52
});
53
54
console.log(patches);
55
// [
56
// { op: "replace", path: ["user", "age"], value: 31 },
57
// { op: "add", path: ["todos", 2], value: "Master patches" },
58
// { op: "add", path: ["user", "email"], value: "john@example.com" }
59
// ]
60
61
// Apply patches to recreate the same transformation
62
const recreatedState = applyPatches(baseState, patches);
63
console.log(JSON.stringify(recreatedState) === JSON.stringify(nextState)); // true
64
65
// Apply patches to different base state
66
const differentBase = {
67
user: { name: "Jane", age: 25 },
68
todos: ["Task 1"]
69
};
70
71
const transformedDifferent = applyPatches(differentBase, patches);
72
console.log(transformedDifferent);
73
// {
74
// user: { name: "Jane", age: 31, email: "john@example.com" },
75
// todos: ["Task 1", undefined, "Master patches"]
76
// }
77
78
// Apply inverse patches for undo functionality
79
const revertedState = applyPatches(nextState, inversePatches);
80
console.log(JSON.stringify(revertedState) === JSON.stringify(baseState)); // true
81
```
82
83
### Patch Listener Integration
84
85
Patch listeners can be used with `produce` and `finishDraft` to track changes without using `produceWithPatches`.
86
87
```typescript
88
import { produce, finishDraft, createDraft, enablePatches } from "immer";
89
90
enablePatches();
91
92
const state = { count: 0, items: [] as string[] };
93
94
// Using patch listener with produce
95
const updatedState = produce(
96
state,
97
draft => {
98
draft.count += 1;
99
draft.items.push("new item");
100
},
101
(patches, inversePatches) => {
102
console.log("Changes made:", patches);
103
console.log("To undo:", inversePatches);
104
}
105
);
106
107
// Using patch listener with finishDraft
108
const draft = createDraft(state);
109
draft.count = 10;
110
draft.items.push("manual item");
111
112
const result = finishDraft(draft, (patches, inversePatches) => {
113
// Log or store patches for later use
114
console.log("Manual draft changes:", patches);
115
});
116
```
117
118
## Patch Structure
119
120
```typescript { .api }
121
interface Patch {
122
/** The type of operation: add, remove, or replace */
123
op: "replace" | "remove" | "add";
124
/** Path to the changed value as array of keys/indices */
125
path: (string | number)[];
126
/** The new value (undefined for remove operations) */
127
value?: any;
128
}
129
130
type PatchListener = (patches: Patch[], inversePatches: Patch[]) => void;
131
```
132
133
### Patch Operation Types
134
135
**Add Operations:**
136
```typescript
137
// Adding array element
138
{ op: "add", path: ["items", 2], value: "new item" }
139
140
// Adding object property
141
{ op: "add", path: ["user", "email"], value: "user@example.com" }
142
143
// Adding nested property
144
{ op: "add", path: ["config", "settings", "theme"], value: "dark" }
145
```
146
147
**Replace Operations:**
148
```typescript
149
// Replacing primitive value
150
{ op: "replace", path: ["count"], value: 42 }
151
152
// Replacing array element
153
{ op: "replace", path: ["items", 0], value: "updated item" }
154
155
// Replacing nested object property
156
{ op: "replace", path: ["user", "profile", "name"], value: "New Name" }
157
```
158
159
**Remove Operations:**
160
```typescript
161
// Removing array element
162
{ op: "remove", path: ["items", 1] }
163
164
// Removing object property
165
{ op: "remove", path: ["user", "temporaryData"] }
166
167
// Removing nested property
168
{ op: "remove", path: ["config", "deprecated", "oldSetting"] }
169
```
170
171
## Advanced Patch Usage
172
173
### Undo/Redo System
174
175
```typescript
176
import { enablePatches, produce, applyPatches, Patch } from "immer";
177
178
enablePatches();
179
180
class UndoRedoStore<T> {
181
private history: T[] = [];
182
private patches: Patch[][] = [];
183
private inversePatches: Patch[][] = [];
184
private currentIndex = -1;
185
186
constructor(private initialState: T) {
187
this.history.push(initialState);
188
this.currentIndex = 0;
189
}
190
191
get current(): T {
192
return this.history[this.currentIndex];
193
}
194
195
update(updater: (draft: any) => void): T {
196
const [nextState, patches, inversePatches] = produceWithPatches(
197
this.current,
198
updater
199
);
200
201
// Remove any future history when making new changes
202
this.history = this.history.slice(0, this.currentIndex + 1);
203
this.patches = this.patches.slice(0, this.currentIndex);
204
this.inversePatches = this.inversePatches.slice(0, this.currentIndex);
205
206
// Add new state and patches
207
this.history.push(nextState);
208
this.patches.push(patches);
209
this.inversePatches.push(inversePatches);
210
this.currentIndex++;
211
212
return nextState;
213
}
214
215
undo(): T | null {
216
if (this.currentIndex <= 0) return null;
217
218
const patches = this.inversePatches[this.currentIndex - 1];
219
const prevState = applyPatches(this.current, patches);
220
this.currentIndex--;
221
222
return prevState;
223
}
224
225
redo(): T | null {
226
if (this.currentIndex >= this.history.length - 1) return null;
227
228
const patches = this.patches[this.currentIndex];
229
const nextState = applyPatches(this.current, patches);
230
this.currentIndex++;
231
232
return nextState;
233
}
234
235
canUndo(): boolean {
236
return this.currentIndex > 0;
237
}
238
239
canRedo(): boolean {
240
return this.currentIndex < this.history.length - 1;
241
}
242
}
243
244
// Usage
245
const store = new UndoRedoStore({ todos: [], count: 0 });
246
247
// Make changes
248
store.update(draft => {
249
draft.todos.push("Task 1");
250
draft.count = 1;
251
});
252
253
store.update(draft => {
254
draft.todos.push("Task 2");
255
draft.count = 2;
256
});
257
258
console.log(store.current); // { todos: ["Task 1", "Task 2"], count: 2 }
259
260
// Undo
261
store.undo();
262
console.log(store.current); // { todos: ["Task 1"], count: 1 }
263
264
// Redo
265
store.redo();
266
console.log(store.current); // { todos: ["Task 1", "Task 2"], count: 2 }
267
```
268
269
### State Synchronization
270
271
```typescript
272
import { enablePatches, produceWithPatches, applyPatches, Patch } from "immer";
273
274
enablePatches();
275
276
class StateSynchronizer<T> {
277
private listeners: Array<(patches: Patch[]) => void> = [];
278
279
constructor(private state: T) {}
280
281
// Subscribe to state changes
282
subscribe(listener: (patches: Patch[]) => void): () => void {
283
this.listeners.push(listener);
284
return () => {
285
const index = this.listeners.indexOf(listener);
286
if (index > -1) this.listeners.splice(index, 1);
287
};
288
}
289
290
// Update local state and notify listeners
291
update(updater: (draft: any) => void): T {
292
const [nextState, patches] = produceWithPatches(this.state, updater);
293
294
if (patches.length > 0) {
295
this.state = nextState;
296
this.listeners.forEach(listener => listener(patches));
297
}
298
299
return nextState;
300
}
301
302
// Apply patches from remote source
303
applyRemotePatches(patches: Patch[]): T {
304
this.state = applyPatches(this.state, patches);
305
return this.state;
306
}
307
308
getCurrentState(): T {
309
return this.state;
310
}
311
}
312
313
// Usage for real-time collaboration
314
const localSync = new StateSynchronizer({
315
document: { title: "Shared Doc", content: "" },
316
users: [] as string[]
317
});
318
319
// Send patches to remote when local changes occur
320
const unsubscribe = localSync.subscribe(patches => {
321
// In real app, send patches to server/other clients
322
console.log("Sending patches to remote:", patches);
323
// websocket.send(JSON.stringify({ type: 'patches', patches }));
324
});
325
326
// Apply changes locally
327
localSync.update(draft => {
328
draft.document.title = "Collaborative Document";
329
draft.users.push("Alice");
330
});
331
332
// Simulate receiving remote patches
333
const remotePatches: Patch[] = [
334
{ op: "replace", path: ["document", "content"], value: "Hello world!" },
335
{ op: "add", path: ["users", 1], value: "Bob" }
336
];
337
338
localSync.applyRemotePatches(remotePatches);
339
console.log(localSync.getCurrentState());
340
// {
341
// document: { title: "Collaborative Document", content: "Hello world!" },
342
// users: ["Alice", "Bob"]
343
// }
344
```
345
346
### Patch Serialization and Storage
347
348
```typescript
349
import { enablePatches, produceWithPatches, applyPatches, Patch } from "immer";
350
351
enablePatches();
352
353
class PatchLogger<T> {
354
private patchHistory: Array<{ timestamp: number; patches: Patch[] }> = [];
355
356
constructor(private initialState: T) {}
357
358
// Apply update and log patches
359
update(updater: (draft: any) => void): T {
360
const [nextState, patches] = produceWithPatches(this.initialState, updater);
361
362
if (patches.length > 0) {
363
// Store patches with timestamp
364
this.patchHistory.push({
365
timestamp: Date.now(),
366
patches: patches
367
});
368
369
this.initialState = nextState;
370
}
371
372
return nextState;
373
}
374
375
// Serialize patch history to JSON
376
exportHistory(): string {
377
return JSON.stringify({
378
initialState: this.initialState,
379
patches: this.patchHistory
380
});
381
}
382
383
// Restore from serialized patch history
384
static fromHistory<T>(serialized: string): { state: T; logger: PatchLogger<T> } {
385
const { initialState, patches } = JSON.parse(serialized);
386
387
// Replay all patches to reconstruct current state
388
let currentState = initialState;
389
for (const entry of patches) {
390
currentState = applyPatches(currentState, entry.patches);
391
}
392
393
const logger = new PatchLogger(currentState);
394
logger.patchHistory = patches;
395
396
return { state: currentState, logger };
397
}
398
399
// Get patches within time range
400
getPatchesBetween(startTime: number, endTime: number): Patch[] {
401
return this.patchHistory
402
.filter(entry => entry.timestamp >= startTime && entry.timestamp <= endTime)
403
.flatMap(entry => entry.patches);
404
}
405
}
406
407
// Usage
408
const logger = new PatchLogger({ data: [], version: 1 });
409
410
logger.update(draft => {
411
draft.data.push("item1");
412
});
413
414
logger.update(draft => {
415
draft.version = 2;
416
draft.data.push("item2");
417
});
418
419
// Export and restore
420
const serialized = logger.exportHistory();
421
const { state, logger: restoredLogger } = PatchLogger.fromHistory(serialized);
422
423
console.log(state); // { data: ["item1", "item2"], version: 2 }
424
```
425
426
The patches system provides powerful capabilities for change tracking, state synchronization, and building complex state management patterns with Immer.