CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-immer

Create immutable state by mutating the current one with structural sharing

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

patches-system.mddocs/

Patches System

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.

Capabilities

enablePatches

Enables the patches plugin, which is required for all patch-related functionality.

/**
 * Enable the patches plugin for tracking changes as patches
 * Must be called before using patch-related functions
 */
function enablePatches(): void;

This function must be called once before using produceWithPatches, applyPatches, or patch listeners with produce.

applyPatches

Applies an array of Immer patches to the first argument, creating a new state with the patches applied.

/**
 * Apply an array of Immer patches to the first argument
 * @param base - The object to apply patches to
 * @param patches - Array of patch objects describing changes
 * @returns New state with patches applied
 */
function applyPatches<T>(base: T, patches: readonly Patch[]): T;

Usage Examples:

import { enablePatches, produceWithPatches, applyPatches } from "immer";

// Enable patches functionality
enablePatches();

const baseState = {
  user: { name: "John", age: 30 },
  todos: ["Learn Immer", "Use patches"]
};

// Generate patches
const [nextState, patches, inversePatches] = produceWithPatches(baseState, draft => {
  draft.user.age = 31;
  draft.todos.push("Master patches");
  draft.user.email = "john@example.com";
});

console.log(patches);
// [
//   { op: "replace", path: ["user", "age"], value: 31 },
//   { op: "add", path: ["todos", 2], value: "Master patches" },
//   { op: "add", path: ["user", "email"], value: "john@example.com" }
// ]

// Apply patches to recreate the same transformation
const recreatedState = applyPatches(baseState, patches);
console.log(JSON.stringify(recreatedState) === JSON.stringify(nextState)); // true

// Apply patches to different base state
const differentBase = {
  user: { name: "Jane", age: 25 },
  todos: ["Task 1"]
};

const transformedDifferent = applyPatches(differentBase, patches);
console.log(transformedDifferent);
// {
//   user: { name: "Jane", age: 31, email: "john@example.com" },
//   todos: ["Task 1", undefined, "Master patches"]
// }

// Apply inverse patches for undo functionality
const revertedState = applyPatches(nextState, inversePatches);
console.log(JSON.stringify(revertedState) === JSON.stringify(baseState)); // true

Patch Listener Integration

Patch listeners can be used with produce and finishDraft to track changes without using produceWithPatches.

import { produce, finishDraft, createDraft, enablePatches } from "immer";

enablePatches();

const state = { count: 0, items: [] as string[] };

// Using patch listener with produce
const updatedState = produce(
  state,
  draft => {
    draft.count += 1;
    draft.items.push("new item");
  },
  (patches, inversePatches) => {
    console.log("Changes made:", patches);
    console.log("To undo:", inversePatches);
  }
);

// Using patch listener with finishDraft
const draft = createDraft(state);
draft.count = 10;
draft.items.push("manual item");

const result = finishDraft(draft, (patches, inversePatches) => {
  // Log or store patches for later use
  console.log("Manual draft changes:", patches);
});

Patch Structure

interface Patch {
  /** The type of operation: add, remove, or replace */
  op: "replace" | "remove" | "add";
  /** Path to the changed value as array of keys/indices */
  path: (string | number)[];
  /** The new value (undefined for remove operations) */
  value?: any;
}

type PatchListener = (patches: Patch[], inversePatches: Patch[]) => void;

Patch Operation Types

Add Operations:

// Adding array element
{ op: "add", path: ["items", 2], value: "new item" }

// Adding object property
{ op: "add", path: ["user", "email"], value: "user@example.com" }

// Adding nested property
{ op: "add", path: ["config", "settings", "theme"], value: "dark" }

Replace Operations:

// Replacing primitive value
{ op: "replace", path: ["count"], value: 42 }

// Replacing array element
{ op: "replace", path: ["items", 0], value: "updated item" }

// Replacing nested object property
{ op: "replace", path: ["user", "profile", "name"], value: "New Name" }

Remove Operations:

// Removing array element
{ op: "remove", path: ["items", 1] }

// Removing object property
{ op: "remove", path: ["user", "temporaryData"] }

// Removing nested property
{ op: "remove", path: ["config", "deprecated", "oldSetting"] }

Advanced Patch Usage

Undo/Redo System

import { enablePatches, produce, applyPatches, Patch } from "immer";

enablePatches();

class UndoRedoStore<T> {
  private history: T[] = [];
  private patches: Patch[][] = [];
  private inversePatches: Patch[][] = [];
  private currentIndex = -1;

  constructor(private initialState: T) {
    this.history.push(initialState);
    this.currentIndex = 0;
  }

  get current(): T {
    return this.history[this.currentIndex];
  }

  update(updater: (draft: any) => void): T {
    const [nextState, patches, inversePatches] = produceWithPatches(
      this.current,
      updater
    );

    // Remove any future history when making new changes
    this.history = this.history.slice(0, this.currentIndex + 1);
    this.patches = this.patches.slice(0, this.currentIndex);
    this.inversePatches = this.inversePatches.slice(0, this.currentIndex);

    // Add new state and patches
    this.history.push(nextState);
    this.patches.push(patches);
    this.inversePatches.push(inversePatches);
    this.currentIndex++;

    return nextState;
  }

