Arrays
Legend-State is especially optimized for arrays since Legend has to handle huge lists of data. Here are a few tips to get the best performance out of arrays.
Arrays of objects require a unique id
To optimize rendering of arrays of objects, Legend-State requires a unique id
, _id
, or __id
field on each object.
Under the hood, Legend-State listens to elements by path within the object. Operations like splice
can change the index of an element which changes its path, so it uses the unique id
to handle elements being moved and keep observables as stable references to their underlying element. It also optimizes for repositioning items within arrays and only re-renders the changed elements.
Use the For
component
The For
component is optimized for rendering arrays of observable objects so that they are extracted into a separate tracking context and don't re-render the parent.
You can use it in two ways, providing an item
component or a function as a child.
An optimized
prop adds additional optimizations, but in an unusual way by re-using React nodes. See Optimized rendering for more details.
import { observable } from "@legendapp/state"
import { For } from "@legendapp/state/react"
const obs = observable({ arr: [{ id: 1, text: 'hi' }]})
function Row({ item }) {
return <div>{item.text}</div>
}
function List() {
// 1. Use the For component with an item prop
return <For each={list} item={Row} />
// 2. Use the For component with a render function as the child
return (
<For each={list}>
{item => (
<div>
{item.text}
</div>
)}
</div>
)
}
For doesn't re-render the parent
In this more complex example you can see that as elements are added to and update the array, the parent component does not re-render.
import { observable } from "@legendapp/state"
import { For } from "@legendapp/state/react"
function Item({ item }) {
useEffect(() => {
item.renders.set(r => r + 1);
})
return (
<div>
{item.text}
</div>
)
}
function TodosExample() {
const renderCount = ++useRef(0).current
const todos = useObservable([])
const onClickAdd = () =>
todos.push({
id: ++total,
text: 'Item ' + total,
renders: 1
})
const onClickUpdate = () => {
todos[todos.length - 1].text.set(text => text + '!')
}
return (
<div className="flex">
<button onClick={onClickAdd}>Add</button>
<button onClick={onClickUpdate}>Update</button>
<div>Renders: {renderCount}</div>
<For each={todos} item={Item}>
</div>
)
})
Don't access observables while mapping
The map
function automatically sets up a shallow listener, so it will only re-render when the array is changed and not when individual elements are changed. For best performance it's best to let the child component track each item observable.
Make sure that you don't access any observable properties while mapping, like accessing the id for the key, so use peek()
to prevent tracking.
import { observable } from "@legendapp/state"
import { For } from "@legendapp/state/react"
const obs = observable({ arr: [{ id: 1, text: 'hi' }]})
function Row({ item }) {
return <div>{item.text}</div>
}
function List() {
// Be sure to use peek() to make sure you don't track any observable fields here
return list.map(item =>
<Row key={item.id.peek()} item={item} />
)
}
Optimized rendering
The For
component has an optimized
prop which takes the optimizations even further. It prevents re-rendering the parent component when possible, so if the array length doesn't change it updates React elements in place instead of the whole list rendering. This massively reduces the rendering time when swapping elements, sorting an array, or replacing some individual elements. But because it reuses React nodes rather than replacing them as usual, it may have unexpected behavior with animations or if you are modifying the DOM externally.
This is how the fast "replace all rows" and "swap rows" speeds in the benchmark are achieved.
import { For } from "@legendapp/state/react"
...
function List() {
// Use the optimized prop
return <For each={list} item={Row} optimized />
}