A seamless bridge between MobX observables and Vue 3's reactivity system, enabling effortless two-way data binding and state synchronization.
- π 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
npm install mobx-vue-bridge
Peer Dependencies:
- Vue 3.x
- MobX 6.x
npm install vue mobx
First, create your MobX presenter:
// 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)
<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
<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:
<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>
Bridges a MobX observable object with Vue's reactivity system.
Parameters:
mobxObject
- The MobX observable object to bridgeoptions
- Configuration options (optional)
Options:
allowDirectMutation
(boolean, default:true
) - Whether to allow direct mutation of properties
Returns: Vue reactive state object
// With configuration
const state = useMobxBridge(store, {
allowDirectMutation: false // Prevents direct mutations
})
Alias for useMobxBridge
- commonly used with presenter pattern.
const state = usePresenterState(presenter, options)
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)
// 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)
The bridge accepts an optional configuration object to customize its behavior:
const state = useMobxBridge(mobxObject, {
allowDirectMutation: true // default: true
})
Controls whether direct mutations are allowed on the Vue state:
true
(default): Allowsstate.name = 'New Name'
false
: Mutations must go through MobX actions
// 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
The bridge automatically handles deep changes in objects and arrays:
// 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()
:
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()
:
<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:
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:
// β
Presenter pattern - always synchronous, no nextTick needed
presenter.items.push(newItem)
console.log(presenter.items) // Immediately updated!
The bridge gracefully handles edge cases:
- Uninitialized computed properties
- Circular references
- Failed setter operations
- Missing dependencies
- Intelligent change detection prevents unnecessary updates
- Efficient shallow/deep equality checks
- Minimal overhead for large object graphs
npm test # Run tests
npm run test:watch # Watch mode
npm run test:coverage # Coverage report
Contributions are welcome! Please feel free to submit a Pull Request.
MIT Β© Visar Uruqi