Skip to content

Commit

Permalink
feat: implement testing and reactivity adapter for MobX
Browse files Browse the repository at this point in the history
resolves #14
  • Loading branch information
maxnowack committed Oct 4, 2023
1 parent 9c7e5c6 commit b6b45a8
Show file tree
Hide file tree
Showing 9 changed files with 126 additions and 15 deletions.
43 changes: 43 additions & 0 deletions __tests__/Reactivity.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ import {
} from '@preact/signals-core'
import { reactive as reactively, onCleanup as reactivelyOnCleanup } from '@reactively/core'
import S from 's-js'
import {
observable as mobxObservable,
autorun as mobxAutorun,
runInAction as mobxRunInAction,
onBecomeUnobserved as mobxOnBecomeUnobserved,
} from 'mobx'
// import {
// createSignal as solidSignal,
// createEffect as solidEffect,
Expand Down Expand Up @@ -338,6 +344,43 @@ describe('Reactivity', () => {
})
})

describe('MobX', () => {
const reactivity = createReactivityAdapter({
create: () => {
const dep = mobxObservable({ count: 0 })
return {
depend: () => {
// eslint-disable-next-line no-unused-expressions
dep.count
},
notify: () => {
mobxRunInAction(() => {
dep.count += 1
})
},
raw: dep,
}
},
onDispose(callback, { raw: dep }) {
mobxOnBecomeUnobserved(dep, 'count', callback)
},
})

it('should be reactive with MobX', () => {
const collection = new Collection({ reactivity })
const callback = vi.fn()

const stop = mobxAutorun(() => {
const cursor = collection.find({ name: 'John' })
callback(cursor.count())
})
collection.insert({ id: '1', name: 'John' })
expect(callback).toHaveBeenCalledTimes(2)
expect(callback).toHaveBeenLastCalledWith(1)
stop()
})
})

