immertation
v0.1.26
Published
<p align="center"> <img src="media/logo.png" alt="Immertation" width="33%" /> </p>
Downloads
622
Readme
State management library that tracks changes to your data using Immer patches and provides a powerful annotation system for operation tracking.
Operations are particularly useful for async operations and optimistic updates, where the model is being operated on but not yet committed to the final value. This allows you to track pending changes and distinguish between the current committed state and the draft state with pending operations.
Contents
Getting started
import { State, Op } from 'immertation';
type Model = {
name: string;
age: number;
};
const state = new State<Model>();
state.hydrate({ name: 'Imogen', age: 30 });
state.produce((draft) => {
draft.name = 'Phoebe';
draft.age = 31;
});
console.log(state.model.name); // 'Phoebe'
console.log(state.model.age); // 31
console.log(state.inspect.name.pending()); // false
console.log(state.inspect.age.pending()); // falseUsing annotations
Annotations allow you to track pending changes. This is especially useful for optimistic updates in async operations, where you want to immediately reflect changes in the UI while the operation is still in progress:
import { State, Op } from 'immertation';
// Annotate a value to mark it as pending
state.produce((draft) => void (draft.name = state.annotate(Op.Update, 'Phoebe')));
// The model retains the original value
console.log(state.model.name); // 'Imogen'
// But we can check if it has a pending operation
console.log(state.inspect.name.pending()); // true
// Later, commit the actual change
state.produce((draft) => void (draft.name = 'Phoebe'));
console.log(state.model.name); // 'Phoebe'
console.log(state.inspect.name.pending()); // falseAvailable operations
The Op enum provides operation types for annotations:
Op.Add- Mark a value as being addedOp.Remove- Mark a value as being removedOp.Update- Mark a value as being updatedOp.Replace- Mark a value as being replacedOp.Move- Mark a value as being movedOp.Sort- Mark a value as being sorted
// Adding a new item
state.produce((draft) => void draft.locations.push(state.annotate(Op.Add, { id: State.pk(), name: 'Horsham' })));
// Marking for removal (keeps item until actually removed)
state.produce((draft) => {
const index = draft.locations.findIndex((loc) => loc.id === id);
draft.locations[index] = state.annotate(Op.Remove, draft.locations[index]);
});
// Updating a property
state.produce((draft) => void (draft.user.name = state.annotate(Op.Update, 'Phoebe')));Inspecting state
The inspect property provides a proxy to check pending operations at any path:
// Check if a value has any pending operation
state.inspect.name.pending(); // boolean
// Check for a specific operation type
state.inspect.users[0].is(Op.Add); // true if being created
state.inspect.users[0].is(Op.Remove); // true if being deleted
// Get the draft value (annotated value or actual model value)
state.inspect.name.draft(); // returns annotated value if pending, otherwise model value
// Wait for a value to have no pending annotations
const value = await state.inspect.name.settled(); // resolves when annotations are pruned
// Works with nested paths
state.inspect.user.profile.email.pending();
// Works with array indices
state.inspect.locations[0].name.pending();Pruning annotations
Remove annotations by process after async operations complete:
const process = state.produce((draft) => void (draft.name = state.annotate(Op.Update, 'Phoebe')));
// After async operation completes
state.prune(process);Observing changes
Subscribe to model changes to react whenever mutations occur:
const unsubscribe = state.observe((model) => {
console.log('Model changed:', model);
});
// Later, stop listening
unsubscribe();Identity function
By default, Immertation tracks object identity using an internal κ property — you typically don't need to configure this. However, if you need custom identity tracking (e.g., using your own id fields), you can optionally pass a custom identity function to the State constructor:
const state = new State<Model>((snapshot) => {
if ('id' in snapshot) return snapshot.id;
if (Array.isArray(snapshot)) return snapshot.map((item) => item.id).join(',');
return JSON.stringify(snapshot);
});