mobx-state-tree

repository·master·Indexed 27 days ago

https://github.com/mobxjs/mobx-state-tree

An opinionated, transactional, MobX-powered state container system. It provides a structured way to define models with types, manage references between models, and handle state through snapshots. Key features include support for multiple stores, async actions, strong TypeScript integration, and targeted re-renders when used with React.

Tokens
51.7K
Snippets
111
Records
331
Agent score
92%

What's inside mobx-state-tree

  1. Introduction to MobX-State-Tree

    master

    MobX-State-Tree (MST) is a batteries-included state management library designed to provide structure and safety to reactive state. It is built on top of MobX and provides several key features:

    • Centralized stores: Organize your data in structured models.
    • Protected Mutability: Data is easy to work with via mutation but remains safe through controlled access.
    • Snapshots & Time-travel: Updates are serializable and traceable, allowing you to generate snapshots of your state.
    • Side effect management: Manage consequences of mutations directly within MST models, reducing the need for external useEffect hooks.
    • Type Safety: Provides both runtime type checking and automatic TypeScript static type inference.
    • Data Normalization: Supports references to normalize data across your application.

    While MobX acts as the underlying reactive engine, MST provides the high-level structure and tools (the 'luxury car') to manage complex application states efficiently.

  2. Overview of mobx-state-tree (MST)

    master

    mobx-state-tree (MST) is a state container system built on top of MobX. While MobX acts as a functional reactive state management engine, MST provides structure, common tools, and a type system for your application state.

    Key features include:

    • Support for multiple stores.
    • Support for async actions and side effects.
    • Extremely targeted re-renders (especially when used with React).
    • Strong TypeScript integration.
    • Zero dependencies other than MobX.
    • Snapshot-based state management.

    It is designed to scale from small applications to large-scale team projects and is often used as a high-performance, lower-boilerplate alternative to Redux.

  3. Understand the core concepts of mobx-state-tree

    master

    mobx-state-tree (MST) is a state container that uses a "living tree" model. It combines the benefits of:

    • Mutability: Easy to use via actions that modify local instance properties directly.
    • Immutability: Automatic generation of structurally shared snapshots for traceability and time-travel.
    • Reactiveness: High performance via MobX-based observability.

    Key concepts include:

    • Models: Composable components that capture pieces of state.
    • Snapshots: Immutable representations of the tree's state.
    • Actions: The only way to modify the tree, ensuring encapsulated and protected updates.
    • Liveness Guarantees: MST prevents stale reads/writes by throwing exceptions if you attempt to access an object that is no longer part of a state tree.
  4. Understand JSON Patches in mobx-state-tree

    master

    Modifying a model in mobx-state-tree generates a stream of JSON-patches (RFC 6902) that describe the specific modifications made.

    Key characteristics:

    • Immediate Emission: Patches are emitted immediately upon mutation and do not respect transaction boundaries (unlike snapshots).
    • Deep Observing: Patch listeners can be used to achieve deep observing of models.
    • Relative Paths: The path attribute in a patch is relative to the location where the listener is attached.
    • Granularity: A single mutation (like splicing an array) can result in multiple patches.
    • Reversibility: Patches can be reverse-applied, which is useful for implementing undo/redo functionality.
  5. Compare mobx-state-tree to Redux

    master

    While MST resembles an immutable state tree like Redux, it differs in several key ways:

    • Architecture: Like Redux, MST prescribes a specific state architecture.
    • Mutation: Unlike Redux, MST allows direct modification of values in the tree within actions; you do not need to construct a new tree manually.
    • Observation: MST allows for fine-grained and efficient observation of any point in the state tree.
    • Patches: MST automatically generates JSON patches for every modification.
    • Interoperability: MST provides utilities to convert any MST tree into a valid Redux store.
    • Scalability: You can have multiple MST trees within a single application.
  6. Understand and use Snapshots in mobx-state-tree

    master

    Snapshots are immutable, plain-object serializations of a tree at a specific point in time. They are stripped of type information and actions, making them ideal for transportation (e.g., sending to a server or storing in local storage). Requesting a snapshot is efficient because MST maintains them in the background using structural sharing.

    Key properties:

    • Immutability: Snapshots cannot be changed.
    • Transportability: Suitable for JSON serialization.
    • State Restoration: Can be used to update models or restore them to a previous state.
    • Automatic Conversion: Snapshots are automatically converted to models when used to populate a tree. For example, store.todos.push({ title: "test" }) is equivalent to store.todos.push(Todo.create({ title: "test" })).
  7. Use runtime type safety in MobX-State-Tree

    master

    MobX-State-Tree enforces type safety at runtime when creating models. If you attempt to instantiate a model with data that violates its type definitions (e.g., providing a function where a identifierNumber is expected), MST will throw a detailed error identifying the exact path and the nature of the mismatch.

    Note: For performance reasons, MST does not run these runtime checks in production mode by default.

  8. Handle circular dependencies between files and types using `types.late`

    master

    When you have circular dependencies between models across different files, you can use types.late to defer the evaluation of the model type. This allows you to import a model that hasn't been fully initialized yet.

    To implement this:

    1. In the file exporting the model, export a function that returns the model definition.
    2. In the file importing the model, wrap the imported model in types.late(() => ImportedModel).
    // In the exporting file:
    export function LateStore() {
        return types.model({
            title: types.string
        })
    }
    
    // In the importing file:
    import { LateStore } from "./circular-dep"
    
    const Store = types.late(() => LateStore)
  9. Use mst-form-type for UI form management

    master

    The mst-form-type library allows you to model UI field management (such as Ant Design's Form component status and validation rules) as conventional MobX State Tree type definitions. This enables you to keep form logic inside your MST models instead of manually syncing status changes between a UI component and your state tree.

    Note: mst-form-type provides the model types for the form structure; it does not manage the business logic related to field interactions.

  10. Simulate inheritance using type composition

    master

    MobX-State-Tree (MST) does not support classical inheritance. Instead, you should use composition to build new types from existing ones. You can achieve this by:

    1. Chaining methods: Using .props(), .views(), or .actions() on an existing type to produce a fresh type.
    2. Using types.compose: Combining multiple types into a single new type.
    3. Using .named(): Providing a specific name to the newly composed type.

    To simulate 'super' calls (overriding a method while still accessing the original implementation), capture the base method as a local variable within the new .views() or .actions() block before returning the new implementation.

    const Square = types
        .model(
            "Square",
            {
                width: types.number
            }
        )
        .views(self => ({
            surface() {
                return self.width * self.width
            }
        }))
    
    // create a new type, based on Square
    const Box = Square
        .named("Box")
        .views(self => {
            // capture the base implementation to simulate 'super'
            const superSurface = self.surface
    
            return {
                surface() {
                    return superSurface() * 1
                },
                volume() {
                    return self.surface() * self.width
                }
            }
        }))