Skip to main content

Overview

The configureLegendState function allows you to extend Legend-State by adding custom functions and properties to all observables. This is useful for creating reusable utilities or integrating with other libraries.

Signature

function configureLegendState(config: {
    observableFunctions?: Record<string, (node: NodeInfo, ...args: any[]) => any>;
    observableProperties?: Record<string, { 
        get: (node: NodeInfo) => any; 
        set: (node: NodeInfo, value: any) => any 
    }>;
    jsonReplacer?: (this: any, key: string, value: any) => any;
    jsonReviver?: (this: any, key: string, value: any) => any;
}): void
config
object
required
Configuration object
observableFunctions
Record<string, Function>
Custom functions to add to all observables. Each function receives the node and additional arguments.
observableProperties
Record<string, { get, set }>
Custom properties to add to all observables. Each property has a getter and setter that receive the node.
jsonReplacer
(key: string, value: any) => any
Custom JSON.stringify replacer function for observables
jsonReviver
(key: string, value: any) => any
Custom JSON.parse reviver function for observables

Adding Custom Functions

Add methods that can be called on any observable:
import { configureLegendState, type NodeInfo } from '@legendapp/state';
import { internal } from '@legendapp/state';

const { get, set } = internal;

const { configureLegendState } from '@legendapp/state/config/configureLegendState';

configureLegendState({
    observableFunctions: {
        // Add a toggle() method for boolean observables
        toggle(node: NodeInfo) {
            const current = get(node);
            set(node, !current);
        },
        
        // Add an increment() method
        increment(node: NodeInfo, amount: number = 1) {
            const current = get(node);
            set(node, current + amount);
        },
        
        // Add a reset() method
        reset(node: NodeInfo, defaultValue: any) {
            set(node, defaultValue);
        }
    }
});

// Now use the custom functions
import { observable } from '@legendapp/state';

const state$ = observable({
    isOpen: false,
    count: 0
});

// Use custom methods
state$.isOpen.toggle(); // true
state$.count.increment(5); // 5
state$.count.reset(0); // 0
Custom functions are added to the prototype, so they’re available on all observables throughout your application.

Adding Custom Properties

Add getter/setter properties to all observables:
import { configureLegendState, type NodeInfo } from '@legendapp/state';
import { internal } from '@legendapp/state';

const { get, set } = internal;

configureLegendState({
    observableProperties: {
        // Add a 'value' property as an alias for get/set
        value: {
            get(node: NodeInfo) {
                return get(node);
            },
            set(node: NodeInfo, value: any) {
                set(node, value);
            }
        },
        
        // Add a 'double' property
        double: {
            get(node: NodeInfo) {
                return get(node) * 2;
            },
            set(node: NodeInfo, value: any) {
                set(node, value / 2);
            }
        }
    }
});

// Now use the custom properties
import { observable } from '@legendapp/state';

const num$ = observable(5);

// Use custom property
console.log(num$.value); // 5 (same as num$.get())
num$.value = 10; // Same as num$.set(10)

console.log(num$.double); // 20
num$.double = 20; // Sets num$ to 10

Custom JSON Serialization

Configure how observables are serialized and deserialized:
import { configureLegendState } from '@legendapp/state/config/configureLegendState';
import { observable } from '@legendapp/state';

configureLegendState({
    // Custom replacer for JSON.stringify
    jsonReplacer(key: string, value: any) {
        // Convert Dates to ISO strings
        if (value instanceof Date) {
            return { __type: 'Date', value: value.toISOString() };
        }
        return value;
    },
    
    // Custom reviver for JSON.parse
    jsonReviver(key: string, value: any) {
        // Restore Date objects
        if (value && value.__type === 'Date') {
            return new Date(value.value);
        }
        return value;
    }
});

const state$ = observable({
    createdAt: new Date('2024-01-01'),
    name: 'Test'
});

// Serialize with custom replacer
const json = JSON.stringify(state$.get());
console.log(json);
// {"createdAt":{"__type":"Date","value":"2024-01-01T00:00:00.000Z"},"name":"Test"}

// Deserialize with custom reviver
const restored = JSON.parse(json);
console.log(restored.createdAt instanceof Date); // true

Real-World Examples

Validation Methods

import { configureLegendState, type NodeInfo } from '@legendapp/state';
import { internal } from '@legendapp/state';

const { get } = internal;

