Code Monkey home page Code Monkey logo

mobx-utils's Introduction

MobX-utils

Utility functions and common patterns for MobX

Build Status Coverage Status Join the chat at https://gitter.im/mobxjs/mobx npm

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 & Usage

NPM: npm install mobx-utils --save

CDN: https://unpkg.com/mobx-utils/mobx-utils.umd.js

import {function_name} from 'mobx-utils'

API

Table of Contents

fromPromise

fromPromise takes a Promise, extends it with 2 observable properties that track the status of the promise and returns it. 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 difference.
  • state: one of "pending", "fulfilled" or "rejected"

And the following methods:

  • 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.

The returned object implements PromiseLike<TValue>, so you can chain additional Promise handlers using then. You may also use it with await in async functions.

Note that the status strings are available as constants: mobxUtils.PENDING, mobxUtils.REJECTED, mobxUtil.FULFILLED

fromPromise takes an optional second argument, a previously created fromPromise based observable. This is useful to replace one promise based observable with another, without going back to an intermediate "pending" promise state while fetching data. For example:

Parameters

  • origPromise The promise which will be observed
  • oldPromise The previously observed promise

Examples

@observer
class SearchResults extends React.Component {
  @observable.ref searchResults

  componentDidUpdate(nextProps) {
    if (nextProps.query !== this.props.query)
      this.searchResults = fromPromise(
        window.fetch("/search?q=" + nextProps.query),
        // by passing, we won't render a pending state if we had a successful search query before
        // rather, we will keep showing the previous search results, until the new promise resolves (or rejects)
        this.searchResults
      )
  }

  render() {
    return this.searchResults.case({
       pending: (staleValue) => {
         return staleValue || "searching" // <- value might set to previous results while the promise is still pending
       },
       fulfilled: (value) => {
         return value // the fresh results
       },
       rejected: (error) => {
         return "Oops: " + error
       }
    })
  }
}

Observable promises can be created immediately in a certain state using
`fromPromise.reject(reason)` or `fromPromise.resolve(value?)`.
The main advantage of `fromPromise.resolve(value)` over `fromPromise(Promise.resolve(value))` is that the first _synchronously_ starts in the desired state.

It is possible to directly create a promise using a resolve, reject function:
`fromPromise((resolve, reject) => setTimeout(() => resolve(true), 1000))`
const fetchResult = fromPromise(fetch("http://someurl"))

// combine with when..
when(
  () => fetchResult.state !== "pending",
  () => {
    console.log("Got ", fetchResult.value)
  }
)

// or a mobx-react component..
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>
  }
})

// or using the case method instead of switch:

const myComponent = observer(({ fetchResult }) =>
  fetchResult.case({
    pending:   () => <div>Loading...</div>,
    rejected:  error => <div>Ooops.. {error}</div>,
    fulfilled: value => <div>Gotcha: {value}</div>,
  }))

// chain additional handler(s) to the resolve/reject:

fetchResult.then(
  (result) =>  doSomeTransformation(result),
  (rejectReason) => console.error('fetchResult was rejected, reason: ' + rejectReason)
).then(
  (transformedResult) => console.log('transformed fetchResult: ' + transformedResult)
)

Returns any origPromise with added properties and methods described above.

isPromiseBasedObservable

Returns true if the provided value is a promise-based observable.

Parameters

  • value any

Returns boolean

moveItem

Moves an item from one position to another, checking that the indexes given are within bounds.

Parameters

Examples

const source = observable([1, 2, 3])
moveItem(source, 0, 1)
console.log(source.map(x => x)) // [2, 1, 3]

Returns ObservableArray<T>

lazyObservable

lazyObservable creates an observable around a fetch method that will not be invoked until 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)

Examples

const userProfile = lazyObservable(
  sink => fetch("/myprofile").then(profile => sink(profile))
)

// use the userProfile in a React component:
const Profile = observer(({ userProfile }) =>
  userProfile.current() === undefined
  ? <div>Loading user profile...</div>
  : <div>{userProfile.current().displayName}</div>
)

// triggers refresh the userProfile
userProfile.refresh()

fromResource

fromResource creates an observable whose 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, or the implementation of mobxUtils.now

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 the current state
      sink(dbUserRecord.fields)
      // subscribe to the record, invoke the sink callback whenever new data arrives
      currentSubscription = dbUserRecord.onUpdated(() => {
        sink(dbUserRecord.fields)
      })
    },
    () => {
      // the user observable is not in use at the moment, unsubscribe (for now)
      dbUserRecord.unsubscribe(currentSubscription)
    }
  )
}

// usage:
const myUserObservable = createObservableUser(myDatabaseConnector.query("name = 'Michel'"))

// use the observable in autorun
autorun(() => {
  // printed everytime the database updates its records
  console.log(myUserObservable.current().displayName)
})

// ... or a component
const userComponent = observer(({ user }) =>
  <div>{user.current().displayName}</div>
)

toStream

Converts an expression to an observable stream (a.k.a. TC 39 Observable / RxJS observable). The provided expression is tracked by mobx as long as there are subscribers, automatically emitting when new values become available. The expressions respect (trans)actions.

Parameters

  • expression
  • fireImmediately boolean (by default false)

Examples

const user = observable({
  firstName: "C.S",
  lastName: "Lewis"
})

Rx.Observable
  .from(mobxUtils.toStream(() => user.firstname + user.lastName))
  .scan(nameChanges => nameChanges + 1, 0)
  .subscribe(nameChanges => console.log("Changed name ", nameChanges, "times"))

Returns IObservableStream<T>

StreamListener

ViewModel

createViewModel

createViewModel takes an object with observable properties (model) and wraps a viewmodel around it. The viewmodel proxies all enumerable properties 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 state
  • reset(): resets the state of the viewmodel, abandoning all local modifications
  • resetProperty(propName): resets the specified property of the viewmodel
  • isDirty: observable property indicating if the viewModel contains any modifications
  • isPropertyDirty(propName): returns true if the specified property is dirty
  • changedValues: returns a key / value map with the properties that have been changed in the model so far
  • model: 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

  • model T

Examples

class Todo {
  @observable title = "Test"
}

const model = new Todo()
const viewModel = createViewModel(model);

autorun(() => console.log(viewModel.model.title, ",", viewModel.title))
// prints "Test, Test"
model.title = "Get coffee"
// prints "Get coffee, Get coffee", viewModel just proxies to model
viewModel.title = "Get tea"
// prints "Get coffee, Get tea", viewModel's title is now dirty, and the local value will be printed
viewModel.submit()
// prints "Get tea, Get tea", changes submitted from the viewModel to the model, viewModel is proxying again
viewModel.title = "Get cookie"
// prints "Get tea, Get cookie" // viewModel has diverged again
viewModel.reset()
// prints "Get tea, Get tea", changes of the viewModel have been abandoned

