From input components to real-time safe patches
Learn more about real-time safe patches, with recommendations to safely sync changes between front and back ends.
Sanity Studio is a real-time application. Therefore, signaling changes from the editor to the back end uses a different strategy than the typical one.
Traditionally, editing content online is based on the following model:
- The editor interface reads the content to modify from the database.
- Users introduce changes to their local copy of the document.
- After completing their edits, users save their work; all the content of the document is sent back to the server and written to the database.
This is akin to downloading an MS Word document to a local computer, editing it, and sending it back to the server when done.
It’s an approach that works well if there’s only one user working on a single document at the time. If you’re collaborating with someone else, this model breaks down. You risk either overwriting someone else’s work or going through a tedious change conflict resolution process before you can save the document without losing any changes.
Sanity applies a different collaboration model:
- The editor interface (Sanity Studio) loads the content to modify from the database (Content Lake.)
- While users edit local versions of the document, the editor emits fine-grained, computer-readable descriptions of what exactly changed. We call these descriptions mutations.
- The editor collects these mutations and sends them to the server, which then applies them directly to the stored document.
- Then, the server distributes the mutations to any other collaborators who are working on the same document at the same time.
- Finally, the editor applies the mutations to each concurrent user’s local version so that everything is in sync.
Because of this model, input components in Sanity are designed to work with granular mutations called patches.
Following the traditional model, an input component for an object may look like this:
function MyObjectInput(props) {
const {fields, value, onChange} = props
return (
<>
{fields.map((field) => (
<div>
<label>
{field.title}
<input
type="text"
value={value[field.name]}
onChange={(event) => {
onChange({...value, [field.name]: event.currentTarget.value})
}}
/>
</label>
</div>
))}
</>
)
}
This model is easy to work with when you keep the input value in a state variable: all you need to do is call setState
with the emitted value, and feed the state variable back to <MyObjectInput>
.
However, this model doesn’t work as well in a real-time scenario where you don’t want to send and receive full values, but rather granular change descriptions (mutations).
In a real-time environment, the following works better:
function MyObjectInput(props) {
const {fields, value, onChange} = props
return (
<>
{fields.map((field) => (
<div>
<label>
{field.title}
<input
type="text"
value={value[field.name]}
onChange={(event) => {
onChange({set: {[field.name]: event.currentTarget.value}})
}}
/>
</label>
</div>
))}
</>
)
}
As a bonus, to set a new object input value, you don’t need to consider the current one.
The PatchEvent
class offers a set of utilities for composing real-time safe patches when developing object and array inputs. Instead of manually constructing field paths and patches, you can import a set of patch creators from the Sanity package.
The PatchEvent
class has several static methods to declare a granular operation:
path
can only accept an array of path segments.
set
set(value: any, path?: Path)
: sets the value at the specified path. It overwrites any existing value.
unset
unset(path: Path?)
: unsets any value at the specified path.
setIfMissing
setIfMissing(value: any, path?: Path)
: performs a setIfMissing
patch on the specified path.
insert
insert(items: any[], position: "before" | "after", path?: Path)
: performs an insert
patch, inserting the items
provided before or after the node at the specified path.
diffMatchPatch
diffMatchPatch(value: string, path?: Path)
: performs a diffMatchPatch
on the string at the specified path.
inc
inc(amount, path?: Path)
: performs an increment
operation on the number value at the specified path.
dec
dec(amount, path?: Path)
: performs a decrement
operation on the number value at the specified path.
The change event you emit from the input component needs to consider what users want to achieve when they make a change.
For example:
- Do you want the change to only affect what a user sees on their screen, regardless of the corresponding value in the database (which might not be the same as what is displayed to the user)?
Or do you want to modify the most recent value stored in the database, regardless of what is displayed to the user on the screen? - When a user changes the value of a number, do they want to increase or decrease the original value? Or do they want to set it to a new arbitrary value?
The differences in the outcomes can be subtle. As a rule of thumb, when creating patches, it’s preferable to avoid reading input values locally. This is possible only when using the insert
, inc
and dec
patches.
Create patches that are as fine-grained as possible. When creating a custom array or object input, you can optionally call onChange
with a patch that sets the whole array or object value.
When creating patches that target array elements, avoid targeting the elements with their array index reference. Array indices are unreliable because users may add, remove, and change the order of the items in the array over time. For example: if you have an array with two items A
and B
with index 0
and 1
, respectively, their reference array index changes as soon as you or other users modify the order of the elements in the array.
Sanity supports diff-match-patch, which offers a robust way to describe a change in plain text. Usually, you don’t need to create diff match patches; Sanity Studio does it for you under the hood.
Gotcha: if you implement emitting diff match patches from your custom input, you miss out on built-in optimizations. Therefore, it’s preferable to avoid creating diff match patches from custom input components.