@eficy/reactive
v1.2.3
Published
A modern annotation-based reactive state management library with MobX-compatible API, powered by @preact/signals-core
Maintainers
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: '[email protected]',
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: '[email protected]' });📚 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 two-way binding
Signal API
Signals use .value property for reading and writing:
const count = signal(0);
// Reading values
count.value; // Property style (recommended)
// Writing values
count.value = 5; // Property style (recommended)
// Functional update
count.value = count.value + 1;Two-way Binding Helper
import { signal, bind } from '@eficy/reactive';
const name = signal('');
const checked = signal(false);
// bind() returns { value, onChange } for standard React components
<input {...bind(name)} />
<input type="checkbox" {...bind(checked)} />
// Custom keys:
<CustomInput {...bind(value, { valueKey: 'selected', eventKey: 'onSelect' })} />Note:
bind()returns{ value: signal.value, onChange }- a standard props object that works with any React component. Thevalueis the current signal value (not the signal itself).
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.value; // Read signal to create dependency
});
expect(effectRuns).toBe(1);
count.value = 5;
expect(effectRuns).toBe(2);Signal 的 value 用法(不消费事件)
import { signal } from '@eficy/reactive';
const count = signal(0);
// 直接设置值
count.value = 1;
// 或使用函数式更新
count.value = count.value + 1;
// 表单事件中请显式取值(不会自动从事件中读取 value/checked)
// input 文本框
const text = signal('');
// onChange={(e) => text.value = e.target.value}
// checkbox
const checked = signal(false);
// onChange={(e) => checked.value = 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
