Legend-State Documentation

repository·main·Indexed 26 days ago

https://github.com/legendapp/legend-state

A high-performance state management and synchronization library designed for minimal boilerplate and maximum speed, specifically optimized for React applications. It provides fine-grained reactivity via the <Memo> component, observable-based state creation, and built-in support for persistence and synchronization through plugins like Keel and Supabase. Key features include computed observables, batching, undo/redo functionality, and optimized rendering for object records.

Tokens
8.2K
Snippets
9
Records
56
Agent score
84%

What's inside Legend-State

  1. Use optimized `<For>` with object records

    main

    When using the <For> component in React, you can pass the optimized prop to improve performance. If the each prop is an observable object record (e.g., Record<string, T>), setting optimized ensures that the component correctly reacts to key membership changes, such as inserting new keys or deleting existing ones.

    Previously, optimized object records might not have re-rendered when keys were added or removed; with the current implementation, <For each={record$} optimized> will correctly update when record keys are inserted, deleted, or replaced by a same-size key set, while still suppressing unnecessary renders for nested field updates.

  2. Avoid React 'Cannot update a component' errors with useSelector

    main

    Legend-State handles potential React errors by automatically deferring useSelector notifications when they are triggered during a Legend-managed render scope.

    When an observable update occurs during the execution of Legend-owned hooks (like useSelector, useObservable, or useObserve), the library queues the notification as a microtask. This prevents the "Cannot update a component while rendering another component" React warning.

    Key Behaviors:

    • Synchronous Updates: Updates to observables made outside of Legend's managed render scopes remain synchronous, preserving standard useSyncExternalStore behavior.
    • Coalescing: If multiple updates occur within the same render scope, notifications are coalesced into a single microtask to optimize performance.
    • Safety: If a selector unmounts before the microtask executes, the queued notification is ignored to prevent memory leaks or state errors.
  3. Run Legend-State benchmarks

    main
    To run the performance benchmarks for Legend-State, you must first install hyperfine. The benchmarks are organized into directories by optimization category: architecture-optimizations, micro-optimizations, and array-optimizations. Each category contains individual JavaScript files representing different approaches and a bench.sh script. To execute a benchmark, navigate to the desired optimization directory and run the bench.sh script.
  4. Handle derived lists from Record-backed Observables

    main

    When using Object.values() on an Observable<Record<string, T>> to create a derived list, Legend-State ensures that deleting items from the source record correctly updates the derived array.

    Previously, deleting a non-tail item in a record could leave stale activated array index children. Now, when a record-backed computed array shrinks, trailing child nodes are removed, and deleted children are fully deactivated to ensure that shifted indexes and out-of-range reads correctly reflect the current state of the array.

  5. Sync and persist state with plugins

    main

    Legend-State supports powerful sync and persistence via plugins (e.g., Keel, Supabase, TanStack Query). This enables local-first behavior where changes are applied optimistically and synced to a server in the background.

    const state$ = observable({
        users: syncedKeel({
            list: queries.getUsers,
            create: mutations.createUsers,
            update: mutations.updateUsers,
            delete: mutations.deleteUsers,
            persist: { name: 'users', retrySync: true },
            debounceSet: 500,
            retry: {
                infinite: true,
            },
            changesSince: 'last-sync',
        }),
        // direct link to my user within the users observable
        me: () => state$.users['myuid']
    })
    
    // Accessing the data triggers the sync process
    observe(() => {
        const name = state$.me.name.get()
    })
    
    // Setting a value updates the local state and syncs to the server
    state$.me.name.set('Annyong')
  6. Understand CRUD synchronization behavior in Legend-State

    main

    The CRUD synchronization plugin handles remote data operations through create, update, and delete functions.

    Key Behaviors:

    • Creates: When creating an item, the plugin tracks pending creations. If a creation fails, the plugin can attempt to refresh the value from the local state to retry the operation.
    • Updates: Updates are performed after ensuring any pending creation for the same itemKey has completed. It uses a transform.save function to convert local data to the remote format (TRemote) before sending.
    • Deletes:
      • If a deleteFn is provided, it is called with the itemKey and the previous value.
      • If fieldDeleted and updateFn are provided instead of a deleteFn, the plugin performs a 'soft delete' by updating the item with { [fieldId]: itemKey, [fieldDeleted]: true }.
    • Array Synchronization: When synchronizing arrays, the plugin identifies items by a fieldId. If an item is saved that no longer exists in the local array, a warning is issued: [legend-state] Item saved that does not exist in array.
  7. Avoid data loss when replacing root arrays immutably

    main

    When using syncedCrud with arrays, performing an immutable root replacement (e.g., using .set() on the entire array) can cause unsynced records to be lost if a previous create operation failed. This happens because multiple logical operations might share the same root path (e.g., pending['']), and a successful operation could prematurely clear the pending state for the entire path.

    Workaround: Instead of replacing the entire array immutably, mutate the array using .push(). This avoids the root-path collision by targeting specific indices rather than the root path.

  8. Clear an observable without triggering listeners using setSilently

    main

    If you need to reset or clear an observable (e.g., setting it to undefined) inside an onChange listener or any other logic where you want to avoid triggering a second change notification/cleanup loop, use the setSilently function.

    While calling .delete() on an observable is intentionally observable (it triggers listeners with undefined), setSilently acts as an escape hatch to update the value without notifying any listeners.

    import { setSilently } from '@legendapp/state';
    
    // Clears the observable without triggering any listeners
    setSilently(observable$, undefined);
  9. Achieve fine-grained reactivity with <Memo>

    main

    To prevent an entire React component from re-rendering when a value changes, use the <Memo> component. This allows only the specific part of the UI to update, leaving the rest of the component untouched.

    import { useObservable } from "@legendapp/state/react"
    import { Memo } from "@legendapp/state/react"
    
    function FineGrained() {
        const count$ = useObservable(0)
    
        // Example interval updating the state
        useInterval(() => {
            count$.set(v => v + 1)
        }, 600)
    
        // The text updates itself so the component doesn't re-render
        return (
            <div>
                Count: <Memo>{count$}</Memo>
            </div>
        )
    }
  10. Create and manipulate observables

    main

    Use observable() to create state. You can access raw data using .get() and update it using .set(). You can also create computed observables by passing a function to observable() that tracks other observables.

    import { observable, observe } from "@legendapp/state"
    
    const settings$ = observable({ theme: 'dark' })
    
    // get returns the raw data
    settings$.theme.get() // 'dark'
    
    // set sets the value
    settings$.theme.set('light')
    
    // Computed observables with just a function
    const isDark$ = observable(() => settings$.theme.get() === 'dark')
    
    // observing contexts re-run when tracked observables change
    observe(() => {
      console.log(settings$.theme.get())
    })
  11. Use Legend-State in React components

    main

    To use Legend-State in React, wrap your component with observer from @legendapp/state/react. Inside the component, calling .get() on an observable will cause the component to re-render whenever that specific value changes.

    import { observable } from "@legendapp/state"
    import { observer } from "@legendapp/state/react"
    
    const settings$ = observable({ theme: 'dark' })
    
    const Component = observer(function Component() {
        const theme = settings$.theme.get()
    
        return <div>Theme: {theme}</div>
    })