  undo(): T | null {
    if (this.currentIndex <= 0) return null;

    const patches = this.inversePatches[this.currentIndex - 1];
    const prevState = applyPatches(this.current, patches);
    this.currentIndex--;

    return prevState;
  }

  redo(): T | null {
    if (this.currentIndex >= this.history.length - 1) return null;

    const patches = this.patches[this.currentIndex];
    const nextState = applyPatches(this.current, patches);
    this.currentIndex++;

    return nextState;
  }

  canUndo(): boolean {
    return this.currentIndex > 0;
  }

  canRedo(): boolean {
    return this.currentIndex < this.history.length - 1;
  }
}

// Usage
const store = new UndoRedoStore({ todos: [], count: 0 });

// Make changes
store.update(draft => {
  draft.todos.push("Task 1");
  draft.count = 1;
});

store.update(draft => {
  draft.todos.push("Task 2");
  draft.count = 2;
});

console.log(store.current); // { todos: ["Task 1", "Task 2"], count: 2 }

// Undo
store.undo();
console.log(store.current); // { todos: ["Task 1"], count: 1 }

// Redo
store.redo();
console.log(store.current); // { todos: ["Task 1", "Task 2"], count: 2 }

State Synchronization

import { enablePatches, produceWithPatches, applyPatches, Patch } from "immer";

enablePatches();

class StateSynchronizer<T> {
  private listeners: Array<(patches: Patch[]) => void> = [];

  constructor(private state: T) {}

  // Subscribe to state changes
  subscribe(listener: (patches: Patch[]) => void): () => void {
    this.listeners.push(listener);
    return () => {
      const index = this.listeners.indexOf(listener);
      if (index > -1) this.listeners.splice(index, 1);
    };
  }

  // Update local state and notify listeners
  update(updater: (draft: any) => void): T {
    const [nextState, patches] = produceWithPatches(this.state, updater);
    
    if (patches.length > 0) {
      this.state = nextState;
      this.listeners.forEach(listener => listener(patches));
    }
    
    return nextState;
  }

  // Apply patches from remote source
  applyRemotePatches(patches: Patch[]): T {
    this.state = applyPatches(this.state, patches);
    return this.state;
  }

  getCurrentState(): T {
    return this.state;
  }
}

// Usage for real-time collaboration
const localSync = new StateSynchronizer({ 
  document: { title: "Shared Doc", content: "" },
  users: [] as string[]
});

// Send patches to remote when local changes occur
const unsubscribe = localSync.subscribe(patches => {
  // In real app, send patches to server/other clients
  console.log("Sending patches to remote:", patches);
  // websocket.send(JSON.stringify({ type: 'patches', patches }));
});

// Apply changes locally
localSync.update(draft => {
  draft.document.title = "Collaborative Document";
  draft.users.push("Alice");
});

// Simulate receiving remote patches
const remotePatches: Patch[] = [
  { op: "replace", path: ["document", "content"], value: "Hello world!" },
  { op: "add", path: ["users", 1], value: "Bob" }
];

localSync.applyRemotePatches(remotePatches);
console.log(localSync.getCurrentState());
// {
//   document: { title: "Collaborative Document", content: "Hello world!" },
//   users: ["Alice", "Bob"]
// }

Patch Serialization and Storage

import { enablePatches, produceWithPatches, applyPatches, Patch } from "immer";

enablePatches();

class PatchLogger<T> {
  private patchHistory: Array<{ timestamp: number; patches: Patch[] }> = [];

  constructor(private initialState: T) {}

  // Apply update and log patches
  update(updater: (draft: any) => void): T {
    const [nextState, patches] = produceWithPatches(this.initialState, updater);
    
    if (patches.length > 0) {
      // Store patches with timestamp
      this.patchHistory.push({
        timestamp: Date.now(),
        patches: patches
      });
      
      this.initialState = nextState;
    }
    
    return nextState;
  }

  // Serialize patch history to JSON
  exportHistory(): string {
    return JSON.stringify({
      initialState: this.initialState,
      patches: this.patchHistory
    });
  }

  // Restore from serialized patch history
  static fromHistory<T>(serialized: string): { state: T; logger: PatchLogger<T> } {
    const { initialState, patches } = JSON.parse(serialized);
    
    // Replay all patches to reconstruct current state
    let currentState = initialState;
    for (const entry of patches) {
      currentState = applyPatches(currentState, entry.patches);
    }
    
    const logger = new PatchLogger(currentState);
    logger.patchHistory = patches;
    
    return { state: currentState, logger };
  }

  // Get patches within time range
  getPatchesBetween(startTime: number, endTime: number): Patch[] {
    return this.patchHistory
      .filter(entry => entry.timestamp >= startTime && entry.timestamp <= endTime)
      .flatMap(entry => entry.patches);
  }
}

// Usage
const logger = new PatchLogger({ data: [], version: 1 });

logger.update(draft => {
  draft.data.push("item1");
});

logger.update(draft => {
  draft.version = 2;
  draft.data.push("item2");
});

// Export and restore
const serialized = logger.exportHistory();
const { state, logger: restoredLogger } = PatchLogger.fromHistory(serialized);

console.log(state); // { data: ["item1", "item2"], version: 2 }

The patches system provides powerful capabilities for change tracking, state synchronization, and building complex state management patterns with Immer.

docs

configuration-utilities.md

core-production.md

draft-management.md

index.md

patches-system.md

tile.json