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 { 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 { 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>
    )
})
Renders: 1
{text} {renders}

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 { 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 />
}