configureLegendState({
    observableFunctions: {
        // Validate email
        isValidEmail(node: NodeInfo): boolean {
            const value = get(node);
            if (typeof value !== 'string') return false;
            return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
        },
        
        // Validate required
        isRequired(node: NodeInfo): boolean {
            const value = get(node);
            return value !== null && value !== undefined && value !== '';
        },
        
        // Validate min length
        hasMinLength(node: NodeInfo, minLength: number): boolean {
            const value = get(node);
            return typeof value === 'string' && value.length >= minLength;
        }
    }
});

// Usage
import { observable } from '@legendapp/state';

const form$ = observable({
    email: '',
    password: ''
});

if (!form$.email.isValidEmail()) {
    console.error('Invalid email');
}

if (!form$.password.hasMinLength(8)) {
    console.error('Password must be at least 8 characters');
}

Persistence Helpers

import { configureLegendState, type NodeInfo } from '@legendapp/state';
import { internal } from '@legendapp/state';

const { get, set } = internal;

configureLegendState({
    observableFunctions: {
        // Save to localStorage
        saveLocal(node: NodeInfo, key: string) {
            const value = get(node);
            localStorage.setItem(key, JSON.stringify(value));
        },
        
        // Load from localStorage
        loadLocal(node: NodeInfo, key: string) {
            const stored = localStorage.getItem(key);
            if (stored) {
                set(node, JSON.parse(stored));
            }
        },
        
        // Clear from localStorage
        clearLocal(node: NodeInfo, key: string) {
            localStorage.removeItem(key);
        }
    }
});

// Usage
import { observable } from '@legendapp/state';

const settings$ = observable({
    theme: 'dark',
    lang: 'en'
});

// Load saved settings
settings$.loadLocal('app-settings');

// Save settings
settings$.theme.onChange(() => {
    settings$.saveLocal('app-settings');
});

Array Utilities

import { configureLegendState, type NodeInfo } from '@legendapp/state';
import { internal } from '@legendapp/state';

const { get, set } = internal;

configureLegendState({
    observableFunctions: {
        // Find item in array
        findItem(node: NodeInfo, predicate: (item: any) => boolean) {
            const arr = get(node);
            if (!Array.isArray(arr)) return undefined;
            return arr.find(predicate);
        },
        
        // Remove item from array
        removeItem(node: NodeInfo, predicate: (item: any) => boolean) {
            const arr = get(node);
            if (!Array.isArray(arr)) return;
            const filtered = arr.filter(item => !predicate(item));
            set(node, filtered);
        },
        
        // Toggle item in array
        toggleItem(node: NodeInfo, item: any) {
            const arr = get(node);
            if (!Array.isArray(arr)) return;
            const index = arr.indexOf(item);
            if (index >= 0) {
                arr.splice(index, 1);
            } else {
                arr.push(item);
            }
            set(node, [...arr]);
        }
    }
});

// Usage
import { observable } from '@legendapp/state';

const todos$ = observable([
    { id: 1, text: 'Task 1', done: false },
    { id: 2, text: 'Task 2', done: true }
]);

const todo = todos$.findItem(t => t.id === 1);
todos$.removeItem(t => t.done);

TypeScript Support

Extend the TypeScript types to include your custom functions and properties:
import { configureLegendState } from '@legendapp/state/config/configureLegendState';
import type { Observable } from '@legendapp/state';

// Extend the Observable interface
declare module '@legendapp/state' {
    interface Observable<T> {
        // Add custom function types
        toggle(): void;
        increment(amount?: number): void;
        reset(defaultValue: T): void;
        
        // Add custom property types
        value: T;
        double: T extends number ? number : never;
    }
}

configureLegendState({
    observableFunctions: {
        toggle(node) { /* ... */ },
        increment(node, amount = 1) { /* ... */ },
        reset(node, defaultValue) { /* ... */ }
    },
    observableProperties: {
        value: {
            get(node) { /* ... */ },
            set(node, value) { /* ... */ }
        },
        double: {
            get(node) { /* ... */ },
            set(node, value) { /* ... */ }
        }
    }
});

Best Practices

Call once at startup: Configure Legend-State once when your app initializes, before creating any observables.
Use descriptive names: Choose function and property names that clearly indicate their purpose and don’t conflict with built-in methods.
Access internal APIs carefully: The NodeInfo type and internal functions are low-level APIs. Use the provided get and set functions from internal to safely access and modify node values.

Build docs developers (and LLMs) love