--- name: mobx description: Guides state management implementation using MobX + tsyringe. Triggered when handling data models, state logic, or Store architecture. --- # MobX State Management Guide This skill provides mandatory specifications for using MobX and tsyringe (for dependency injection DI) for state management in this project. ## 1. Architecture Overview - **MobX**: Core state management library (using `makeAutoObservable`). - **tsyringe**: Dependency injection container for models. - **Modular Models**: State is split into specialized models (e.g., `GlobalModel`, `Settings`). - **Auto-binding**: Actions are automatically bound to class instances. ## 2. Model Implementation Patterns ### 2.1 Basic Model Structure ```typescript import { makeAutoObservable } from 'mobx' import { injectable } from 'tsyringe' @injectable() export default class FeatureModel { count = 0 loading = false constructor() { // ✅ Use autoBind: true for convenient event handling makeAutoObservable(this, {}, { autoBind: true }) } increment() { this.count++ } async fetchData() { this.loading = true try { // API call logic } finally { this.loading = false } } } ``` ### 2.2 Singleton Model (Root Store) ```typescript import { singleton } from 'tsyringe' import { Settings } from '@/models' @singleton() export default class GlobalModel { // ✅ Dependency injection via constructor constructor(public settings: Settings) {} init() { this.settings.init() } off() { this.settings.off() } } ``` ### 2.3 Handling Complex Models (DI Composition) For complex state management (more than 20 reactive variables), models should be split into smaller, focused sub-models and composed using DI. ```typescript // ✅ Recommended: split complex models into multiple sub-models and inject @injectable() export default class ComplexFeatureModel { constructor( public data: DataSubModel, public ui: UISubModel, public sync: SyncSubModel, public util: Util ) { // Note: exclude injected dependencies, don't make them observable makeAutoObservable(this, { data: false, ui: false, sync: false, util: false }, { autoBind: true }) } } ``` ## 3. Parent-Child Model Communication ### 3.1 Accessing Singleton GlobalModel In sub-models, you can declare a non-observable property and bind the singleton `GlobalModel` instance in the constructor using the `getGlobal` utility function. ```typescript import { injectable } from 'tsyringe' import { getGlobal } from '@/utils' import type { GlobalModel } from '@/models' @injectable() export default class SubModel { // ✅ Declare as non-reactive property global = null as unknown as GlobalModel constructor() { makeAutoObservable(this, { global: false }, { autoBind: true }) // ✅ Bind instance via utility function getGlobal(this.global) } } ``` ### 3.2 Accessing Parent in Transient Models In non-singleton models, `container.resolve()` creates new instances. To ensure sub-models access the correct parent instance, the parent must explicitly pass its reference. **Key Principles:** 1. **Avoid Recursive Injection**: Never call `container.resolve(ParentModel)` inside the constructor of a sub-model that the parent model depends on. 2. **Active Assignment**: The parent model is responsible for establishing the association reference. ## 4. Lifecycle Management All models should implement `init()` and `off()` methods for initialization setup and destruction cleanup. ```typescript @injectable() export default class Index { constructor(public util: Util) { makeAutoObservable(this, { util: false }, { autoBind: true }) } async init() { // ✅ Collect cleanup functions (disposers) into util.acts array this.util.acts = [ /* ... */ ] } off() { // ✅ Execute cleanup logic this.util.off() } } ``` ## 5. Constraints and Best Practices ### 5.1 Observable Rules - **Reactive State**: Define all reactive state as class properties. - **Exclusions**: Use the second parameter of `makeAutoObservable` to exclude injected services/util classes (set to `false`), preventing them from becoming reactive. ### 5.2 Dependency Injection (tsyringe) - **@injectable()**: For feature models that can be instantiated multiple times or locally injected. - **@singleton()**: For global stores that should only have one instance. - **Constructor Injection**: Always prefer constructor injection, and use the `public` keyword to automatically assign properties. ### 5.3 Code Style - **No Comments**: Don't write explanatory comments in model files. - **snake_case**: All reactive state variable names must use snake_case. - **camelCase**: All methods must use camelCase. - **PascalCase**: All class names must use PascalCase. - **AutoBind**: Always use `{ autoBind: true }` in `makeAutoObservable`. ### 5.4 Common Errors - Forgetting the `@injectable` decorator will cause DI failure. - Forgetting to clean up reactions in `off()` will cause memory leaks. - Constructors should not be too heavy; time-consuming initialization logic must be placed in `init()`.