Conflict-free data types often most useful when nested inside each of other, as this allows a parent data type to be edited at the same time as a child, without any conflicts occurring.
On the previous page, we built a collaborative input field—on this page we’ll
take the input and transform it into an editable list of fields using
LiveList
.
The first step is to set up your types and initial state. Open
liveblocks.config.ts
and modify your types so that we have a LiveList
of people, instead of a singular person.
/liveblocks.config.ts
// Person typetype Person = LiveObject<{ name: string; age: number;}>;
// Global typesdeclare global { interface Liveblocks { Storage: { people: LiveList<Person>; }; }}
Then, open App.tsx
, and modify the initialStorage
value to match your
types—we now need a LiveList
called people
containing a person.
/App.tsx
initialStorage={{ people: new LiveList([ new LiveObject({ name: "Marie", age: 30 }) ]),}}
Great, we’re ready to update our app!
Next, let’s modify the mutations—switch to Room.tsx
and look at
updateName
. We need to update this function to take a list index
, which we
can then use to get the current person
with LiveList.get
.
/Room.tsx
// Update name mutationconst updateName = useMutation( ({ storage }, newName: string, index: number) => { const person = storage.get("people").get(index); person.set("name", newName); }, []);
We can then create a mutation for adding a new person to the list. Within this
mutation we’re creating a new LiveObject
with a default value, before
adding it to the list with LiveList.push
.
/Room.tsx
// Add person mutationconst addPerson = useMutation(({ storage }) => { const newPerson = new LiveObject({ name: "Grace", age: 45 }); storage.get("people").push(newPerson);}, []);
You can find the different methods available to conflict-free data types under
Storage
in our docs.
We’ll call both of these mutations from the UI.
To render the list, we first need to update the selector function to return
people
instead of person
.
/Room.tsx
const people = useStorage((root) => root.people);
Because useStorage
converts your conflict-free data structures into
regular JavaScript structures, people
is an array
, meaning we can simply map
through it in React like any other.
/Room.tsx
return ( <div> {people.map((person, index) => ( <input key={index} type="text" value={person.name} onChange={(e) => updateName(e.target.value, index)} /> ))} </div>);
Make sure to pass the index to updateName
! After adding this, we can then
create a button that calls addPerson
.
/Room.tsx
return ( <div> {people.map((person, index) => ( <input key={index} type="text" value={person.name} onChange={(e) => updateName(e.target.value, index)} /> ))} <button onClick={addPerson}>Add</button> </div>);
We now have nested data structures working!
Try adding a “Reset” button by using LiveList.clear
inside a mutation!