keepAlive

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

  • _1
  • _2
  • target Object an object that has a computed property, created by @computed or extendObservable
  • property string the name of the property to keep alive

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

Parameters

  • _1
  • _2
  • computedValue IComputedValue<any> created using the computed function

Examples

const number = observable(3)
const doubler = computed(() => number.get() * 2)
const stop = keepAlive(doubler)
// doubler will now stay in sync reactively even when there are no further observers
stop()
// normal behavior, doubler results will be recomputed if not observed but needed, but lazily

Returns IDisposer stops this keep alive so that the computed value goes back to normal behavior

queueProcessor

queueProcessor takes an observable array, observes it and calls processor once for each item added to the observable array, optionally debouncing the action

Parameters

  • observableArray Array<T> observable array instance to track
  • processor
  • 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 => {
  // show Desktop notification
  new Notification(msg);
})

// usage:
pendingNotifications.push("test!")

Returns IDisposer stops the processor

chunkProcessor

chunkProcessor takes an observable array, observes it and calls processor once for a chunk of items added to the observable array, optionally deboucing the action. The maximum chunk size can be limited by number. This allows both, splitting larger into smaller chunks or (when debounced) combining smaller chunks and/or single items into reasonable chunks of work.

Parameters

  • observableArray Array<T> observable array instance to track
  • processor
  • debounce number optional debounce time in ms. With debounce 0 the processor will run synchronously (optional, default 0)
  • maxChunkSize number optionally do not call on full array but smaller chunks. With 0 it will process the full array. (optional, default 0)

Examples

const trackedActions = observable([])
const stop = chunkProcessor(trackedActions, chunkOfMax10Items => {
  sendTrackedActionsToServer(chunkOfMax10Items);
}, 100, 10)

// usage:
trackedActions.push("scrolled")
trackedActions.push("hoveredButton")
// when both pushes happen within 100ms, there will be only one call to server

Returns IDisposer stops the processor

resetNowInternalState

Disposes of all the internal Observables created by invocations of now().

The use case for this is to ensure that unit tests can run independent of each other. You should not call this in regular application code.

Examples

afterEach(() => {
    utils.resetNowInternalState()
})

now

Returns the current date time as epoch number. The date time is read from an observable which is updated automatically after the given interval. So basically it treats time as an observable.

The function takes an interval as parameter, which indicates how often now() will return a new value. If no interval is given, it will update each second. If "frame" is specified, it will update each time a requestAnimationFrame is available.

Multiple clocks with the same interval will automatically be synchronized.

Countdown example: https://jsfiddle.net/mweststrate/na0qdmkw/

Parameters

  • interval (number | "frame") interval in milliseconds about how often the interval should update (optional, default 1000)

Examples

const start = Date.now()

autorun(() => {
  console.log("Seconds elapsed: ", (mobxUtils.now() - start) / 1000)
})

expr

expr can be used to create temporary computed values inside computed values. Nesting computed values is useful to create cheap computations in order to prevent expensive computations from needing to run. In the following example the expression prevents that a component is rerender each time the selection changes; instead it will only rerenders when the current todo is (de)selected.

expr(func) is an alias for computed(func).get(). Please note that the function given to expr is evaluated twice in the scenario that the overall expression value changes. It is evaluated the first time when any observables it depends on change. It is evaluated a second time when a change in its value triggers the outer computed or reaction to evaluate, which recreates and reevaluates the expression.

In the following example, the expression prevents the TodoView component from being re-rendered if the selection changes elsewhere. Instead, the component will only re-render when the relevant todo is (de)selected, which happens much less frequently.

Parameters

  • expr

Examples

