# easy-model **easy-model** is a React state management and IoC toolkit built around **Model Classes + Dependency Injection + Fine‑grained change watching**. With plain TypeScript classes that describe your business models, a small set of APIs lets you: - **Create / inject model instances directly in function components** (`useModel` / `useInstance`) - **Share the same instance across components**, with instance caching grouped by arguments (`provide`) - **Watch changes on models and their nested properties** (`watch` / `useWatcher`) - **Use decorators and an IoC container for dependency injection** (`Container` / `CInjection` / `VInjection` / `inject`) - **Manage async loading states in a unified way** (`loader` / `useLoader`) Compared with Redux / MobX / Zustand, easy-model aims to **keep an OOP‑like mental model while providing solid performance, strong typing, and built‑in IoC capabilities**. ### Feature overview - **Class‑based Model** Use TypeScript classes to model your domain: fields are state, methods are business logic. No extra action / reducer ceremony. - **Instance by arguments** With `provide`, calls with the same arguments return the same instance, while different arguments get different instances. This makes it natural to partition state by business keys. - **Deep change watching** `watch` / `useWatcher` can listen to: - Changes of model fields - Nested object property changes - Changes in nested / referenced instances - Changes of derived instances returned from getters - **React Hooks friendly** - `useModel`: create and subscribe to a model in a component - `useInstance`: subscribe to an existing instance - `useWatcher`: attach watchers in function components - `useLoader`: read global loading state and per‑method loading state - **IoC container & dependency injection** - Use `Container` / `CInjection` / `VInjection` / `config` to configure injection - Use `inject` decorator to declare dependencies in classes via schema - Support namespace isolation and `clearNamespace` cleanup ### Examples (`example/`) The `example/` directory contains runnable examples to quickly understand the APIs. Below are several key usage snippets. - **Basic counter: `useModel` / `useWatcher`** (see `example/index.tsx`) `CounterModel` shows how to create a model instance in a function component, read / update fields, and watch changes: ```tsx import { useModel, useWatcher } from "easy-model"; class CounterModel { count = 0; label: string; constructor(initial = 0, label = "Counter") { this.count = initial; this.label = label; } increment() { this.count += 1; } decrement() { this.count -= 1; } } function Counter() { const counter = useModel(CounterModel, [0, "Demo"]); useWatcher(counter, (keys, prev, next) => { console.log("changed:", keys.join("."), prev, "->", next); }); return (

{counter.label}