// Testing angular is a bit tricky and I haven't figured out how to do it yet. Feel free to open a PR
// eslint-disable-next-line vitest/no-commented-out-tests
// describe('angular', () => {
Expand Down
1 change: 1 addition & 0 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export default defineConfig({
{ text: '@preact/signals-core', link: '/reactivity/preact-signals/' },
{ text: 'Solid Signals', link: '/reactivity/solidjs/' },
{ text: 'Angular', link: '/reactivity/angular/' },
{ text: 'MobX', link: '/reactivity/mobx/' },
{ text: 'Maverick-js Signals', link: '/reactivity/maverickjs/' },
{ text: 'Meteor Tracker', link: '/reactivity/meteor-tracker/' },
{ text: 'oby', link: '/reactivity/oby/' },
Expand Down
52 changes: 52 additions & 0 deletions docs/reactivity/mobx/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
head:
- - link
- rel: canonical
href: https://signaldb.js.org/reactivity/mobx/
---
# Reactivity adapter for [`MobX`](https://mobx.js.org/)

Integrating signals (or observables) in MobX with signaldb has revolutionized the way state management operates in modern applications. MobX, renowned for its effortless state management capabilities, transparently implements functional reactive programming. Through the use of a MobX reactivity adapter for signaldb, developers can harness the powerful reactivity mechanisms of MobX within the database context of signaldb. When you marry the dynamic, auto-updating nature of MobX observables with signaldb collections, you cultivate an environment where state and data persistency harmoniously coexist. Especially in scenarios where real-time data updates are crucial, this combination ensures that all dependent parts of your codebase are notified and updated whenever underlying data changes. Furthermore, the adaptability of MobX means that it seamlessly meshes with various frameworks, including the versatile signaldb. As the landscape of web development shifts towards more reactive paradigms, integrating MobX with signaldb emerges as a compelling solution for developers seeking efficient, boilerplate-free state management synchronized with a dependable database. If your framework still lacks a reactivity adapter, consider the value addition of integrating MobX and its unparalleled reactivity system into your tech stack.

## Adapter

```js
import { observable, runInAction, onBecomeUnobserved } from 'mobx'
import { createReactivityAdapter } from 'signaldb'

const reactivityAdapter = createReactivityAdapter({
create: () => {
const dep = observable({ count: 0 })
return {
depend: () => {
dep.count
},
notify: () => {
runInAction(() => {
dep.count += 1
})
},
raw: dep,
}
},
onDispose(callback, { raw: dep }) {
onBecomeUnobserved(dep, 'count', callback)
},
})
```

## Usage

```js
import { Collection } from 'signaldb'
import { autorun } from 'mobx'

const posts = new Collection({
reactivity: reactivityAdapter,
})

autorun(() => {
const cursor = posts.find({ author: 'John' })
console.log(cursor.count())
})
```
10 changes: 6 additions & 4 deletions docs/reactivity/other/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ The ReactivityAdapter object has two methods:

### `create() -> Dependency`

The `create` function creates a new reactive dependency. A `Dependency` object has two methods:
The `create` function creates a new reactive dependency. A `Dependency` object must have at least these two methods:

* `depend()`: This method is called when the collection data is read, marking the place in the code as dependent on the collection data. Subsequent changes to the collection data will cause this place to be re-evaluated.
* `notify()`: This method is called when the collection data changes, notifying all dependent parts of the code that they need to re-evaluate.

You can also include more methods or other data in the dependency which you can access from the [`onDispose`](/reactivity/other/#ondispose-callback-void-dependency-dependency) method.

```js
create: () => {
const dep = signal(0)
Expand All @@ -35,12 +37,12 @@ create: () => {
```


### `onDispose(callback: () -> void)`
### `onDispose(callback: () -> void, dependency: Dependency)`

This method is used to register a callback to be executed when the reactive computation is disposed.
This method is used to register a callback to be executed when the reactive computation is disposed. The dependency created in the [`create`](/reactivity/other/#create-dependency) method, will be passed as the second parameter. This can be useful if a framework requires data from the creation on disposal.

```js
onDispose: (callback) => {
onDispose: (callback, dependency: Dependency) => {
onDispose(callback)
}
```
Expand Down
12 changes: 11 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
},
"dependencies": {
"fast-sort": "^3.4.0",
"mingo": "^6.4.4"
"mingo": "^6.4.4",
"mobx": "^6.10.2"
}
}
7 changes: 3 additions & 4 deletions src/Collection/Cursor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,6 @@ export default class Cursor<T extends BaseItem, U = T> {
this.getAllItems = getAllItems
this.selector = selector
this.options = options || {}

if (this.options.reactive && this.options.reactive.onDispose) {
this.options.reactive.onDispose(this.cleanup.bind(this))
}
}

private filterItems(items: T[]) {
Expand Down Expand Up @@ -75,6 +71,9 @@ export default class Cursor<T extends BaseItem, U = T> {
.map(([key]) => key)
.reduce((memo, key) => ({ ...memo, [key]: notify }), {})
const stop = this.observeChanges(enabledEvents, true)
if (this.options.reactive.onDispose) {
this.options.reactive.onDispose(() => stop(), signal)
}
this.onCleanup(stop)
}

Expand Down
5 changes: 4 additions & 1 deletion src/createReactivityAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import type ReactivityAdapter from './types/ReactivityAdapter'
import type { Signal } from './types/ReactivityAdapter'

export default function createReactivityAdapter(definition: ReactivityAdapter) {
export default function createReactivityAdapter<T extends Signal = Signal>(
definition: ReactivityAdapter<T>,
) {
return definition
}
8 changes: 4 additions & 4 deletions src/types/ReactivityAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
interface Signal {
export interface Signal {
depend(): void,
notify(): void,
}

export default interface ReactivityAdapter {
create(): Signal,
onDispose?(callback: () => void): void,
export default interface ReactivityAdapter<T extends Signal = Signal> {
create(): T,
onDispose?(callback: () => void, signal: T): void,
isInScope?(): boolean,
}

0 comments on commit b6b45a8

Please sign in to comment.