mobx-keystone

repository·master·Indexed 20 days ago

https://github.com/xaviergonz/mobx-keystone

A TypeScript-first state management solution built on MobX. It uses a model-driven approach with data trees to provide a mutable developer experience with immutable traceability via snapshots, patches, and undo/redo capabilities. The ecosystem includes integrations for real-time collaboration and offline support through mobx-keystone-loro (Loro CRDTs) and mobx-keystone-yjs (Y.js).

Tokens
65.8K
Snippets
213
Records
306
Agent score
67%

What's inside mobx-keystone

  1. Overview of mobx-keystone

    master

    mobx-keystone is a TypeScript-first model layer built on top of MobX designed for complex client-side applications. It provides a single source of truth by combining mutable model code with immutable traceability.

    Key features include:

    • Protected Mutability: Write straightforward actions and computed values while the library ensures state changes are explicit and safe via runtime protection.
    • Immutable Traceability: Automatically derive immutable, structurally shared snapshots and JSON patches for persistence, synchronization, replay, and debugging.
    • Built-in Primitives: Includes support for references, transactions, action middlewares, and undo/redo functionality.
    • Strong Typing: Offers robust TypeScript inference for models, snapshots, and actions.
    • MobX Integration: Works seamlessly with mobx and mobx-react-lite.
  2. Compare mobx-keystone with mobx-state-tree

    master

    If you are transitioning from mobx-state-tree (MST), mobx-keystone shares many core concepts like tree-like structures, immutable snapshot generation, patch generation, and action serialization.

    Key advantages of mobx-keystone over MST include:

    • Improved TypeScript Support: Easier typing for self-recursive and cross-referenced models without needing late types or casting.
    • Simplified Type Usage: Clearer distinction between snapshots and instances; snapshots are primarily used with getSnapshot and fromSnapshot.
    • Standard Decorators: Uses standard MobX @computed decorators and this context instead of the self vs this confusion found in MST views.
    • Predictable Lifecycle: Avoids the pitfalls of MST's lazy initialization by providing reliable lifecycle hooks.
    • Optional Runtime Validation: Runtime type checking is completely optional and can be used alongside standard TypeScript annotations.
  3. Understand the properties and limitations of computed trees

    master

    Computed trees are derived trees that behave differently than regular mutable trees due to their immutability.

    Key Behaviors

    • Immutability: Computed trees are derived and immutable. Immutability is enforced at runtime via the readonly middleware. Because they are immutable, action middlewares are never applied and patches are not generated.
    • Contexts & References: Contexts, References, and Back-references are fully available within a computed tree and work across the boundaries between regular and computed trees.
    • Tree Traversal: Traversal methods work across boundaries. However, most utility methods do not work on computed tree nodes due to immutability, with the exception of onChildAttachedTo (which triggers when a child is re-computed).
    • Lifecycle Hooks: Hooks work as expected. For example, onAttachedToRootStore is called during each re-computation if the tree is part of a root store.
    • Snapshots: Snapshots do not contain data from computed trees.

    Critical Requirement for References

    When using References within a computed tree, you must ensure that the ID of the referenced model instance is stable across re-computations of the computed tree.

  4. Create value-type class models

    master

    Value-type models act like primitives. When a value-type model is attached to a tree, it is automatically cloned if it already has a parent. This prevents the error of a single node attempting to have multiple parents.

    To enable this behavior, pass { valueType: true } as the second argument to the Model constructor.

    Note: Because it is a clone, modifying the new instance will not affect the original instance.

    @model("myApp/Color")
    class Color extends Model(
      {
        r: prop<number>(),
        g: prop<number>(),
        b: prop<number>(),
      },
      {
        valueType: true,
      }
    ) {}
  5. Core mobx-keystone primitives and use cases

    master

    Use the following primitives depending on your specific requirement:

    NeedUse
    Mutable domain objects with actions and hooksClass Models
    Backend-shaped data without $modelType in the payloadData Models
    Async actionsStandard and Standalone Actions
    App-level lifecycle and side effectsRoot Stores
    Serialization and persistenceSnapshots
    Fine-grained change streamsPatches
    Runtime validationRuntime Type Checking
  6. Understand the UndoEvent structure

    master

    Every change recorded by the middleware is stored as an UndoEvent. This readonly structure contains:

    • targetPath: Path: The path to the object that triggered the action from its root.
    • actionName: string: The name of the invoked action.
    • patches: ReadonlyArray<Patch>: Patches representing the changes made (applied during redo()).
    • inversePatches: ReadonlyArray<Patch>: Patches used to reverse the changes (applied during undo()).
  7. Use `SimpleActionContext` in tracking middleware

    master

    The SimpleActionContext is a simplified, read-only data object provided to actionTrackingMiddleware hooks. It abstracts away the differences between synchronous steps and asynchronous flows.

    Available properties:

    • actionName: string: The name of the action.
    • type: ActionContextActionType: Whether the action is sync or async.
    • target: AnyModel: The target model instance.
    • args: ReadonlyArray<any>: Arguments passed to the action.
    • parentContext?: SimpleActionContext: The parent context, if applicable.
    • rootContext: SimpleActionContext: The root context (or itself if it is the root).
    • data: any: An object for custom data. Tip: Use Symbols as keys to avoid name collisions between different middlewares.
  8. Use life-cycle event hooks in Class Models

    master

    Class models can implement specific hooks to handle initialization and attachment:

    • onInit(): Replaces the constructor. Fires immediately when the model is created.
    • onAttachedToRootStore(rootStore): Fires when the model becomes part of a root store tree. It can optionally return a disposer function that runs when the model detaches from the tree.
  9. Inherit from other data models

    master

    To extend an existing data model, you must use ExtendedDataModel instead of DataModel.

    Key Rules:

    1. Use ExtendedDataModel(BaseClass, { ...props }) to inherit properties and logic.
    2. If the base model implements onLazyInit, you must call super.onLazyInit(...) in the extended model's implementation.
    3. When extending generic classes, use modelClass<SpecificType>(BaseClass) to avoid generic parameters defaulting to unknown.
    @model("MyApp/Point")
    class Point extends DataModel({
      x: prop<number>(),
      y: prop<number>(),
    }) {
      get sum() {
        return this.x + this.y
      }
    }
    
    @model("MyApp/Point3d")
    class Point3d extends ExtendedDataModel(Point, {
      z: prop<number>(),
    }) {
      get sum() {
        return super.sum + this.z
      }
    }
  10. Use standard decorators and 'this' for computed properties

    master

    In mobx-keystone, you do not need to worry about the distinction between self (for properties from previous chunks) and this (for properties in the current chunk) as required in MST. You can always use this and the standard MobX @computed decorator to define computed values.

    @model("myApp/Todo")
    class Todo extends Model({ 
      done: prop(false), 
      text: prop<string>(), 
      title: prop<string>() 
    }) {
      @computed
      get asStr() {
        return `${this.text} is done? ${this.done}`
      }
    
      @computed
      get asStrWithTitle() {
        return `${this.title} - ${this.asStr}`
      }
    }
  11. What are snapshots in mobx-keystone

    master

    Snapshots are immutable, structurally shared representations of tree nodes (models and their children). They serve two primary purposes:

    1. Serialization/Deserialization: Storing data or sending it over a network.
    2. Data Bridging: Providing data to React components that do not use mobx-react-lite.

    Structural Sharing Optimization: When a property in a node changes, a new snapshot is generated for that node and all its parents. However, unaffected siblings and their subtrees retain their previous snapshot references. This ensures that snapshot generation is highly efficient and minimizes memory overhead.