MobX-utils
Utility functions and common patterns for MobX
This package provides utility functions and common MobX patterns build on top of MobX.
It is encouraged to take a peek under the hood and read the sources of these utilities.
Feel free to open a PR with your own utilities. For large new features, please open an issue first.
Installation
NPM: npm install mobx-utils --save
CDN: https://unpkg.com/mobx-utils/mobx-utils.umd.js
API
fromPromise
lib/from-promise.js:103-107
fromPromise
takes a Promise and returns an object with 3 observable properties that track
the status of the promise. The returned object has the following observable properties:
value
: either the initial value, the value the Promise resolved to, or the value the Promise was rejected with. use .state
if you need to be able to tell the differencestate
: one of "pending"
, "fulfilled"
or "rejected"
promise
: (not observable) the original promise object
and the following method:case({fulfilled, rejected, pending})
: maps over the result using the provided handlers, or returns undefined
if a handler isn't available for the current promise state.
Parameters
promise
IThenable<T> The promise which will be observedinitialValue
[T] Optional predefined initial value (optional, default undefined
)modifier
[any] MobX modifier, e.g. asFlat
, to be applied to the resolved value (optional, default IDENTITY
)
Examples
const fetchResult = fromPromise(fetch("http://someurl"))
when(
() => fetchResult.state !== "pending"
() => {
console.log("Got ", fetchResult.value)
}
)
const myComponent = observer(({ fetchResult }) => {
switch(fetchResult.state) {
case "pending": return <div>Loading...</div>
case "rejected": return <div>Ooops... {fetchResult.value}</div>
case "fulfilled": return <div>Gotcha: {fetchResult.value}</div>
}
})
const myComponent = observer(({ fetchResult }) =>
fetchResult.case({
pending: () => <div>Loading...</div>
rejected: error => <div>Ooops.. {error}</div>
fulfilled: value => <div>Gotcha: {value}</div>
}))
Note that the status strings are available as constants:
`mobxUtils.PENDING`, `mobxUtils.REJECTED`, `mobxUtil.FULFILLED`
Returns IPromiseBasedObservable<T>
lazyObservable
lib/lazy-observable.js:33-49
lazyObservable
creates an observable around a fetch
method that will not be invoked
util the observable is needed the first time.
The fetch method receives a sink
callback which can be used to replace the
current value of the lazyObservable. It is allowed to call sink
multiple times
to keep the lazyObservable up to date with some external resource.
Note that it is the current()
call itself which is being tracked by MobX,
so make sure that you don't dereference to early.
Parameters
fetch
initialValue
[T] optional initialValue that will be returned from current
as long as the sink
has not been called at least once (optional, default undefined
)modifier
[any] optional mobx modifier that determines the the comparison and recursion strategy of the observable, for example asFlat
or asStructure
(optional, default IDENTITY
)
Examples
const userProfile = lazyObservable(
sink => fetch("/myprofile").then(profile => sink(profile))
)
const Profile = observer(({ userProfile }) =>
userProfile.current() === undefined
? <div>Loading user profile...</div>
: <div>{userProfile.current().displayName}</div>
)
fromResource
lib/from-resource.js:67-101
fromResource
creates an observable which current state can be inspected using .current()
,
and which can be kept in sync with some external datasource that can be subscribed to.
The created observable will only subscribe to the datasource if it is in use somewhere,
(un)subscribing when needed. To enable fromResource
to do that two callbacks need to be provided,
one to subscribe, and one to unsubscribe. The subscribe callback itself will receive a sink
callback, which can be used
to update the current state of the observable, allowing observes to react.
Whatever is passed to sink
will be returned by current()
. The values passed to the sink will not be converted to
observables automatically, but feel free to do so.
It is the current()
call itself which is being tracked,
so make sure that you don't dereference to early.
For inspiration, an example integration with the apollo-client on github
The following example code creates an observable that connects to a dbUserRecord
,
which comes from an imaginary database and notifies when it has changed.
Parameters
subscriber
unsubscriber
[IDisposer] (optional, default NOOP
)initialValue
[T] the data that will be returned by get()
until the sink
has emitted its first data (optional, default undefined
)
Examples
function createObservableUser(dbUserRecord) {
let currentSubscription;
return fromResource(
(sink) => {
sink(dbUserRecord.fields)
currentSubscription = dbUserRecord.onUpdated(() => {
sink(dbUserRecord.fields)
})
},
() => {
dbUserRecord.unsubscribe(currentSubscription)
}
)
}
const myUserObservable = createObservableUser(myDatabaseConnector.query("name = 'Michel'"))
autorun(() => {
console.log(myUserObservable.current().displayName)
})
const userComponent = observer(({ user }) =>
<div>{user.current().displayName}</div>
)
createViewModel
lib/create-view-model.js:134-136
createViewModel
takes an object with observable properties (model)
and wraps a viewmodel around it. The viewmodel proxies all enumerable property of the original model with the following behavior:
- as long as no new value has been assigned to the viewmodel property, the original property will be returned.
- any future change in the model will be visible in the viewmodel as well unless the viewmodel property was dirty at the time of the attempted change.
- once a new value has been assigned to a property of the viewmodel, that value will be returned during a read of that property in the future. However, the original model remain untouched until
submit()
is called.
The viewmodel exposes the following additional methods, besides all the enumerable properties of the model:
submit()
: copies all the values of the viewmodel to the model and resets the statereset()
: resets the state of the view model, abandoning all local modificatoinsisDirty
: observable property indicating if the viewModel contains any modificationsisPropertyDirty(propName)
: returns true if the specified property is dirtymodel
: The original model object for which this viewModel was created
You may use observable arrays, maps and objects with createViewModel
but keep in mind to assign fresh instances of those to the viewmodel's properties, otherwise you would end up modifying the properties of the original model.
Note that if you read a non-dirty property, viewmodel only proxies the read to the model. You therefore need to assign a fresh instance not only the first time you make the assignment but also after calling reset()
or submit()
.
Parameters
Examples
class Todo {
\@observable title = "Test"
}
const model = new Todo()
const viewModel = createViewModel(model);
autorun(() => console.log(viewModel.model.title, ",", viewModel.title))
model.title = "Get coffee"
viewModel.title = "Get tea"
viewModel.submit()
viewModel.title = "Get cookie"
viewModel.reset()
whenWithTimeout
lib/guarded-when.js:32-51
Like normal when
, except that this when
will automatically dispose if the condition isn't met within a certain amount of time.
Parameters
expr
action
timeout
[number] maximum amount when spends waiting before giving up (optional, default 10000
)onTimeout
[any] the ontimeout handler will be called if the condition wasn't met within the given time (optional, default ()
)
Examples
test("expect store to load", t => {
const store = {
items: [],
loaded: false
}
fetchDataForStore((data) => {
store.items = data;
store.loaded = true;
})
whenWithTimeout(
() => store.loaded
() => t.end()
2000,
() => t.fail("store didn't load with 2 secs")
)
})
Returns IDisposer disposer function that can be used to cancel the when prematurely. Neither action or onTimeout will be fired if disposed
keepAlive
lib/keep-alive.js:31-36
MobX normally suspends any computed value that is not in use by any reaction,
and lazily re-evaluates the expression if needed outside a reaction while not in use.
keepAlive
marks a computed value as always in use, meaning that it will always fresh, but never disposed automatically.
Parameters
target
Object an object that has a computed property, created by @computed
or extendObservable
property
string the name of the property to keep alive_1
_2
Examples
const obj = observable({
number: 3,
doubler: function() { return this.number * 2 }
})
const stop = keepAlive(obj, "doubler")
Returns IDisposer stops this keep alive so that the computed value goes back to normal behavior
keepAlive
lib/keep-alive.js:31-36
Parameters
computedValue
IComputedValue<any> created using the computed
function_1
_2
Examples
const number = observable(3)
const doubler = computed(() => number.get() * 2)
const stop = keepAlive(doubler)
stop()
Returns IDisposer stops this keep alive so that the computed value goes back to normal behavior
queueProcessor
lib/queue-processor.js:22-40
queueProcessor
takes an observable array, observes it and calls processor
once for each item added to the observable array, optionally deboucing the action
Parameters
observableArray
Array<T> observable array instance to trackprocessor
debounce
[number] optional debounce time in ms. With debounce 0 the processor will run synchronously (optional, default 0
)
Examples
const pendingNotifications = observable([])
const stop = queueProcessor(pendingNotifications, msg => {
new Notification(msg);
})
pendingNotifications.push("test!")
Returns IDisposer stops the processor