const Todo = observer((props) => {
    const todo = props.todo
    const isSelected = mobxUtils.expr(() => props.viewState.selection === todo)
const TodoView = observer(({ todo, editorState }) => {
    const isSelected = mobxUtils.expr(() => editorState.selection === todo)
    return <div className={isSelected ? "todo todo-selected" : "todo"}>{todo.title}</div>
})

createTransformer

Creates a function that maps an object to a view. The mapping is memoized.

See the transformer section for more details.

Parameters

  • transformer
  • arg2
  • onCleanup

deepObserve

Given an object, deeply observes the given object. It is like observe from mobx, but applied recursively, including all future children.

Note that the given object cannot ever contain cycles and should be a tree.

As benefit: path and root will be provided in the callback, so the signature of the listener is (change, path, root) => void

The returned disposer can be invoked to clean up the listener

deepObserve cannot be used on computed values.

Parameters

  • target
  • listener

Examples

const disposer = deepObserve(target, (change, path) => {
   console.dir(change)
})

ObservableGroupMap

Reactively sorts a base observable array into multiple observable arrays based on the value of a groupBy: (item: T) => G function.

This observes the individual computed groupBy values and only updates the source and dest arrays when there is an actual change, so this is far more efficient than, for example base.filter(i => groupBy(i) === 'we'). Call #dispose() to stop tracking.

No guarantees are made about the order of items in the grouped arrays.

The resulting map of arrays is read-only. clear(), set(), delete() are not supported and modifying the group arrays will lead to undefined behavior.

NB: ObservableGroupMap relies on Symbols. If you are targeting a platform which doesn't support these natively, you will need to provide a polyfill.

Parameters

  • base array The array to sort into groups.
  • groupBy function The function used for grouping.
  • options Object with properties: name: Debug name of this ObservableGroupMap. keyToName: Function to create the debug names of the observable group arrays.

Examples

const slices = observable([
    { day: "mo", hours: 12 },
    { day: "tu", hours: 2 },
])
const slicesByDay = new ObservableGroupMap(slices, (slice) => slice.day)
autorun(() => console.log(
    slicesByDay.get("mo")?.length ?? 0,
    slicesByDay.get("we"))) // outputs 1, undefined
slices[0].day = "we" // outputs 0, [{ day: "we", hours: 12 }]

ObservableMap

defineProperty

Base observable array which is being sorted into groups.

defineProperty

The ObservableGroupMap needs to track some state per-item. This is the name/symbol of the property used to attach the state.

defineProperty

The function used to group the items.

defineProperty

This function is used to generate the mobx debug names of the observable group arrays.

defineProperty

Disposes all observers created during construction and removes state added to base array items.

computedFn

computedFn takes a function with an arbitrary amount of arguments, and memoizes the output of the function based on the arguments passed in.

computedFn(fn) returns a function with the very same signature. There is no limit on the amount of arguments that is accepted. However, the amount of arguments must be constant and default arguments are not supported.

By default the output of a function call will only be memoized as long as the output is being observed.

The function passes into computedFn should be pure, not be an action and only be relying on observables.

Setting keepAlive to true will cause the output to be forcefully cached forever. Note that this might introduce memory leaks!

Parameters

  • fn
  • keepAliveOrOptions

Examples

const store = observable({
a: 1,
b: 2,
c: 3,
m: computedFn(function(x) {
return this.a * this.b * x
})
})

const d = autorun(() => {
// store.m(3) will be cached as long as this autorun is running
console.log(store.m(3) * store.c)
})

DeepMapEntry

DeepMap

Details

createTransformer in detail

With createTransformer it is very easy to transform a complete data graph into another data graph. Transformation functions can be composed so that you can build a tree using lots of small transformations. The resulting data graph will never be stale, it will be kept in sync with the source by applying small patches to the result graph. This makes it very easy to achieve powerful patterns similar to sideways data loading, map-reduce, tracking state history using immutable data structures etc.

createTransformer turns a function (that should transform value A into another value B) into a reactive and memoizing function. In other words, if the transformation function computes B given a specific A, the same B will be returned for all other future invocations of the transformation with the same A. However, if A changes, or any derivation accessed in the transformer function body gets invalidated, the transformation will be re-applied so that B is updated accordingly. And last but not least, if nobody is using the transformation of a specific A anymore, its entry will be removed from the memoization table.

The optional onCleanup function can be used to get a notification when a transformation of an object is no longer needed. This can be used to dispose resources attached to the result object if needed.

Always use transformations inside a reaction like observer or autorun.

Transformations will, like any other computed value, fall back to lazy evaluation if not observed by something, which sort of defeats their purpose.

Parameters

  • `transformation: (value: A) => B
  • onCleanup?: (result: B, value?: A) => void)

createTransformer<A, B>(transformation: (value: A) => B, onCleanup?: (result: B, value?: A) => void): (value: A) => B

Examples

This all might still be a bit vague, so here are two examples that explain this whole idea of transforming one data structure into another by using small, reactive functions:

Tracking mutable state using immutable, shared data structures.

This example is taken from the Reactive2015 conference demo:

/*
    The store that holds our domain: boxes and arrows
*/
const store = observable({
    boxes: [],
    arrows: [],
    selection: null,
})

/**
    Serialize store to json upon each change and push it onto the states list
*/
const states = []

autorun(() => {
    states.push(serializeState(store))
})

const serializeState = createTransformer((store) => ({
    boxes: store.boxes.map(serializeBox),
    arrows: store.arrows.map(serializeArrow),
    selection: store.selection ? store.selection.id : null,
}))

const serializeBox = createTransformer((box) => ({ ...box }))

const serializeArrow = createTransformer((arrow) => ({
    id: arrow.id,
    to: arrow.to.id,
    from: arrow.from.id,
}))

In this example the state is serialized by composing three different transformation functions. The autorunner triggers the serialization of the store object, which in turn serializes all boxes and arrows. Let's take closer look at the life of an imaginary example box#3.

  1. The first time box#3 is passed by map to serializeBox, the serializeBox transformation is executed and an entry containing box#3 and its serialized representation is added to the internal memoization table of serializeBox.
  2. Imagine that another box is added to the store.boxes list. This would cause the serializeState function to re-compute, resulting in a complete remapping of all the boxes. However, all the invocations of serializeBox will now return their old values from the memoization tables since their transformation functions didn't (need to) run again.
  3. Secondly, if somebody changes a property of box#3 this will cause the application of the serializeBox to box#3 to re-compute, just like any other reactive function in MobX. Since the transformation will now produce a new Json object based on box#3, all observers of that specific transformation will be forced to run again as well. That's the serializeState transformation in this case. serializeState will now produce a new value in turn and map all the boxes again. But except for box#3, all other boxes will be returned from the memoization table.
  4. Finally, if box#3 is removed from store.boxes, serializeState will compute again. But since it will no longer be using the application of serializeBox to box#3, that reactive function will go back to non-reactive mode. This signals the memoization table that the entry can be removed so that it is ready for GC.

So effectively we have achieved state tracking using immutable, shared datas structures here. All boxes and arrows are mapped and reduced into single state tree. Each change will result in a new entry in the states array, but the different entries will share almost all of their box and arrow representations.

Transforming a datagraph into another reactive data graph

Instead of returning plain values from a transformation function, it is also possible to return observable objects. This can be used to transform an observable data graph into a another observable data graph, which can be used to transform... you get the idea.

Here is a small example that encodes a reactive file explorer that will update its representation upon each change. Data graphs that are built this way will in general react a lot faster and will consist of much more straight-forward code, compared to derived data graph that are updated using your own code. See the performance tests for some examples.

Unlike the previous example, the transformFolder will only run once as long as a folder remains visible; the DisplayFolder objects track the associated Folder objects themselves.

In the following example all mutations to the state graph will be processed automatically. Some examples:

  1. Changing the name of a folder will update its own path property and the path property of all its descendants.
  2. Collapsing a folder will remove all descendant DisplayFolders from the tree.
  3. Expanding a folder will restore them again.
  4. Setting a search filter will remove all nodes that do not match the filter, unless they have a descendant that matches the filter.
  5. Etc.
import {extendObservable, observable, createTransformer, autorun} from "mobx"

function Folder(parent, name) {
	this.parent = parent;
	extendObservable(this, {
		name: name,
		children: observable.shallow([]),
	});
}

function DisplayFolder(folder, state) {
	this.state = state;
	this.folder = folder;
	extendObservable(this, {
		collapsed: false,
		get name() {
			return this.folder.name;
		},
		get isVisible() {
			return !this.state.filter || this.name.indexOf(this.state.filter) !== -1 || this.children.some(child => child.isVisible);
		},
		get children() {
			if (this.collapsed)
				return [];
			return this.folder.children.map(transformFolder).filter(function(child) {
				return child.isVisible;
			})
		},
		get path() {
			return this.folder.parent === null ? this.name : transformFolder(this.folder.parent).path + "/" + this.name;
		})
	});
}

var state = observable({
	root: new Folder(null, "root"),
	filter: null,
	displayRoot: null
});

var transformFolder = createTransformer(function (folder) {
	return new DisplayFolder(folder, state);
});


// returns list of strings per folder
var stringTransformer = createTransformer(function (displayFolder) {
	var path = displayFolder.path;
	return path + "\n" +
		displayFolder.children.filter(function(child) {
			return child.isVisible;
		}).map(stringTransformer).join('');
});

function createFolders(parent, recursion) {
	if (recursion === 0)
		return;
	for (var i = 0; i < 3; i++) {
		var folder = new Folder(parent, i + '');
		parent.children.push(folder);
		createFolders(folder, recursion - 1);
	}
}

createFolders(state.root, 2); // 3^2

autorun(function() {
    state.displayRoot = transformFolder(state.root);
    state.text = stringTransformer(state.displayRoot)
    console.log(state.text)
});

state.root.name = 'wow'; // change folder name
state.displayRoot.children[1].collapsed = true; // collapse folder
state.filter = "2"; // search
state.filter = null; // unsearch

mobx-utils's People

Contributors

a-gambit avatar bb avatar daedalus28 avatar davidosomething avatar dr0p avatar hc-12 avatar hepuxuan avatar hsrobbv avatar itamarshdev avatar jamiewinder avatar kuitos avatar laszlopandy avatar mattruby avatar miton18 avatar mweststrate avatar n9 avatar naridal avatar nokel81 avatar noreflection avatar raineratspirit avatar realityforge avatar samdroid-apps avatar taj-p avatar thinhvoxuan avatar uppajung avatar upsuper avatar vonovak avatar wbercx avatar wemyss avatar xaviergonz avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

mobx-utils's Issues

proxy functions/computed on ViewModel

Is it possible to "bypass" functions from model, including computed and "get function()" ? it looks like that for now it only proxy properties...

introduce `guard` (or `invariant`)

function guard(expr) {
   return autorun(() => {
     if (!expr) throw "Inviariant violated!"
   })
}
guard(() => car.isSpeeding === car.speed > 120)

guard(() => cache.size() === data.length)

// etc

README \@

Hey, I've noticed that all README examples prefix @decorators with \. Is it a TypeScript, did I miss something or is it just a typo and I should send a PR? :)

Observable created by fromResource can't be used in `computed` if it sinks immediately

Observables created using fromResource seem to be impossible to use inside a computed from mainline MobX. Apart from creating derived values, this is also useful for simply wrapping the resulting observables in a familiar interface to be passed to functions that normally received an IComputedValue<T> rather than a {current(): T, dispose(): void, isAlive(): boolean}.

Even a simple computed: computed(() => fromResourceObservable.current()) throws an exception.

The exception thrown is an invariant violation of actions being used inside computed:

Uncaught Error: [mobx] Invariant failed: Computed values or transformers should not invoke actions or trigger other side effects

This happens because it appears that fromResource's sink uses an action in its implementation, here:

subscriber(action("ResourceBasedObservable-sink", (newValue: T) => {
.

Example

You can see an example of the problem here http://www.webpackbin.com/4k5kc14BM. I also made a zip of the project, since WebpackBin doesn't let me make the bin immutable.

In the above project, if you render the React component using obsUser directly, the component works fine. If you render it using computedUser, you trigger the above exception.

I used exactly the code from the example by @mweststrate in the mobx-utils README with a mock dbUserRecord wrapped in a computed.

I ran into this problem myself trying to use MobX to wrap apollo-client as in your comment here. The reason being that Apollo's subscribe immediately calls sink on the stack of its first call, thus tripping this action guard.

All of these problems can be avoided as far as I can tell, by deferring the sink to occur on the 'next tick', so that it's outside the guards of the computed's execution, but this is an unreliable solution that should hopefully not be necessary.

adding new values to a lazyObservable array

My lazyObservable array works great the first time the react component is mounted, and later if I do something like

list.current()[ 5 ].name = "jones"

The table showing the list is re-rendered and I see the new name. But if I

list.current().push( newPerson )

My table is not growing by one.

    { list.current().map( person => 
      <Row key={person.id} ...
          <Cell>{person.name}</Cell>

What am I missing?

IPromiseBasedObservable typings improvement

As for now, IPromiseBasedObservable<T> has this definition:

export interface IPromiseBasedObservable<T> {
    value: T;
    state: PromiseState;
    reason: any;
    promise: PromiseLike<T>;
    case<U>(handlers: {pending?: () => U, fulfilled?: (t: T) => U, rejected?: (e: any) => U}): U;
}

I suggest to change that to get stronger types:

interface IPromiseBasedObservablePending {
  state: "pending"
}
interface IPromiseBasedObservableFulfilled<T> {
  state: "fulfilled"
  value: T
}
interface IPromiseBasedObservableRejected {
  state: "rejected"
  value: Error
}
export type IPromiseBasedObservable<T> = (
  IPromiseBasedObservablePending |
  IPromiseBasedObservableFulfilled<T> |
  IPromiseBasedObservableRejected
) & {
  promise: PromiseLike<T>;
  case<U>(handlers: {pending?: () => U, fulfilled?: (t: T) => U, rejected?: (e: Error) => U}): U;
}

After that typescript will force developer to cover all cases of state and gives strict access to value:

function render(v: IPromiseBasedObservable<string>): string {
  return v.value // Property 'value' does not exists on type 'IPromiseBasedObservable<string>'
}

function render(v: IPromiseBasedObservable<string>): string {
    if (v.state === "pending") {
        return "value is pending"
    }
    if (v.state === "rejected") {
        return `value is rejected with: ${v.value.message}` // v.value is Error here
    }
    return `value is: ${v.value}`; // v.value is string here
}

Also I suggest to either:

  1. Remove case function from IPromiseBasedObservable
  2. Make all handlers required, for each state.

With strictNullChecks enabled it gives proper check for all cases of v.state are covered.
It is breaking change and usable only for TS2+, so, it should introduce new major version.

createViewModel from ObservableMap

Is it possible to use createViewModel from an ObservableMap?

I'm seeing Uncaught Error: [mobx-utils] createViewModel expects an observable object.

In the submit action there's an isObservableArray and isObservableMap check which then calls replace, or clear and merge, on the destination. But I don't think they will be used because of the invariant check in the constructor only checks for isObservableObject.

fromAjax with automatic abort on becoming unobserved

I have a few scenarios like this where I have a @computed property which returns a fromPromise result:

@observable filter = '';

@computed get filterItems() {
    const url = `/loaditems?filter=${this.filter}`;
    return fromPromise(getItemsFromUrl(url));
}

This works really well but if the observables that filterItems uses changes often, specifically mid-flight, we end up continuing a request that is no longer needed. I wonder if a special fromAjax utility might be useful here, which'd have the following behaviour:

  • The result would have the same shape as IPromiseBasedObservable, however the state would have an additional value of 'cancelled'.
  • Once created, if the result value of fromAjax become unobserved, the underlying AJAX request would be cancelled, and the state of the result would permanently transition to 'cancelled'.

This should mean that code like the snippet above should work well; recomputations which cause the fromAjax result to become unobserved will automatically abort the request in favour of a new request.

I wanted to float the idea before giving it a stab; please poke holes in anything I've posted above!

fromPromise initialValue from previous fromPromise

Hi

I'm trying to reuse fromPromise value for next fromPromise with initalValue, so the value wont be reset between calls and I'm getting this error "Uncaught TypeError: this._observable.get is not a function". I've looked in to the source but don't know exactly why

class Store {
    @observable data = fromPromise(Promise.resolve({}));
    @action getData(){
        this.data = fromPromise(somePromise(),this.data.value);
    }
}

first getData call works fine, but the second one throws an Error
Any suggestions?
Thanks for awesome lib!

Lazy Async Loading Decorator differences between Node and browser

I've been trying to turn an async loading pattern I've been using into a decorator. Looks something like this:

const {Component} = React;
const {observable, computed, extendObservable, useStrict} = mobx;
const {lazyObservable, fromPromise} = mobxUtils;
const {observer} = mobxReact;

useStrict(true);

function loadable(initialValue) {
  
  return (target, name, descriptor) => {

        const promise = descriptor.get;

        const lazyPromise = lazyObservable(
          (sink) => sink(fromPromise(promise(), initialValue)), 
          Promise.resolve(initialValue)
        );

        const newProps = {};

        newProps[name + 'Loading'] = computed(() => lazyPromise.current().state == 'pending');
        newProps[name + 'LoadingFailed'] = computed(() => lazyPromise.current().state == 'rejected');

        extendObservable(target, newProps);

        descriptor.get = () => lazyPromise.current().value;

        return descriptor;
    };
}

class Store {
  
  @loadable([])
  get something() {
    return new Promise((resolve, reject) => {
      setTimeout(() => {

        Math.random() > .25  ? resolve([1,2,3,4,5,6,7,8,9,10]) : reject([]);

      }, 5000);

    });
  }
   
  @computed get somethingFiltered() {
     return this.something.filter((e) => e % 2);
  } 
}

@observer
class LoadableThing extends Component {

  render () {
    
    const {store} = this.props;
    
    console.log(store);
   
    if (store.somethingLoading) return <div>Loading...</div>;
    if (store.somethingLoadingFailed) return <div>Loading failed...</div>;
    
    return <div>
      Original: {store.something.join(',')}<br />
      Filtered: {store.somethingFiltered.join(',')}
    </div>;
  }
}

class App extends Component {
  render = () => (
    <div>
      <LoadableThing store={ new Store() }/>
    </div>
  );
}

ReactDOM.render(<App/>, document.getElementById('container'));

codepen: http://codepen.io/anon/pen/XMBooq

This seems to work as expected in the codepen, and works for the most part in my node application except for one thing: the computed property that references the loadable property (somethingFiltered in the example) is always undefined. I've reduced my node application as much as possible to see where things are going wrong, but the behavior is always the same. This leads me to believe the issue has something to do with a difference between Node and the browser, likely involving decorators, since when I remove the decorator and explicitly implement all the different pieces (and initialize the lazyObservable in the constructor), things work fine, even in my Node application.

Can anybody offer a reason why this would not fully work within Node or perhaps a better pattern that I should be using to achieve the desired behavior??

fromPromise suggestion: case method

Would be very useful in the middle of JSX expressions.

Badly indented example:

<div> 
{
  promiseObservables.map(o => 
    o.case({
      pending: () => <p>Loading...</p>, 
      success: value => <MyComponent prop={value} />, 
      error: err => <Alert message={err.message} />
    }))
}
</div>

Feature Request: whenAsync

I have a few use cases where I'd like a promise for when an observable is set with a certain value. when is awesome, but it effectively takes a callback for when the predicate returns true instead of resolving a promise.

I've implemented this as whenAsync like so:

let whenAsync = fn => new Promise(resolve => when(fn, resolve))

And I use it like this:

await whenAsync(() => !state.someBoolean)

It's especially useful for detecting when boolean values change, e.g. getting a promise for when a modal closes, something finishes loading, etc. It's also useful for testing. We have utils here to convert from a promise to an observable, but not the other way around.

Would something like this be useful as a pull request?

Create "memoize" helper

As for now, mobx has computed decorators, which could be used on argumentless functions. It would be useful to create "memoize" helper, which acts like lodash memoize function, but with usage of mobx power.

fromPromise which starts with fulfilled state

We mostly use fromPromise to render loading or other states in react components. Here is a sample component:

@observer
class MyComponent extends React.Component {
  @action componentDidMount() {
    this.entity = this.props.store.getEntity();  // getEntity returns a `IPromiseBasedObservable`
  }

  @observable entity = fromPromise(Promise.resolve());

  render() {
    return this.entity.case({
      fulfilled: value => (<div>{JSON.stringify(value)}</div>),
      rejected: error => (<div>Error</div>),
      pending: () => (<div>Loading entity...</div>)
    });
  }
}

I have initialized entity with a resolved fromPromise to avoid writing an if in render method. What I think is really useful is fromPromise(value). So fromPromise does not necessarily needs a Thenable object.

Sink or Sync

In the fromResource documentation is sink supposed to be mean sync, as in syncing the fields of the external resource with the observable, or as a sink, in Rx terms?

screen shot 2016-12-13 at 11 48 59 am

fromPromise.state as properties of fromPromise.status

it would be cool if you could access the status of a fromPromise as properties instead of accessing the state as strings.
e.g

const promise = Promise.resolve()
fromPromise(promise).status.pending && <span>Pending</span>

instead of

const promise = Promise.resolve()
fromPromise(promise).state === "pending" && <span>Pending</span>

i think it would be a bit easier to spot spelling errors if there was an error thrown.
you could do something similar with case but its still not quite as nice

const promise = Promise.resolve()
promise && fromPromise(promise).case({pending: () => <span>Pending</span>})

thoughts?

toStream should produce an iterable, for better compatiblity with other libs

Just spent some time trying to get it to work with the npm package rx but it didn't work. I only noticed I had to use RxJS 5 (npm package rxjs) when I looked at the relevant tests file.
With RxJS 5 the examples work fine.

The errors were:
(toStream)

Uncaught TypeError: Object is not iterable
    at getIterable (rx.all.js:2706)
    at FromObservable.subscribeCore (rx.all.js:2634)
    at FromObservable.tryCatcher (rx.all.js:63)
    at setDisposable (rx.all.js:2082)

(fromStream)

Cannot read property fn of undefined
(forgot to save stack trace)

Computed Async

I don't know if this has been brought up before, I'd assume so, but is there any plans to release a ComputedAsync in mobx-utils?

Here it's been developed by a 3rd party: https://github.com/danielearwicker/computed-async-mobx

I personally enjoy this plugin and so far it hasn't failed me. Of course I'd prefer using features that are supported by the MobX community and so maybe there's a reason why the use of ComputedAsync is not a good option.

I know I can achieve the same results of a ComputedAsync with either the fromPromise utils or even just using autorun, but I find it makes the code less readable and intuitive as a ComputedAsync does.

OnAction util?

Currently have this in one of my projects

spy((change) => {
  if (change.type === 'action' && (change.name === 'update' || change.name === 'select')) {
    // setTimeout is because action is "spyed" before mutations take place
    setTimeout(() => socket.emit('data', { stuff: this.stuff }), 0) 
  }
})

Would it make sense as a util that's basically a more controlled autorun (like reaction)?

onAction(['update', 'select'], () => socket.emit('data', { stuff: this.stuff }))

Refreshing a lazyObservable forces it to load a value

I am using lazyObservable along with autorun to keep track of fields from a Dexie database. (Not 100% sure if this is a sane approach as I'm new to MobX, so feel free to tell me if I'm doing it wrong).

Here's how that looks:

export class ObservableDexieTable {
    @observable ready = false;

    tableLazyFields = {
        count: () => this.table.count()
    }

    constructor(observableDexie, tableName) {
        this._observableDexie = observableDexie;
        this.tableName = tableName;

        for (const [field, callback] of Object.entries(this.tableLazyFields)) {
            this[field] = lazyObservable(
                sink => callback().then(result => {
                    console.log("Load field", this.tableName, field, this[field]);
                    return sink(result);
                })
            );
        }

        autorun(
            this.table;
            untracked(() => {
                for (const field of Object.keys(this.tableLazyFields)) {
                    this[field].refresh();
                }
            });
        );
    }

    get db() {
        return this._observableDexie.db;
    }

    get table() {
        return this._observableDexie.table(this.tableName);
    }
}

So, we have a bunch of lazy-loading fields, and we trigger refresh() on them whenever the database changes, to keep their values up to date. (observableDexieTable is another class that uses "dexie-observable" and an atom that fires whenever the database changes).

So lazyObservable works great and our fields aren't loaded until we access them, except when I include that autorun code. As soon as I add that, the field's fetch callback runs immediately, no matter what! I was instead expecting the observable to only run fetch if it is being observed.

keepAlive timeout?

I've started using keepAlive to get around an issue where my observable objects become momentarily unobserved, and avoid complex computations (including those that result in network requests) re-running. Sometimes this can be between frames, other times I might want something to hang onto the computed value for a longer time.

So does a keepAlive timeout make sense? I was going to have a go at this, but I don't know how I can hook into the 'I'm now the last observer, I'll drop my observer in X seconds' situation so I thought I'd float the idea first.

fromPromise: Pass initialValue to the pending handler of case method

The purpose of initialValue is to provide a value when the promise final value is not yet available.
From https://github.com/mobxjs/mobx-utils/blob/master/src/from-promise.ts#L50

switch (this.state) {
    case "pending": return handlers.pending && handlers.pending();
    case "rejected": return handlers.rejected && handlers.rejected(this.value);
    case "fulfilled": return handlers.fulfilled && handlers.fulfilled(this.value);
}

The way to distinguish between a fulfilled this.value and a rejected this.value is the promise state. Same applies to a pending this.value since the boxed observable has its value set to initialValue in the beginning. Therefore I propose that we also pass this.value to the pending handler. Since I haven't studied more scenario, please tell me if there is any problem that might arise that I'm unaware of.

Passing initialValue to the pending handler in this case eliminates the need of using closure to access the promise value from the handler (good for composability?)

I can attempt a PR if need be

possible problem with es2015 promise & fromPromise

I'm using VS code and typescript targeting ES2015, so it's using lib.es6.d.ts for the typings of Promise. (Incidentally I'm using the isomorphic-fetch lib, but that shouldn't be relevant as I'm working on compiling problems, not run-time). I just found this utils library and thought "fromPromise is exactly what I need!". My problem is a compilation error on something simple like:

const response = fromPromise(fetch('http://example.com'));

My TypeScript compiler is complaining that Promise is not assignable to IThenable - "Types of property then are incompatible."

I'm hoping this is something I'm missing or easy, but I haven't been able to figure it out yet. Comparing the type definitions may have a clue: Promise has two overloads of then, though I'm not sure why. I'm including the relevant snippets of the definitions for reference below.

Any help is much appriciated.

From lib.es6.d.ts:

interface Promise<T> {
    /**
    * Attaches callbacks for the resolution and/or rejection of the Promise.
    * @param onfulfilled The callback to execute when the Promise is resolved.
    * @param onrejected The callback to execute when the Promise is rejected.
    * @returns A Promise for the completion of which ever callback is executed.
    */
    then<TResult>(onfulfilled?: (value: T) => TResult | PromiseLike<TResult>, onrejected?: (reason: any) => TResult | PromiseLike<TResult>): Promise<TResult>;
    then<TResult>(onfulfilled?: (value: T) => TResult | PromiseLike<TResult>, onrejected?: (reason: any) => void): Promise<TResult>;

    /**
     * Attaches a callback for only the rejection of the Promise.
     * @param onrejected The callback to execute when the Promise is rejected.
     * @returns A Promise for the completion of the callback.
     */
    catch(onrejected?: (reason: any) => T | PromiseLike<T>): Promise<T>;
    catch(onrejected?: (reason: any) => void): Promise<T>;

    [Symbol.toStringTag]: "Promise";
}
interface PromiseLike<T> {
    /**
    * Attaches callbacks for the resolution and/or rejection of the Promise.
    * @param onfulfilled The callback to execute when the Promise is resolved.
    * @param onrejected The callback to execute when the Promise is rejected.
    * @returns A Promise for the completion of which ever callback is executed.
    */
    then<TResult>(onfulfilled?: (value: T) => TResult | PromiseLike<TResult>, onrejected?: (reason: any) => TResult | PromiseLike<TResult>): PromiseLike<TResult>;
    then<TResult>(onfulfilled?: (value: T) => TResult | PromiseLike<TResult>, onrejected?: (reason: any) => void): PromiseLike<TResult>;
}

From mobx-utils:

export interface IThenable<T> {
    then<R>(onFulfilled?: (value: T) => IThenable<R> | R, onRejected?: (error: any) => IThenable<R> | R): IThenable<R>;
    catch<R>(onRejected?: (error: any) => IThenable<R> | R): IThenable<R>;
    done<R>(onFulfilled?: (value: T) => IThenable<R> | R, onRejected?: (error: any) => IThenable<R> | R): IThenable<R>;
    nodeify<R>(callback: Function): IThenable<R>;
}

Get values from view modal?

Hi, do you think it's reasonable to add a method to viewModal to get current values?
Now I end up using var { localValues, model, isPropertyDirty, ...data } = viewModal
and use the data
I am happy to add it and make PR.

fromPromise: Shallowly observe a promise

From https://github.com/mobxjs/mobx-utils/blob/master/src/from-promise.ts#L24
The value of the wrapper is an observable.box, which means result of the underlying promise will be made deeply observable

The purpose of fromPromise should be observing the state of the promise itself, and not the state of its value (except the reference). Therefore I propose adding an option (or make it the default behavior, but that would be a breaking change?) to shallowly observe the promise using observable.shallowBox

throttledValue

http://codepen.io/jamiewinder/pen/kkLdGa?editors=1010

I found this quite useful. It's handy when you want an element of your state to be updated in a throttled manner, and want access to both the current and incoming values. For example, if you've got a text filtered list somewhere in your application, you can use the 'latest' value as your value (<input value={a.latest} />) and the current value as the actual value used (.map(text => text.includes(a.current))).

I thought I'd float the idea and see what feedback I get. I can tidy this up and make a pull request if there is interest?

Invariant failed error when using fromStream in Mobx strict mode.

I'm getting the following error:

Uncaught Error: [mobx] Invariant failed: It is not allowed to create or change state outside an action when MobX is in strict mode. Wrap the current method in action if this state change is intended

when using the MobxUtils fromStream method in Mobx strict mode. Opting out of strict mode resolves the problem, however I'd strongly like to keep using strict mode.

Here is the callstack:

at invariant (mobx.js:2674)
    at checkIfStateModificationsAreAllowed (mobx.js:929)
    at ObservableValue.prepareNewValue (mobx.js:2447)
    at setPropertyValue (mobx.js:2368)
    at StreamListener.set [as current] (mobx.js:2336)
    at new StreamListener (observable-stream.js:56)
    at fromStream (observable-stream.js:105)
    at exports.default (store.js:68)
    at Object.<anonymous> (index.js:15)
    at __webpack_require__ (bootstrap cbf2afaโ€ฆ:555)

My code from store.js that might be relevant:

    // all the rows selected by the user
  const selected = observable([])

  const
    source$ = Observable
      .from(toStream(() => selected.slice())),
    firstSelected$ = source$
      .first()
      .map(x => x[0]),
    nextToggleSelected$ = source$
      .pairwise()
      .map(([prev, curr]) => {
        if (prev.length < curr.length)
          return last(curr)
        return difference(prev, curr)[0]
      }),
    lastToggleSelected$ = Observable.merge(
      firstSelected$, nextToggleSelected$
    )

  const lastToggleSelected = fromStream(lastToggleSelected$)

The last line in that sample is the one from which the error is thrown. The parts at new StreamListener (observable-stream.js:56) and at fromStream (observable-stream.js:105) from the transpiled mobx-utils correspond to line 72 this.current = initialValue; and line 81 this.current = value; in the observable-stream.ts source file.

Is there any way to fix this without disabling useStrict?

EDIT: Forgot to mention - I tried wrapping fromStream into an action, but it didn't seem to work.

A standard promise with observable state and value

I've implemented a simple Promise that has two observable properties: value and state. It is exactly like fromPromise but it is actually a Promise. As an example I have converted axios.get result and:

  const myPromise = toObservablePromise(axios.get('/api/get'));
  
  // It is possible to write chained then functions
  const myNewPromise = myPromise
    .then(response => response.data)
    .then(data => console.log(data))
    // Also you can write a catch handler
    .catch(err => console.error(err));

  // And in every step it is possible to observe the state
  when(
    () => myNewPromise.state === 'fulfilled',
    () => {
      console.log(myNewPromise.value); // data
  });

  when(
    () => myPromise.state === 'fulfilled',
    () => {
      console.log(myPromise.value); // response
  });

  // It is possible to write async tests easily
  it('should run this async test without done function', () => {
    return myNewPromise
       .then(data => {
         expect(data).toEqual(...);
       });
  });

  // Also it is possible to watch the whole process using autorun
  autorun(() => {
    console.log('Promise state=', myNewPromise.state, ', value=', myNewPromise.value);
  });
  // Promise state=pending, value=undefined
  // Promise state=fulfilled, value=<data>

  // also you can use Promise.all
  Promise.all(myPromise, new ObservablePromise(Promise.resolve('value')), aNativePromise);

Having this gives the feeling that you are working with a native promise ๐Ÿ˜† .

Here is a simple implementation for ObservablePromise and toObservablePromise (Promise implementation pasted from here):

import { observable } from 'mobx';
/**
 * Check if a value is a Promise and, if it is,
 * return the `then` method of that promise.
 *
 * @param {Promise|Any} value
 * @return {Function|Null}
 */
function getThen(value) {
  const t = typeof value;

  if (value && (t === 'object' || t === 'function')) {
    const then = value.then;

    if (typeof then === 'function') {
      return then;
    }
  }

  return null;
}

/**
 * Take a potentially misbehaving resolver function and make sure
 * onFulfilled and onRejected are only called once.
 *
 * Makes no guarantees about asynchrony.
 *
 * @param {Function} fn A resolver function that may not be trusted
 * @param {Function} onFulfilled
 * @param {Function} onRejected
 */
function doResolve(fn, onFulfilled, onRejected) {
  let done = false;

  try {
    fn((value) => {
      if (done) return;
      done = true;
      onFulfilled(value);
    }, (reason) => {
      if (done) return;
      done = true;
      onRejected(reason);
    });
  } catch (ex) {
    if (done) return;
    done = true;
    onRejected(ex);
  }
}

const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

function ObservablePromise(fn) {
  // store state which can be PENDING, FULFILLED or REJECTED
  const state = observable.box(PENDING);

  // store value once FULFILLED or REJECTED
  const value = observable.box(null);

  // store sucess & failure handlers
  let handlers = [];

  function handle(handler) {
    if (state.get() === PENDING) {
      handlers.push(handler);
    } else {
      if (state.get() === FULFILLED &&
        typeof handler.onFulfilled === 'function') {
        handler.onFulfilled(value.get());
      }
      if (state.get() === REJECTED &&
        typeof handler.onRejected === 'function') {
        handler.onRejected(value.get());
      }
    }
  }

  function fulfill(result) {
    value.set(result);
    state.set(FULFILLED);
    handlers.forEach(handle);
    handlers = null;
  }

  function reject(error) {
    value.set(error);
    state.set(REJECTED);
    handlers.forEach(handle);
    handlers = null;
  }

  function resolve(result) {
    try {
      const then = getThen(result);

      if (then) {
        doResolve(then.bind(result), resolve, reject);

        return;
      }
      fulfill(result);
    } catch (e) {
      reject(e);
    }
  }

  this.done = (onFulfilled, onRejected) => {
    // ensure we are always asynchronous
    setTimeout(() => {
      handle({
        onFulfilled,
        onRejected
      });
    }, 0);
  };

  this.then = (onFulfilled, onRejected) =>
    new ObservablePromise((resolveFn, rejectFn) => this.done((result) => {
      if (typeof onFulfilled === 'function') {
        try {
          return resolveFn(onFulfilled(result));
        } catch (ex) {
          return rejectFn(ex);
        }
      } else {
        return resolveFn(result);
      }
    }, (error) => {
      if (typeof onRejected === 'function') {
        try {
          return resolveFn(onRejected(error));
        } catch (ex) {
          return rejectFn(ex);
        }
      } else {
        return rejectFn(error);
      }
    }));

  this.catch = (onRejected) => this.then(null, onRejected);

  Object.defineProperty(this, 'value', {
    get() {
      return value.get();
    }
  });
  Object.defineProperty(this, 'state', {
    get() {
      return state.get();
    }
  });

  doResolve(fn, resolve, reject);
}

export default function toObservablePromise(promise) {
  return new ObservablePromise((resolve, reject) => {
    promise.then(resolve, reject);
  });
}

export { ObservablePromise };

IPromiseBasedObservable-s don't respect transactions

This is probably best explained by example:
https://codesandbox.io/s/L88MnpzRX

(see console)

Basically I have four observables - a and b are fromPromise IPromiseBasedObservable-s, and c and d are just boxed values. The autorun displays their values.

I change c and d within a runInAction and as expected the autorun runs once.
I change a and b within a runInAction and not as expected the autorun runs twice - once for each.

Expected output:

?, ?, c , d
?, ?, C , D
A, B, C , D

Actual output:

?, ?, c , d
?, ?, C , D
A, ?, C , D
A, B, C , D

I think this is just an inevitable side-effect of using promises, but I thought I'd raise it.

[Feature Request] Support for nested objects in ViewModel?

I've read the source and I understand perfectly how it works and why nested objects aren't supported by createViewModel, but what would it take to add support for that?

I'm creating ViewModels for objects like this

type DataEntry = {
    [key: string]: {
        $metadata; {}
        value: any
    }
} & {
    clear(): void
    set(data: {}): void
}

With clear setting all values to undefined and set taking an object representing the data entry and setting all the corresponding values.

Without support for nested objects I can't use either methods or set a value directly, with severely limits my use of the ViewModel.

I'm totally open to work on this myself, but I'd like to make sure that it makes sense and that it's doable before jumping in.

Thanks.

Concept of readable and writeable References

Problem

Lets say I have the following model:

interface Model {
  firstName: string;
  lastName: string;
}
const model: Model = observable({ firstName: "John", lastName: "Doe" });

And the following react component:

@observer
class MyTextBox extends React.Component<{ ... }, {}> { 
    render() { ... <Input type="text" onChange={this.onTextChange} /> ...  }
    onTextChange() { ... }
}

And now I want to have a view that has a MyTextBox for model.firstName and another one for model.lastName.
The default approach would be to to pass to functions to MyTextBox:

<MyTextBox getText={() => model.firstName} setText={val => model.firstName = val;} />
<MyTextBox getText={() => model.lastName} setText={val => model.lastName = val;} />

However, this is cumbersome and error prone as firstName and lastName must be referenced multiple times.

It would be very convenient, if I could just bind firstName somehow to the text value of the text box as supported by Microsofts WPF:

<MyTextBox text={binding(model.firstName)} />
<MyTextBox text={binding(model.lastName)} />

Clearly, binding can never be implemented in a way that this would work.

Proposed Solution

Instead of passing a getter and setter, one could combine both to a "reference":

interface Reference<T> {
    get(): T;
    set(value: T);
}

With the help of typescripts recently introduced keyof-feature, it becomes possible to create a reference to a property in a typesafe manner:

function ref<T, TKey extends keyof T>(obj: T, key: TKey): Reference<T[TKey]> {
    // ...
}

Using this function, the example above could be implemented as:

<MyTextBox text={ref(model, "firstName")} />
<MyTextBox text={ref(model, "lastName")} />

TypeScript checks that the properties firstName and lastName exist on model and that they have the required type of Reference<string> (assuming that text has this type).
Sadly, TypeScript tooling does not offer completion of the property name.

To overcome that issue, a little bit of black magic could be used to denote the property name:

<MyTextBox text={ref2(model, m => m.firstName)} />
<MyTextBox text={ref2(model, m => m.lastName)} />

With ref2 being implemented as following (ref2 could even be an overloading of ref):

function ref2<T, TProperty>(obj: T, memberAccessor: (obj: T) => TProperty): Reference<TProperty> {
    const dummy: { [key: string]: any } = {};
    for (const member in obj)
        dummy[member] = member;
    const key = memberAccessor(dummy as T) as string;
    return ref(obj, key);
}

As the concept of references turned out to be very useful in one of my projects and thus might be useful for others too, I think it might be great to have the interfaces Reference<T> as well as a ref-like function in mobx-utils.
What do you think?

`fromResource` should return subscription function from it's setup function

Most usage of fromResource follow this pattern

function createThing(x) {
   let subscription
   return fromResouce(
       sink => { subscription = x.subscribe(sink) },
       () => subscription()
   )
}

If the setup function returns the subscription function and if it would be stored in the resource, it could be simplified to:

function createThing(x) {
   return fromResouce(
       sink => x.subscribe(sink)
   )
}

best way to use the lib

I'm not sure if the fromPromise object can be mutated directly or I need to use set function? The same question applies to the createViewModal, it seems that we can mutate viewModal directly without having set function. And can we createViewModal from fromPromise? Thanks!

Resolving memory leak using asyncAction

New to mobx-utils, so I suspect this is more a question for clarification than an issue report. Please direct me accordingly and thanks in advance for any help.

What I want to do is use a store that I can refresh when I know new service data is available like lazyObservable, but easily attach more computed values and action functions. The endgame is to have a method for writing stores that's easy to teach to my team.

Starting from create-react-app, install the shown dependencies and use this app script.

import React from 'react';
import ReactDOM from 'react-dom';
import {action, computed, extendObservable, observable} from 'mobx';
import {observer} from 'mobx-react';
import {asyncAction} from 'mobx-utils';

const flipACoin = () => Math.random() > 0.5;

function determineGodliness() {
  const screwYourRAM = new Array(1000000).fill(Math.random());

  return new Promise((resolve) => {
    setTimeout(() => {
      resolve([flipACoin(), flipACoin()]);
    }, 1500);
  });
}

function godStore() {
  const store = observable({
    alpha: false,
    omega: false,
    waiting: false,
    refresh: asyncAction(function *() {
      this.waiting = true;
      [this.alpha, this.omega] = yield determineGodliness();
      this.waiting = false;
    }),
    get isGod() {
      return this.alpha && this.omega;
    }
  });

  store.refresh();

  return store;
}

window.store = godStore();

const App = observer(({store}) => <p>{
  (store.waiting)
    ? 'The suspense is killing me!'
    : (store.isGod)
      ? 'I am the Alpha and the Omega'
      : 'I just work here.'
}</p>);

ReactDOM.render(<App store={window.store} />, document.getElementById('root'));

Task manager shows memory usage increase every time you run window.store.refresh() in the console. Interestingly enough, using setInterval(window.store.refresh, 3000) actually causes oscillation in memory usage instead of a linear climb.

What can I do to be sure screwYourRAM will get garbage collected? All I care about keeping is what's returned from the generator, not what was allocated in the interim.

how do I use fromStream with mobx-react observer?

in TS, when I do:

const someSignal = fromStream(someStream);

I only get someSignal of type:

{
   current: {},
   dispose: () => ...
}

which ...doesn't seem like a mobx observable.
Is there an example of using fromStream with mobx-react's observer?

Thank you!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.