MobX

repository·main·Indexed 12 days ago

https://github.com/mobxjs/mobx

A signal-based state management library using functional reactive programming for scalable state management. Includes mobx-react and mobx-react-lite for React integration, providing the observer HOC, <Observer> component, and useLocalObservable hook. Also features eslint-plugin-mobx for linting rules such as mobx/exhaustive-make-observable and mobx/missing-observer.

Tokens
56.8K
Snippets
139
Records
199
Agent score
95%

What's inside MobX

  1. Introduction to MobX

    main

    MobX is a signal-based, functional reactive programming library for simple and scalable state management. It follows the philosophy that anything that can be derived from the application state should be, and it does so automatically.

    Key benefits include:

    • Straightforward: Use normal JavaScript assignments to update data; the reactivity system detects changes and propagates them.
    • Effortless optimal rendering: MobX tracks data usage at runtime to build a dependency tree. This ensures that computations (like React components) only re-run when strictly necessary, eliminating the need for manual memoization or selectors.
    • Architectural freedom: MobX is unopinionated and can manage state outside of any UI framework, making code decoupled, portable, and testable.
  2. Use mobx-react-lite for React function components

    main

    The mobx-react-lite package is designed specifically for React function components. It provides the necessary tools to make these components reactive by tracking observable usage automatically.

    If your project requires support for class components, you should use the mobx-react package instead. However, you can still use the <Observer> component inside the render methods of class components to create reactive regions.

  3. How to navigate MobX documentation

    main

    The MobX documentation is organized by complexity:

    • Common Concepts: Introduced first to help you get started quickly.
    • Advanced Concepts: Marked with a {🚀} icon. These are specialized topics you likely won't need unless you encounter specific use cases. You can effectively use MobX without mastering these.
    • Version Note: This documentation is written for MobX 6. While the API is largely the same in older versions, MobX 6 differs in its recommendation of syntax (moving away from decorators as the primary way to write enhanced classes).
  4. How MobX reactivity works

    main

    MobX reactivity is based on tracking property access during the execution of a tracked function. It does not track values themselves, but rather the "arrows" (references) used to reach those values.

    The Core Rule

    MobX reacts to any observable property that is read during the execution of a tracked function.

    Key Definitions

    • "Reading": Dereferencing a property via dot notation (user.name), bracket notation (user['name'], todos[3]), or destructuring (const {name} = user).
    • "Tracked Functions":
      • The expression of a computed value.
      • The rendering of an observer React component.
      • The render() method of an observer React class component.
      • The functions passed to autorun, reaction, and when.
    • "During": Only observables read while the function is actively executing are tracked. Observables accessed inside asynchronous blocks (like setTimeout, promise.then, or await) spawned by the function are not tracked.

    What MobX does NOT react to

    • Values obtained from observables but outside a tracked function.
    • Observables read in asynchronously invoked code blocks.
    • Changing a non-observable variable that happens to point to an observable object.
    • Simply referencing an observable object without accessing its properties (e.g., just mentioning message.likes without reading its length or an index).
  5. Compare `intercept` and `observe`

    main

    Both intercept and observe are low-level utilities used to monitor changes to a single observable, but they operate at different stages of the mutation lifecycle:

    Featureinterceptobserve
    TimingBefore mutation is appliedAfter mutation is applied
    Primary UseValidating, normalizing, or cancelling changesMonitoring changes after they happen
    ControlCan prevent the change (return null or throw)Cannot prevent the change (only reacts to it)
    TransactionsN/ADoes not respect transactions (fires per mutation)
    NestingDoes not track nested observablesDoes not track nested observables

    Recommendation: For most use cases, use reaction or autorun instead of these utilities.

  6. Best practices for using `observer`

    main

    Which components should be marked with observer?

    Rule of thumb: All components that render observable data.

    Even small components should be marked with observer. This allows them to render independently from their parents, which improves overall application performance. The overhead of using observer is negligible.

    If you want to create a generic component package that doesn't depend on MobX, ensure you only pass plain (non-observable) data to those components.

  7. How the `observer` HoC works

    main

    The observer HoC works by tracking which observables are accessed during the component's render function.

    Key behaviors:

    • Automatic Subscription: It subscribes the component to any observable read during rendering.
    • Precise Re-renders: Components only re-render when the specific observables they depend on change. If an observable is accessible but not actually read during render, it won't trigger a re-render.
    • Deep Tracking: It supports deep reading of observables (e.g., todos[0].author.displayName) automatically.
    • Efficiency: Unlike frameworks that require explicit dependency declarations or pre-computed selectors, MobX tracks dependencies dynamically and precisely.
    import React from "react"
    import ReactDOM from "react-dom"
    import { makeAutoObservable } from "mobx"
    import { observer } from "mobx-react-lite"
    
    class Timer {
        secondsPassed = 0
    
        constructor() {
            makeAutoObservable(this)
        }
    
        increaseTimer() {
            this.secondsPassed += 1
        }
    }
    
    const myTimer = new Timer()
    
    // A function component wrapped with `observer` will react
    // to any future change in an observable it used before.
    const TimerView = observer(({ timer }) => <span>Seconds passed: {timer.secondsPassed}</span>)
    
    ReactDOM.render(<TimerView timer={myTimer} />, document.body)
    
    setInterval(() => {
        myTimer.increaseTimer()
    }, 1000)
  8. Best practices for using reactions

    main

    Reactions should be used sparingly. Before implementing one, evaluate your code against these three principles:

    1. Only use Reactions if there is no direct relation between cause and effect: If a side effect is a direct result of a specific action (e.g., a button click), trigger it directly from that action instead of reacting to a state change. Use reactions for broad synchronization (e.g., syncing any form state change to local storage).
    2. Reactions shouldn't update other observables: If a reaction's purpose is to calculate a new value based on other observables, use a computed value instead. Reactions should cause effects, not compute data.
    3. Reactions should be independent: Do not rely on a specific execution order between different reactions. MobX does not guarantee the order in which reactions run. If one reaction depends on another, they should likely be merged.
  9. Actions and inheritance rules

    main

    When using inheritance with MobX actions, note the following:

    1. Prototype vs Instance: Only actions defined on the prototype can be overridden by a subclass.
    2. Arrow Functions: If you define an action as an arrow function on the instance (e.g., arrowAction = () => {}), it cannot be overridden by a subclass and will throw a TypeError if you attempt to redefine it.
    3. Binding: To avoid using arrow functions for binding this, use action.bound on prototype methods instead.
    class Parent {
        // on prototype - OK to override
        action() {}
        boundAction() {}
    
        // on instance - NOT OK to override
        arrowAction = () => {}
    
        constructor() {
            makeObservable(this, {
                arrowAction: action,
                action: action,
                boundAction: action.bound,
            })
        }
    }
    
    class Child extends Parent {
        // OK
        action() {}
        boundAction() {}
    
        // THROWS: TypeError: Cannot redefine property: arrowAction
        arrowAction = () => {}
    }
  10. Implementing Domain Objects as Classes

    main

    While domain objects can be plain objects, using classes is recommended for several reasons:

    • Methods: You can attach business logic directly to the object, making it easier to pass around without needing to pass the store or context constantly.
    • Visibility Control: Fine-grained control over which attributes and methods are visible.
    • Mixed Observability: Easily mix observable and non-observable properties/methods.
    • Type Safety: Easier to recognize and strictly type-check in TypeScript/JavaScript.

    Unlike Redux, MobX does not require data normalization. Domain objects can hold real references to other domain objects, even creating cyclic structures.

  11. When to apply the `observer` decorator

    main

    The rule of thumb for React integration is: apply observer to all components that read observable data.

    observer only enhances the specific component it decorates, not the components called by it. Applying observer to more components actually makes rendering more efficient because updates become more fine-grained. Do not worry about the overhead of having many observer components.

  12. Architectural pattern for separating Domain and UI stores

    main

    For large-scale maintainable projects, it is a best practice to separate state into two distinct types of stores:

    1. Domain Stores: These manage the core data your application is about (e.g., Users, Products, Orders). They handle business logic, backend integration, and ensure data consistency (e.g., ensuring only one instance of a specific entity exists in memory).
    2. UI Stores: These manage transient, loosely coupled pieces of information that affect the interface but aren't part of the core business data. This includes session info, loading states, window dimensions, current language, active themes, or global UI visibility (like toolbars or wizards).

    Separating these allows you to reuse and test domain logic universally across different environments (frontend/backend) while keeping UI-specific logic isolated.