Package Exports
- @eficy/reactive
- @eficy/reactive/dist/index.js
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@eficy/reactive) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
@eficy/reactive
A modern annotation-based reactive state management library with MobX-compatible API, powered by @preact/signals-core for high-performance reactivity.
🚀 Key Features
- 🎯 Signal-Based: High-performance reactivity powered by
@preact/signals-core - 📝 MobX-Compatible API: Familiar annotations and patterns from MobX
- ⚡ Automatic Batching: Actions automatically batch state updates
- 📦 Type-Safe: Full TypeScript support with excellent type inference
- 🔄 No Proxy: Better compatibility without Proxy dependency
- 🎨 Flexible Design: Supports arrays, objects and complex state structures
- 🧪 Well Tested: >90% test coverage with comprehensive unit tests
- 🎯 Decorator Support: TypeScript decorators for class-based reactive programming
📦 Installation
npm install @eficy/reactive reflect-metadata
# or
yarn add @eficy/reactive reflect-metadata
# or
pnpm add @eficy/reactive reflect-metadataNote: reflect-metadata is required for decorator support.
🚀 Quick Start
Function-based API (Recommended)
import { observable, computed, effect, action } from '@eficy/reactive';
// Create reactive state
const userStore = observable({
firstName: 'John',
lastName: 'Doe',
age: 25,
});
// Create computed values
const fullName = computed(() => `${userStore.get('firstName')} ${userStore.get('lastName')}`);
const isAdult = computed(() => userStore.get('age') >= 18);
// Auto-run effects
effect(() => {
console.log(`User: ${fullName()}, Adult: ${isAdult()}`);
});
// Create actions (MobX-style)
const updateUser = action((first: string, last: string, age: number) => {
userStore.set('firstName', first);
userStore.set('lastName', last);
userStore.set('age', age);
});
// Trigger updates
updateUser('Jane', 'Smith', 30); // Output: User: Jane Smith, Adult: trueDecorator-based API (Class Style)
For TypeScript projects with decorator support, you can use the class-based API:
import 'reflect-metadata';
import { Observable, Computed, Action, makeObservable, ObservableClass } from '@eficy/reactive/annotation';
// Option 1: Manual makeObservable
class UserStore {
@Observable
firstName = 'John';
@Observable
lastName = 'Doe';
@Observable
age = 25;
@Computed
get fullName(): string {
return `${this.firstName} ${this.lastName}`;
}
@Computed
get isAdult(): boolean {
return this.age >= 18;
}
@Action
updateUser(first: string, last: string, age: number) {
this.firstName = first;
this.lastName = last;
this.age = age;
}
constructor() {
makeObservable(this);
}
}
// Option 2: ObservableClass base class (auto makeObservable)
class UserStore extends ObservableClass {
@Observable
firstName = 'John';
@Observable
lastName = 'Doe';
@Observable
age = 25;
@Computed
get fullName(): string {
return `${this.firstName} ${this.lastName}`;
}
@Computed
get isAdult(): boolean {
return this.age >= 18;
}
@Action
updateUser(first: string, last: string, age: number) {
this.firstName = first;
this.lastName = last;
this.age = age;
}
}
// Usage
const store = new UserStore();
effect(() => {
console.log(`User: ${store.fullName}, Adult: ${store.isAdult}`);
});
store.updateUser('Jane', 'Smith', 30);Decorator Configuration
To use decorators, ensure your TypeScript configuration supports them:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}If using Vite or other build tools, you may need additional configuration for decorator support.
Observable Arrays (MobX-Compatible)
import { observableArray, action } from '@eficy/reactive';
const todos = observableArray<string>(['Learn', 'Work']);
// Array operations automatically trigger updates
const addTodo = action((todo: string) => {
todos.push(todo);
});
const removeTodo = action((index: number) => {
todos.splice(index, 1);
});
// Watch array changes
effect(() => {
console.log('Todos:', todos.toArray());
console.log('Count:', todos.length);
});
addTodo('Exercise'); // Automatically triggers updatesObservable Objects (MobX-Compatible)
import { observableObject } from '@eficy/reactive';
const user = observableObject({
name: 'John',
email: 'john@example.com',
preferences: {
theme: 'dark',
notifications: true,
},
});
// Reactive updates
effect(() => {
console.log(`${user.get('name')} (${user.get('email')})`);
});
// Update nested properties
user.set('name', 'Jane');
user.update({ email: 'jane@example.com' });📚 API Reference
Core Functions
signal(initialValue)- Create a reactive signalcomputed(fn)- Create a computed value that automatically updateseffect(fn)- Create a side effect that runs when dependencies changeaction(fn)- Wrap function to batch updates and improve performancebatch(fn)- Manually batch multiple updateswatch(signal, callback)- Watch for signal changesbind(signal, options?)- Create{ value$, onChange }props for reactive two-way binding ($ suffix protocol)
Signal API
Signals support multiple ways to read and write values:
const count = signal(0);
// Reading values
count(); // Call style (recommended)
count.get(); // Method style (LLM-friendly)
count.value; // Property style (familiar to Vue/Svelte users)
// Writing values
count(5); // Call style
count.set(5); // Method style (recommended)
count.value = 5; // Property style
// Functional update
count((c) => c + 1);
count.set((c) => c + 1);Two-way Binding Helper
import { signal, bind } from '@eficy/reactive';
const name = signal('');
const checked = signal(false);
// In JSX with @eficy/core-jsx:
// bind() generates value$ prop for reactive protocol
<input {...bind(name)} />
<input type="checkbox" {...bind(checked)} />
// Custom keys (generates selected$ instead of value$):
<CustomInput {...bind(value, { valueKey: 'selected', eventKey: 'onSelect' })} />Note:
bind()returns{ value$: signal, onChange }to work with Eficy's $ suffix reactive protocol. Thevalue$prop triggers reactive wrapping in the JSX runtime.
Observable Creation
observable(value)- Auto-detect type and create appropriate observableobservable.box(value)- Create observable primitive (signal)observable.object(obj)- Create observable objectobservable.array(arr)- Create observable arrayobservable.map(map)- Create observable Mapobservable.set(set)- Create observable Set
Decorators (from '@eficy/reactive/annotation')
@Observable- Mark class property as observable@Observable(initialValue)- Mark property as observable with initial value@Computed- Mark getter as computed property@Action- Mark method as action@Action('name')- Mark method as action with custom namemakeObservable(instance)- Apply decorators to class instanceObservableClass- Base class that auto-applies makeObservable
Collections
observableArray<T>(items?)- Reactive array with MobX-compatible APIobservableObject<T>(obj)- Reactive object with get/set methodsobservableMap<K,V>(entries?)- Reactive MapobservableSet<T>(values?)- Reactive Set
🎯 Migration from MobX
This library is designed to be largely compatible with MobX patterns:
// MobX style
import { Observable, Computed, Action, makeObservable } from 'mobx';
// @eficy/reactive style (very similar!)
import { Observable, Computed, Action, makeObservable } from '@eficy/reactive/annotation';Key differences:
- Uses
@preact/signals-coreinstead of Proxy-based reactivity - Requires
reflect-metadatafor decorators - Function-based API available as alternative to class-based
- Some advanced MobX features may not be available
⚡ Performance Tips
- Use actions for batching: Wrap multiple state updates in
action()for better performance - Computed caching: Computed values are automatically cached and only recalculate when dependencies change
- Selective observation: Only observe the data you actually need in components
- Avoid creating observables in render: Create observables outside render functions
🧪 Testing
import { signal, effect } from '@eficy/reactive';
// Test reactive behavior
const count = signal(0);
let effectRuns = 0;
effect(() => {
effectRuns++;
count(); // Read signal to create dependency
});
expect(effectRuns).toBe(1);
count(5);
expect(effectRuns).toBe(2);Signal 的 set 用法(不消费事件)
import { signal } from '@eficy/reactive';
const count = signal(0);
// 直接设置值
count.set(1);
// 或使用函数式更新
count.set((prev) => prev + 1);
// 也可以使用调用风格(与 set 等价)
count(5);
count((prev) => prev + 1);
// 表单事件中请显式取值(不会自动从事件中读取 value/checked)
// input 文本框
const text = signal('');
// onChange={(e) => text.set(e.target.value)}
// checkbox
const checked = signal(false);
// onChange={(e) => checked.set(e.target.checked)}📝 TypeScript Support
This library is written in TypeScript and provides excellent type inference:
// Types are automatically inferred
const user = observable({
name: 'John', // string
age: 25, // number
active: true, // boolean
});
// TypeScript knows the return type
const greeting = computed(() => {
return `Hello, ${user.get('name')}!`; // string
});🔗 Ecosystem
- @eficy/reactive-react - React bindings for @eficy/reactive
- @eficy/core - UI framework using @eficy/reactive
📜 License
MIT License - see LICENSE file for details.
🤝 Contributing
Contributions welcome! Please read our contributing guidelines and submit pull requests to our repository.
Made with ❤️ by the Eficy team