Valtio

repository·main·Indexed 11 days ago

https://github.com/pmndrs/valtio

A proxy-state management library for React and Vanilla JavaScript that allows direct mutation of state while providing optimized, snapshot-based re-renders. Version 2.3.2 features include the useSnapshot hook for React, a vanilla JS implementation via valtio/vanilla, and utilities like proxyMap, proxySet, and ref for untracked objects.

Tokens
29.1K
Snippets
105
Records
133
Agent score
93%

What's inside Valtio

  1. Valtio v2 Requirements and Changes

    main

    Valtio v2 introduced several breaking changes and environment requirements:

    • React Version: Requires React 18 or above.
    • TypeScript Version: Requires TypeScript 4.5 or above.
    • Build Target: Updated to ES2018.
    • useSnapshot() Behavior: The implementation was altered to improve compatibility with useMemo and the React compiler. This may result in extra re-renders in certain edge cases.
    • Deprecated Features: All previously deprecated features have been removed.
  2. Explore community libraries for Valtio

    main

    While Valtio provides the core necessities for proxy state management, you can extend its functionality using various community-maintained libraries.

    Note: These libraries are not officially recommended or maintained by the Valtio team and may have bugs or limited maintenance.

    Available Extensions:

    • Framework & Runtime Integrations:

      • electron-valtio: Share state between Electron main process and renderer windows.
      • sveltio: State management for Svelte using proxies.
      • tauri-plugin-valtio: Persistent state for Tauri, accessible from JS and Rust.
      • valtio-element: Create reactive, declarative custom elements.
    • State Management Patterns & Utilities:

      • valtio-fsm: A TypeScript-first finite state machine library.
      • valtio-factory: Create state using the factory pattern.
      • valtio-persist: Save state to disk (includes valtio-auto-persist for automatic object identification).
      • valtio-zod: Validate state updates using Zod.
      • valtio-yjs: Integration with yjs for shared state.
    • React Bindings:

      • use-valtio: An alternative custom hook for proxy state.
      • valtio-signal: Another React binding for proxy state.
    • Developer Tools & Tooling:

      • eslint-plugin-valtio: ESLint plugin for Valtio.
      • storybook-valtio-auto-bind: Bidirectional sync between Storybook args and Valtio stores.
      • swc-plugin-valtio: useProxy transformer for SWC.
    • Core Extensions:

      • valtio-plugin: A lifecycle plugin system for customizing Valtio usage.
      • valtio-reactive: Makes Valtio a reactive library.
  3. Preserve key equality in `proxyMap` using `ref`

    main

    When using objects as keys in a proxyMap, Valtio's proxying mechanism might interfere with key equality. To ensure a key remains exactly what you intended (preserving its identity), wrap the key object with ref.

    • With ref: The key is treated as a stable reference, allowing state.get(key) to work correctly.
    • Without ref: The key is proxied, which can cause state.get(key) to return undefined because the proxy object does not match the original key used in .set().
    import { proxyMap, ref } from 'valtio/utils'
    
    // Use ref to preserve key identity
    const key = ref({})
    state.set(key, 'hello')
    state.get(key) // 'hello'
    
    // Without ref, the key is proxied and equality fails
    const key2 = {}
    state.set(key2, 'value')
    state.get(key2) // undefined
  4. Alternatives for managing component-scoped Valtio state

    main

    If you prefer not to use useRef to manage the lifecycle of a proxy within a component, you can consider the following alternatives:

    • use-constant: A utility to keep a value constant across renders.
    • bunshi: A dependency injection library that provides recipes for Valtio.
    • Custom Hooks: Create your own hooks that wrap useContext and optionally useSnapshot to provide a cleaner API for consuming component-scoped state.
  5. Use `ref` to prevent proxy update propagation

    main

    You can use ref with an existing Valtio proxy to prevent a parent proxy from tracking a child proxy. This is useful when a child proxy is managed independently and you do not want its updates to trigger notifications for subscribers of the parent proxy.

    Key Behaviors:

    • Identity Preservation: When you pass an existing proxy to ref(child), it returns the same object (ref(child) === child).
    • Snapshot Behavior: The child is kept by identity in the parent snapshot rather than being replaced with an immutable child snapshot (snapshot(parent).child === child).
    • Global Marking: The ref marking applies to the proxy's identity globally within the current Valtio runtime. If the same proxy is added to another parent without being wrapped in ref again, that parent will also treat it as an untracked reference.
    • Subscription: Because the parent does not track the child, you must subscribe to the child proxy separately to observe its updates.
    const child = proxy({ count: 0 })
    const parent = proxy({ child: ref(child) })
    
    // Changes to child do not notify subscribers of parent.
    // To observe updates, subscribe to the child directly:
    subscribe(child, () => {
      // child changed
    })
    
    // The marking is global; another parent will also treat it as untracked:
    const anotherParent = proxy({ child })
  6. Supported and unsupported types in proxies

    main

    While most serializable objects and classes can be proxied, certain special objects cannot be tracked effectively. Changes to these objects will not trigger updates in the Valtio state.

    Unsupported types (will not trigger updates):

    • DOM elements (e.g., d3.select('#chart'))
    • React elements (e.g., React.createElement('div'))
    • Certain built-in collections like Map (use proxyMap instead)
    • Browser APIs like localStorage

    Supported types:

    • Serializable objects
    • Class instances
    // This works
    class User {
      first = null
      last = null
      constructor(first, last) {
        this.first = first
        this.last = last
      }
      greet() {
        return `Hi ${this.first}!`
      }
    }
    
    const state = proxy(new User('Timo', 'Kivinen'))
    // This works
    class User {
      first = null
      last = null
      constructor(first, last) {
        this.first = first
        this.last = last
      }
      greet() {
        return `Hi ${this.first}!`
      }
    }
    const state = proxy(new User('Timo', 'Kivinen'))
  7. What are Ops and how do they work?

    main

    Ops (Operations) are granular mutation records that provide a detailed description of exactly what was modified in a Valtio proxy. While standard subscribe only notifies you that something changed, Ops provide a tuple describing the change type, the location (Path), the new value, and (for certain types) the previous value.

    Op Types

    TypeStructureDescription
    set[op: 'set', path: Path, value: unknown, prevValue: unknown]Triggered when a property is assigned a new value.
    delete[op: 'delete', path: Path, prevValue: unknown]Triggered when a property is deleted.
    resolve[op: 'resolve', path: Path, value: unknown]Triggered when a promise in the state is fulfilled.
    reject[op: 'reject', path: Path, error: unknown]Triggered when a promise in the state is rejected.

    Note: Path is an array of strings or symbols representing the nested location (e.g., ['user', 'profile', 'name']).

  8. Alternatives to useRef for scoped Valtio state

    main

    If you prefer not to use useRef to manage the lifecycle of a scoped Valtio proxy, you can use the following alternatives:

    • use-constant: A utility to keep a value constant across renders.
    • Bunshi: A dependency injection library that has specific recipes for Valtio.
    • Custom Hooks: Create a custom hook that combines useContext and useSnapshot to simplify consumption in child components.
  9. Listen to detailed state operations (Ops)

    main

    By default, the subscribe callback only provides access to the current state. However, you can enable Ops (Operations) to receive detailed records of exactly what changed (e.g., which property was set, updated, or deleted).

    This is useful for implementing advanced features like:

    • State synchronization across different environments
    • Undo/redo functionality

    Note: Tracking operations incurs a small performance cost, so they are disabled by default. You must explicitly enable them to access the Ops argument in the callback.

  10. Nest proxies within other proxies

    main

    Proxies can be nested inside other proxy objects. When a nested proxy is updated, the parent proxy tracks the change, allowing you to update deep properties through the parent reference.

    import { proxy } from 'valtio'
    
    const personState = proxy({ name: 'Timo', role: 'admin' })
    const authState = proxy({ status: 'loggedIn', user: personState })
    
    // Updating the nested proxy via the parent
    authState.user.name = 'Nina'
  11. Optimizations: noop and batching in Valtio proxies

    main

    Valtio includes built-in optimizations to prevent unnecessary re-renders and updates:

    1. Noop (No-operation): If you set a property to the exact same value it currently holds, the update is ignored and subscribers are not notified.
    2. Batching: Multiple mutations occurring within the same event loop tick are batched together. Subscribers will only be notified once for the entire batch of changes.
    const state = proxy({ count: 0, text: 'hello' })
    
    // Noop: setting to the same value has no effect
    state.count = 0 
    
    // Batching: subscribers are notified once after both mutations
    state.count = 1
    state.text = 'world'
    const state = proxy({ count: 0, text: 'hello' })
    
    // noop
    state.count = 0 
    
    // batching
    state.count = 1
    state.text = 'world'