Skip to main content

Patches

Overview

As seen in the previous snapshots section, any change made to a tree node will generate a new snapshot, but this is only one of the two possible ways mobx-keystone offers to detect changes. The second way is "patches".

Basically, every change will generate two kinds of patches, patches from the previous to the new value (simply known as "patches") and patches from the new to the previous value (known as "inverse patches"). A patch object has this structure:

export interface Patch {
readonly op: "replace" | "remove" | "add"
readonly path: Path
readonly value?: any // value is not available for remove operations
}

The difference with JSON patches is that the path is an array of strings / numbers rather than a simple string. This makes it faster to parse and use since there is no parsing / splitting involved.

Getting patches

onPatches

onPatches(target: object, listener: OnPatchesListener): OnPatchesDisposer

onPatches allows you to access the patches generated for a tree node and all its children like this:

const disposer = onPatches(todo, (patches, inversePatches) => {
// ...
})

Note that the listener callback is called immediately after an observable value has changed and before the outermost action has completed. This behavior differs from, e.g., onSnapshot or a MobX reaction.

patchRecorder

patchRecorder(target: object, opts?: PatchRecorderOptions): PatchRecorder

patchRecorder is an abstraction over onPatches that can be used like this:

const recorder = patchRecorder(todo, options)

Where the allowed options are:

/**
* Patch recorder options.
*/
export interface PatchRecorderOptions {
/**
* If the patch recorder is initially recording when created.
*/
recording?: boolean

/**
* An optional callback filter to select wich patches to record/skip.
* It will be executed before the event is added to the events list.
*
* @param patches Patches about to be recorded.
* @param inversePatches Inverse patches about to be recorded.
* @returns `true` to record the patch, `false` to skip it.
*/
filter?(patches: Patch[], inversePatches: Patch[]): boolean

/**
* An optional callback run once a patch is recorded.
* It will be executed after the event is added to the events list.
*
* @param patches Patches just recorded.
* @param inversePatches Inverse patches just recorded.
*/
onPatches?: OnPatchesListener
}

It will return an interface implementation that allows you to handle patch recording via the following properties:

interface PatchRecorder {
/**
* Gets/sets if the patch recorder is currently recording.
*/
recording: boolean

/**
* Observable array of patching events.
*/
readonly events: PatchRecorderEvent[]

/**
* Dispose of the patch recorder.
*/
dispose(): void
}

The PatchRecorderEvent definition is:

interface PatchRecorderEvent {
/**
* Target object.
*/
readonly target: object
/**
* Recorded patches.
*/
readonly patches: Patch[]
/**
* Recorded inverse patches.
*/
readonly inversePatches: Patch[]
}

Applying patches

applyPatches

applyPatches(obj: object, patches: Patch[] | Patch[][], reverse?: boolean): void

It is also possible to apply patches doing this:

applyPatches(todo, patches)

as well as in reverse order (usually used for inverse patches):

applyPatches(todo, patches, true)

Conversion to JSON patches / paths

The only difference with the JSON Patch specification is that paths generated by this library are arrays instead of strings. For compatibility reasons the following conversion functions are provided:

pathToJsonPointer

pathToJsonPointer(path: Path): string

Converts a path into a JSON pointer.

jsonPointerToPath

jsonPointerToPath(jsonPointer: string): Path

Converts a JSON pointer into a path.

patchToJsonPatch

patchToJsonPatch(patch: Patch): JsonPatch

Converts a patch into a JSON patch.

jsonPatchToPatch

jsonPatchToPatch(jsonPatch: JsonPatch): Patch

Converts a JSON patch into a patch.