{counter.count}
); } ``` - **Cross‑component communication: `useModel` + `useInstance`** With `CommunicateModel` + `provide`, multiple components share the same instance (grouped by key): ```tsx import { provide, useModel, useInstance } from "easy-model"; class CommunicateModel { constructor(public name: string) {} value = 0; random() { this.value = Math.random(); } } const CommunicateProvider = provide(CommunicateModel); function CommunicateA() { const { value, random } = useModel(CommunicateModel, ["channel"]); return (
Component A: {value}
); } function CommunicateB() { const { value } = useInstance(CommunicateProvider("channel")); return
Component B: {value}
; } ``` - **Standalone watcher: `watch`** Watch an instance outside React or in plain functions, and record changes into a log list: ```tsx import { provide, watch } from "easy-model"; class WatchModel { constructor(public name: string) {} value = 0; } const WatchProvider = provide(WatchModel); const inst = WatchProvider("watch-demo"); const stop = watch(inst, (keys, prev, next) => { console.log(`${keys.join(".")}: ${prev} -> ${next}`); }); inst.value += 1; // Stop watching when it is no longer needed stop(); ``` - **Skip watching: `offWatch`** Use the `@offWatch` decorator to skip watching specific fields for better performance: ```tsx import { provide, watch, offWatch } from "easy-model"; class OffWatchModel { constructor(public name: string) {} value = 0; @offWatch internalCounter = 0; increment() { this.value += 1; this.internalCounter += 1; } } const OffWatchProvider = provide(OffWatchModel); const inst = OffWatchProvider("offwatch-demo"); const stop = watch(inst, (keys, prev, next) => { console.log(`${keys.join(".")}: ${prev} -> ${next}`); }); inst.increment(); // Only outputs value changes, internalCounter is skipped stop(); ``` - **Async loading & global loading: `loader` / `useLoader`** Decorate async methods and read global / per‑method loading states in components: ```tsx import { loader, useLoader, useModel } from "easy-model"; class LoaderModel { constructor(public name: string) {} @loader.load(true) async fetch() { return new Promise(resolve => setTimeout(() => resolve(42), 1000)); } } function LoaderDemo() { const { isGlobalLoading, isLoading } = useLoader(); const inst = useModel(LoaderModel, ["loader-demo"]); return (
Global loading: {String(isGlobalLoading)}
Current loading: {String(isLoading(inst.fetch))}
); } ``` - **Dependency injection example: `example/inject.tsx`** Demonstrates how to describe dependencies with a zod schema and inject them via the container: ```tsx import { CInjection, Container, VInjection, config, inject } from "easy-model"; import { object, number } from "zod"; const schema = object({ number: number() }).describe("schema for demo"); class Test { xxx = 1; } class MFoo { @inject(schema) bar?: { number: number }; @inject(schema2) qux?: { xxx: number }; } config( ); ``` - **Benchmark example: `example/benchmark.tsx`** A **rough performance comparison panel** for easy-model / Redux / MobX / Zustand under the same scenario, explained below. ### Tests (`test/`) The `test/` directory uses Vitest + React Testing Library to cover core behaviors: - **`provide` and instance caching** - Same arguments return the same instance - Different arguments return different instances - **Deep watch capabilities** - Listen for simple field changes - Listen for nested object property changes - Handle nested reference relationships between instances - Support getters returning instances (such as `child2`) with correct change paths ### Form utilities (`form-utils`) easy-model provides a decorator-based form field metadata toolkit that helps you declare form field configuration (validation, permission, dependencies, etc.) directly on your model class and extract it via `getProps` for rendering. #### Core API - **`@formUtils.prop(name)`**: Sets the display name for the field. - **`@formUtils.required()`**: Marks the field as required. - **`@formUtils.validate(fn)`**: Provides a validation function returning `{ valid: boolean; message?: string }`. - **`@formUtils.readonly()`**: Marks the field as read-only. - **`@formUtils.permission(code)`**: Sets a permission code. - **`@formUtils.dependsOn(fn)`**: Sets a conditional function that controls whether the field is active. - **`@formUtils.config(fieldConfig)`**: Sets UI-related configuration (type, width, options, etc.). - **`@formUtils.placeholder(text)`**: Sets placeholder text. #### Example (form-utils) ```tsx import { formUtils } from "easy-model"; class UserFormModel { @(formUtils .prop("username") .required() .validate(value => { if (typeof value !== "string" || value.length < 3) { return { valid: false, message: "Username must be at least 3 characters", }; } return { valid: true }; }) .config({ type: "input", width: "100%" }) .placeholder("Enter username")) username = ""; @(formUtils .prop("email") .required() .validate(value => { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(String(value))) { return { valid: false, message: "Please enter a valid email" }; } return { valid: true }; }) .config({ type: "input", width: "100%" }) .placeholder("Enter email")) email = ""; @(formUtils .prop("role") .permission(1) .config({ type: "select", width: "100%", getOptions: () => ["user", "moderator", "admin"], })) role = "user"; } const formProps = formUtils.getProps(UserFormModel); ``` ### History tracking (`history`) easy-model includes a lightweight history tracker that records model changes and allows undo/redo/reset operations. #### Core API - **`collect(model)`**: Creates a history manager for a model instance. - **`useModelHistory(model)`**: React hook returning the history manager. #### History manager methods - `hasPrev`: whether there is a previous history entry - `hasNext`: whether there is a next history entry - `back()`: undo to the previous state - `forward()`: redo to the next state - `reset()`: restore the model to its initial state - `batch(fn: () => void)`: group multiple changes into a single history entry - `clear()`: clear all history records #### Example (history) ```tsx import { useModel, useModelHistory } from "easy-model"; class CounterModel { count = 0; increment() { this.count += 1; } decrement() { this.count -= 1; } } function HistoryDemo() { const counter = useModel(CounterModel, []); const history = useModelHistory(counter); return (
{counter.count}
); } ``` - **IoC configuration and namespaces** - Register dependencies via `config` + `Container` + `CInjection` / `VInjection` - Use `isRegistered` to check whether a schema is registered in a namespace - Use `clearNamespace` to clear registrations in a namespace - **Hooks behavior** - `useModel` + `useInstance` share state between components and keep UI in sync - `useWatcher` listens to model changes in function components - `loader` + `useLoader` correctly reflect global / per‑method loading states ### Comparison with Redux / MobX / Zustand The following table compares easy-model with popular libraries from the perspective of **mental model / complexity / performance & capability boundaries**: | Library | Programming model | Typical cognitive load | Built‑in IoC / DI | Performance (in this repo's scenario) | | -------------- | ------------------------------- | --------------------------------------------------------------------------------- | ------------------------- | ----------------------------------------------------------- | | **easy-model** | Class model + Hooks + IoC | Write classes and methods, use a few APIs (`provide` / `useModel` / `watch` etc.) | Yes | Still **single‑digit milliseconds** in extreme bulk updates | | **Redux** | Immutable state + reducers | Requires actions / reducers / dispatch and lots of boilerplate | No | **Tens of milliseconds** in the same scenario | | **MobX** | Observable objects + decorators | Some learning cost about the reactive system and hidden dependency tracking | No (reactive but not IoC) | Faster than Redux, **teens of milliseconds** | | **Zustand** | Hook store + functional updates | Simple API, lightweight, good for local state | No | **Fastest** in this particular scenario | From the project’s perspective, easy-model features: - **Compared with Redux** - No need to split actions / reducers / selectors; business logic lives directly in model methods - Avoids heavy boilerplate; type inference is more straightforward (based on class fields and method signatures) - Automatically handles instance caching and subscriptions, without manual connect / useSelector - **Compared with MobX** - Keeps the intuitive benefits of class models, while using explicit APIs (`watch` / `useWatcher`) to expose dependencies - Dependency injection, namespaces, and cleanup are first‑class in easy-model, not extra utilities - **Compared with Zustand** - Performance is comparable (in this benchmark, easy-model is still in the single‑digit millisecond range) while providing a more complete mix of IoC / DI / deep watch - Better suited for medium‑to‑large projects where you need clear domain models and dependency relationships, not just lightweight local stores ### Benchmark scenario (rough comparison with Redux / MobX / Zustand) In `example/benchmark.tsx`, the project includes a **simple yet extreme** benchmark to roughly compare different state managers under a "mass synchronous writes" scenario. The core scenario: 1. **Initialize an array with 10,000 numbers** 2. On button click, perform **5 rounds of increment** on all elements 3. Use `performance.now()` to measure the time of **synchronous computation and state writes** 4. **Do not include React initial render time**, only focus on the time per click On a typical dev machine, one sample run (ms, single representative run) may look like: | Implementation | Time (ms) | | -------------- | --------- | | **easy-model** | ≈ 3.1 | | **Redux** | ≈ 51.5 | | **MobX** | ≈ 16.9 | | **Zustand** | ≈ 0.6 | Important notes: - This is an **intentionally extreme bulk update** scenario, meant to amplify differences in "large synchronous writes + notification" paths. - Results depend on browser / Node version, hardware, bundling mode, etc., so they are **only indicative trends**, not a rigorous benchmark. - Zustand is expected to be the fastest here, which matches its positioning as a minimal store. - easy-model is slightly slower than Zustand in this extreme case but **significantly faster than Redux / MobX**, while: - Providing class models + IoC + deep watch - Offering a structured coding experience for mid‑ to large‑scale business apps ### Links & keywords - **GitHub repo**: [`ZYF93/easy-model`](https://github.com/ZYF93/easy-model) - **npm package**: [`@e7w/easy-model`](https://www.npmjs.com/package/@e7w/easy-model) - **Keywords**: `react` / `state management` / `state manager` / `ioc` / `dependency injection` / `class model` / `react hooks` / `watcher` / `loader` / `typescript` ### When to use it easy-model works best when: - You have clear domain models and want classes to hold both data and behavior - You need convenient dependency injection for repositories, services, configs, schemas, etc. - You often need to **watch models / nested fields** for changes - You want performance close to light‑weight state libraries without sacrificing structure and maintainability If you are using Redux / MobX / Zustand today and you want to: - Reduce mental overhead and boilerplate - Get a more natural class model + IoC experience - Avoid a noticeable performance hit …then you can start by migrating a subset of modules to easy-model and run the benchmark in `example/benchmark.tsx` on your own machine and data scale. ### Further documentation (`docs/`) This repo also provides more **practical, engineering‑oriented** Chinese documentation, recommended for real‑world projects: - [GUIDE (CN)](./docs/GUIDE.md): Best‑practice guide covering design concepts, model design, React integration, loader / watcher usage, etc. - [ARCHITECTURE (CN)](./docs/ARCHITECTURE.md): How to organize directories and layering around easy-model in a real project. - [COOKBOOK (CN)](./docs/COOKBOOK.md): Scenario‑based recipes (forms, lists, global user, notification center, etc.). - [FAQ (CN)](./docs/FAQ.md): Frequently asked questions and troubleshooting ideas. English versions of these docs are also available: - [GUIDE (EN)](./docs/GUIDE.en.md) - [ARCHITECTURE (EN)](./docs/ARCHITECTURE.en.md) - [COOKBOOK (EN)](./docs/COOKBOOK.en.md) - [FAQ (EN)](./docs/FAQ.en.md)