UNPKG

mobx-vue-bridge

Version:

A lightweight bridge for seamless two-way data binding between MobX observables and Vue 3 reactivity

332 lines (251 loc) โ€ข 8.26 kB
# ๐ŸŒ‰ MobX-Vue Bridge A seamless bridge between MobX observables and Vue 3's reactivity system, enabling effortless two-way data binding and state synchronization. [![npm version](https://badge.fury.io/js/mobx-vue-bridge.svg)](https://www.npmjs.com/package/mobx-vue-bridge) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) ## โœจ Features - ๐Ÿ”„ **Two-way data binding** between MobX observables and Vue reactive state - ๐ŸŽฏ **Automatic property detection** (properties, getters, setters, methods) - ๐Ÿ—๏ธ **Deep object/array observation** with proper reactivity - โš™๏ธ **Configurable mutation behavior** - ๐Ÿ”’ **Type-safe bridging** between reactive systems - ๐Ÿš€ **Optimized performance** with intelligent change detection - ๐Ÿ›ก๏ธ **Error handling** for edge cases and circular references ## ๐Ÿ“ฆ Installation ```bash npm install mobx-vue-bridge ``` **Peer Dependencies:** - Vue 3.x - MobX 6.x ```bash npm install vue mobx ``` ## ๐Ÿš€ Quick Start First, create your MobX presenter: ```javascript // presenters/UserPresenter.js import { makeAutoObservable } from 'mobx' export class UserPresenter { constructor() { this.name = 'John' this.age = 25 this.emails = [] makeAutoObservable(this) } get displayName() { return `${this.name} (${this.age})` } addEmail(email) { this.emails.push(email) } } ``` Then use it in your Vue component: **Option 1: Modern `<script setup>` syntax (recommended)** ```vue <script setup> import { useMobxBridge } from 'mobx-vue-bridge' import { UserPresenter } from './presenters/UserPresenter.js' const userPresenter = new UserPresenter() // Bridge MobX observable to Vue reactive state const state = useMobxBridge(userPresenter) </script> ``` **Option 2: Traditional Composition API** ```vue <script> import { useMobxBridge } from 'mobx-vue-bridge' import { UserPresenter } from './presenters/UserPresenter.js' export default { setup() { const userPresenter = new UserPresenter() // Bridge MobX observable to Vue reactive state const state = useMobxBridge(userPresenter) return { state } } } </script> ``` **Template usage:** ```html <template> <div> <!-- Two-way binding works seamlessly --> <input v-model="state.name" /> <input v-model.number="state.age" /> <!-- Computed properties are reactive --> <p>{{ state.displayName }}</p> <!-- Methods are properly bound --> <button @click="state.addEmail('new@email.com')"> Add Email </button> <!-- Arrays/objects are deeply reactive --> <ul> <li v-for="email in state.emails" :key="email"> {{ email }} </li> </ul> </div> </template> ``` ## ๐Ÿ“š API Reference ### `useMobxBridge(mobxObject, options?)` Bridges a MobX observable object with Vue's reactivity system. **Parameters:** - `mobxObject` - The MobX observable object to bridge - `options` - Configuration options (optional) **Options:** - `allowDirectMutation` (boolean, default: `true`) - Whether to allow direct mutation of properties **Returns:** Vue reactive state object ```javascript // With configuration const state = useMobxBridge(store, { allowDirectMutation: false // Prevents direct mutations }) ``` ### `usePresenterState(presenter, options?)` Alias for `useMobxBridge` - commonly used with presenter pattern. ```javascript const state = usePresenterState(presenter, options) ``` ## ๐ŸŽฏ Use Cases ### Presenter Pattern ```javascript class TodoPresenter { constructor(todoService) { this.todoService = todoService this.todos = [] this.filter = 'all' this.loading = false makeAutoObservable(this) } get filteredTodos() { switch (this.filter) { case 'active': return this.todos.filter(t => !t.completed) case 'completed': return this.todos.filter(t => t.completed) default: return this.todos } } async loadTodos() { this.loading = true try { this.todos = await this.todoService.fetchTodos() } finally { this.loading = false } } } // In component const presenter = new TodoPresenter(todoService) const state = usePresenterState(presenter) ``` ### Store Integration ```javascript // MobX store class AppStore { constructor() { this.user = null this.theme = 'light' this.notifications = [] makeAutoObservable(this) } get isAuthenticated() { return !!this.user } setTheme(theme) { this.theme = theme } } // Bridge in component const state = useMobxBridge(appStore) ``` ## ๐Ÿ”ง Advanced Features ### Configuration Options The bridge accepts an optional configuration object to customize its behavior: ```javascript const state = useMobxBridge(mobxObject, { allowDirectMutation: true // default: true }) ``` #### `allowDirectMutation` (boolean) Controls whether direct mutations are allowed on the Vue state: - `true` (default): Allows `state.name = 'New Name'` - `false`: Mutations must go through MobX actions ```javascript // Allow direct mutations (default) const state = useMobxBridge(presenter, { allowDirectMutation: true }) state.name = 'John' // โœ… Works // Disable direct mutations (action-only mode) const state = useMobxBridge(presenter, { allowDirectMutation: false }) state.name = 'John' // โŒ Warning: use actions instead presenter.setName('John') // โœ… Works ``` - โœ… You can use `await nextTick()` when needed for immediate reads ### Deep Reactivity The bridge automatically handles deep changes in objects and arrays: ```javascript // These mutations are automatically synced state.user.profile.name = 'New Name' // Object mutation state.todos.push(newTodo) // Array mutation state.settings.colors[0] = '#FF0000' // Nested array mutation ``` **Note on Async Behavior:** Nested mutations (via the deep proxy) are batched using `queueMicrotask()` to prevent corruption during array operations like `shift()`, `unshift()`, and `splice()`. This ensures data correctness. If you need immediate access to updated values after nested mutations in the same function, use Vue's `nextTick()`: ```javascript import { nextTick } from 'vue' state.items.push(newItem) await nextTick() // Wait for batched update to complete console.log(state.items) // Now updated ``` **However, Vue templates, computed properties, and watchers work automatically without `nextTick()`:** ```vue <template> <!-- Auto-updates, no nextTick needed --> <div>{{ state.items.length }}</div> </template> <script setup> // Computed auto-updates, no nextTick needed const itemCount = computed(() => state.items.length) // Watcher auto-fires, no nextTick needed watch(() => state.items, (newItems) => { console.log('Items changed:', newItems) }) </script> ``` Top-level property assignments are synchronous: ```javascript state.count = 42 // Immediate (sync) state.items = [1, 2, 3] // Immediate (sync) state.items.push(4) // Batched (async - requires nextTick for immediate read) ``` **Best Practice:** Keep business logic in your MobX Presenter. When you mutate via the Presenter, everything is synchronous: ```javascript // โœ… Presenter pattern - always synchronous, no nextTick needed presenter.items.push(newItem) console.log(presenter.items) // Immediately updated! ``` ### Error Handling The bridge gracefully handles edge cases: - Uninitialized computed properties - Circular references - Failed setter operations - Missing dependencies ### Performance Optimization - Intelligent change detection prevents unnecessary updates - Efficient shallow/deep equality checks - Minimal overhead for large object graphs ## ๐Ÿงช Testing ```bash npm test # Run tests npm run test:watch # Watch mode npm run test:coverage # Coverage report ``` ## ๐Ÿค Contributing Contributions are welcome! Please feel free to submit a Pull Request. ## ๐Ÿ“„ License MIT ยฉ [Visar Uruqi](https://github.com/visaruruqi) ## ๐Ÿ”— Links - [GitHub Repository](https://github.com/visaruruqi/mobx-vue-bridge) - [NPM Package](https://www.npmjs.com/package/mobx-vue-bridge) - [Issues](https://github.com/visaruruqi/mobx-vue-bridge/issues)