UNPKG

34.5 kBMarkdownView Raw
1# MobX-utils
2
3_Utility functions and common patterns for MobX_
4
5[![Build Status](https://travis-ci.org/mobxjs/mobx-utils.svg?branch=master)](https://travis-ci.org/mobxjs/mobx-utils)
6[![Coverage Status](https://coveralls.io/repos/github/mobxjs/mobx-utils/badge.svg?branch=master)](https://coveralls.io/github/mobxjs/mobx-utils?branch=master)
7[![Join the chat at https://gitter.im/mobxjs/mobx](https://badges.gitter.im/mobxjs/mobx.svg)](https://gitter.im/mobxjs/mobx?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
8[![npm](https://img.shields.io/npm/v/mobx-utils)](https://www.npmjs.com/package/mobx-utils)
9
10This package provides utility functions and common MobX patterns build on top of MobX.
11It is encouraged to take a peek under the hood and read the sources of these utilities.
12Feel free to open a PR with your own utilities. For large new features, please open an issue first.
13
14# Installation & Usage
15
16NPM: `npm install mobx-utils --save`
17
18CDN: <https://unpkg.com/mobx-utils/mobx-utils.umd.js>
19
20`import {function_name} from 'mobx-utils'`
21
22# API
23
24<!-- Generated by documentation.js. Update this documentation by updating the source code. -->
25
26### Table of Contents
27
28- [fromPromise](#frompromise)
29 - [Parameters](#parameters)
30 - [Examples](#examples)
31- [isPromiseBasedObservable](#ispromisebasedobservable)
32 - [Parameters](#parameters-1)
33- [moveItem](#moveitem)
34 - [Parameters](#parameters-2)
35 - [Examples](#examples-1)
36- [lazyObservable](#lazyobservable)
37 - [Parameters](#parameters-3)
38 - [Examples](#examples-2)
39- [fromResource](#fromresource)
40 - [Parameters](#parameters-4)
41 - [Examples](#examples-3)
42- [toStream](#tostream)
43 - [Parameters](#parameters-5)
44 - [Examples](#examples-4)
45- [StreamListener](#streamlistener)
46- [ViewModel](#viewmodel)
47- [createViewModel](#createviewmodel)
48 - [Parameters](#parameters-6)
49 - [Examples](#examples-5)
50- [keepAlive](#keepalive)
51 - [Parameters](#parameters-7)
52 - [Examples](#examples-6)
53- [keepAlive](#keepalive-1)
54 - [Parameters](#parameters-8)
55 - [Examples](#examples-7)
56- [queueProcessor](#queueprocessor)
57 - [Parameters](#parameters-9)
58 - [Examples](#examples-8)
59- [chunkProcessor](#chunkprocessor)
60 - [Parameters](#parameters-10)
61 - [Examples](#examples-9)
62- [resetNowInternalState](#resetnowinternalstate)
63 - [Examples](#examples-10)
64- [now](#now)
65 - [Parameters](#parameters-11)
66 - [Examples](#examples-11)
67- [expr](#expr)
68 - [Parameters](#parameters-12)
69 - [Examples](#examples-12)
70- [createTransformer](#createtransformer)
71 - [Parameters](#parameters-13)
72- [deepObserve](#deepobserve)
73 - [Parameters](#parameters-14)
74 - [Examples](#examples-13)
75- [ObservableGroupMap](#observablegroupmap)
76 - [Parameters](#parameters-15)
77 - [Examples](#examples-14)
78- [ObservableMap](#observablemap)
79- [defineProperty](#defineproperty)
80- [defineProperty](#defineproperty-1)
81- [defineProperty](#defineproperty-2)
82- [defineProperty](#defineproperty-3)
83- [defineProperty](#defineproperty-4)
84- [computedFn](#computedfn)
85 - [Parameters](#parameters-16)
86 - [Examples](#examples-15)
87- [DeepMapEntry](#deepmapentry)
88- [DeepMap](#deepmap)
89
90## fromPromise
91
92`fromPromise` takes a Promise, extends it with 2 observable properties that track
93the status of the promise and returns it. The returned object has the following observable properties:
94
95- `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.
96- `state`: one of `"pending"`, `"fulfilled"` or `"rejected"`
97
98And the following methods:
99
100- `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.
101
102The returned object implements `PromiseLike<TValue>`, so you can chain additional `Promise` handlers using `then`. You may also use it with `await` in `async` functions.
103
104Note that the status strings are available as constants:
105`mobxUtils.PENDING`, `mobxUtils.REJECTED`, `mobxUtil.FULFILLED`
106
107fromPromise takes an optional second argument, a previously created `fromPromise` based observable.
108This is useful to replace one promise based observable with another, without going back to an intermediate
109"pending" promise state while fetching data. For example:
110
111### Parameters
112
113- `origPromise` The promise which will be observed
114- `oldPromise` The previously observed promise
115
116### Examples
117
118```javascript
119@observer
120class SearchResults extends React.Component {
121 @observable.ref searchResults
122
123 componentDidUpdate(nextProps) {
124 if (nextProps.query !== this.props.query)
125 this.searchResults = fromPromise(
126 window.fetch("/search?q=" + nextProps.query),
127 // by passing, we won't render a pending state if we had a successful search query before
128 // rather, we will keep showing the previous search results, until the new promise resolves (or rejects)
129 this.searchResults
130 )
131 }
132
133 render() {
134 return this.searchResults.case({
135 pending: (staleValue) => {
136 return staleValue || "searching" // <- value might set to previous results while the promise is still pending
137 },
138 fulfilled: (value) => {
139 return value // the fresh results
140 },
141 rejected: (error) => {
142 return "Oops: " + error
143 }
144 })
145 }
146}
147
148Observable promises can be created immediately in a certain state using
149`fromPromise.reject(reason)` or `fromPromise.resolve(value?)`.
150The main advantage of `fromPromise.resolve(value)` over `fromPromise(Promise.resolve(value))` is that the first _synchronously_ starts in the desired state.
151
152It is possible to directly create a promise using a resolve, reject function:
153`fromPromise((resolve, reject) => setTimeout(() => resolve(true), 1000))`
154```
155
156```javascript
157const fetchResult = fromPromise(fetch("http://someurl"))
158
159// combine with when..
160when(
161 () => fetchResult.state !== "pending",
162 () => {
163 console.log("Got ", fetchResult.value)
164 }
165)
166
167// or a mobx-react component..
168const myComponent = observer(({ fetchResult }) => {
169 switch(fetchResult.state) {
170 case "pending": return <div>Loading...</div>
171 case "rejected": return <div>Ooops... {fetchResult.value}</div>
172 case "fulfilled": return <div>Gotcha: {fetchResult.value}</div>
173 }
174})
175
176// or using the case method instead of switch:
177
178const myComponent = observer(({ fetchResult }) =>
179 fetchResult.case({
180 pending: () => <div>Loading...</div>,
181 rejected: error => <div>Ooops.. {error}</div>,
182 fulfilled: value => <div>Gotcha: {value}</div>,
183 }))
184
185// chain additional handler(s) to the resolve/reject:
186
187fetchResult.then(
188 (result) => doSomeTransformation(result),
189 (rejectReason) => console.error('fetchResult was rejected, reason: ' + rejectReason)
190).then(
191 (transformedResult) => console.log('transformed fetchResult: ' + transformedResult)
192)
193```
194
195Returns **any** origPromise with added properties and methods described above.
196
197## isPromiseBasedObservable
198
199Returns true if the provided value is a promise-based observable.
200
201### Parameters
202
203- `value` any
204
205Returns **[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)**
206
207## moveItem
208
209Moves an item from one position to another, checking that the indexes given are within bounds.
210
211### Parameters
212
213- `target` **ObservableArray&lt;T>**
214- `fromIndex` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)**
215- `toIndex` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)**
216
217### Examples
218
219```javascript
220const source = observable([1, 2, 3])
221moveItem(source, 0, 1)
222console.log(source.map(x => x)) // [2, 1, 3]
223```
224
225Returns **ObservableArray&lt;T>**
226
227## lazyObservable
228
229`lazyObservable` creates an observable around a `fetch` method that will not be invoked
230until the observable is needed the first time.
231The fetch method receives a `sink` callback which can be used to replace the
232current value of the lazyObservable. It is allowed to call `sink` multiple times
233to keep the lazyObservable up to date with some external resource.
234
235Note that it is the `current()` call itself which is being tracked by MobX,
236so make sure that you don't dereference to early.
237
238### Parameters
239
240- `fetch`
241- `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`)
242
243### Examples
244
245```javascript
246const userProfile = lazyObservable(
247 sink => fetch("/myprofile").then(profile => sink(profile))
248)
249
250// use the userProfile in a React component:
251const Profile = observer(({ userProfile }) =>
252 userProfile.current() === undefined
253 ? <div>Loading user profile...</div>
254 : <div>{userProfile.current().displayName}</div>
255)
256
257// triggers refresh the userProfile
258userProfile.refresh()
259```
260
261## fromResource
262
263`fromResource` creates an observable whose current state can be inspected using `.current()`,
264and which can be kept in sync with some external datasource that can be subscribed to.
265
266The created observable will only subscribe to the datasource if it is in use somewhere,
267(un)subscribing when needed. To enable `fromResource` to do that two callbacks need to be provided,
268one to subscribe, and one to unsubscribe. The subscribe callback itself will receive a `sink` callback, which can be used
269to update the current state of the observable, allowing observes to react.
270
271Whatever is passed to `sink` will be returned by `current()`. The values passed to the sink will not be converted to
272observables automatically, but feel free to do so.
273It is the `current()` call itself which is being tracked,
274so make sure that you don't dereference to early.
275
276For inspiration, an example integration with the apollo-client on [github](https://github.com/apollostack/apollo-client/issues/503#issuecomment-241101379),
277or the [implementation](https://github.com/mobxjs/mobx-utils/blob/1d17cf7f7f5200937f68cc0b5e7ec7f3f71dccba/src/now.ts#L43-L57) of `mobxUtils.now`
278
279The following example code creates an observable that connects to a `dbUserRecord`,
280which comes from an imaginary database and notifies when it has changed.
281
282### Parameters
283
284- `subscriber`
285- `unsubscriber` **IDisposer** (optional, default `NOOP`)
286- `initialValue` **T** the data that will be returned by `get()` until the `sink` has emitted its first data (optional, default `undefined`)
287
288### Examples
289
290```javascript
291function createObservableUser(dbUserRecord) {
292 let currentSubscription;
293 return fromResource(
294 (sink) => {
295 // sink the current state
296 sink(dbUserRecord.fields)
297 // subscribe to the record, invoke the sink callback whenever new data arrives
298 currentSubscription = dbUserRecord.onUpdated(() => {
299 sink(dbUserRecord.fields)
300 })
301 },
302 () => {
303 // the user observable is not in use at the moment, unsubscribe (for now)
304 dbUserRecord.unsubscribe(currentSubscription)
305 }
306 )
307}
308
309// usage:
310const myUserObservable = createObservableUser(myDatabaseConnector.query("name = 'Michel'"))
311
312// use the observable in autorun
313autorun(() => {
314 // printed everytime the database updates its records
315 console.log(myUserObservable.current().displayName)
316})
317
318// ... or a component
319const userComponent = observer(({ user }) =>
320 <div>{user.current().displayName}</div>
321)
322```
323
324## toStream
325
326Converts an expression to an observable stream (a.k.a. TC 39 Observable / RxJS observable).
327The provided expression is tracked by mobx as long as there are subscribers, automatically
328emitting when new values become available. The expressions respect (trans)actions.
329
330### Parameters
331
332- `expression`
333- `fireImmediately` **[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** (by default false)
334
335### Examples
336
337```javascript
338const user = observable({
339 firstName: "C.S",
340 lastName: "Lewis"
341})
342
343Rx.Observable
344 .from(mobxUtils.toStream(() => user.firstname + user.lastName))
345 .scan(nameChanges => nameChanges + 1, 0)
346 .subscribe(nameChanges => console.log("Changed name ", nameChanges, "times"))
347```
348
349Returns **IObservableStream&lt;T>**
350
351## StreamListener
352
353## ViewModel
354
355## createViewModel
356
357`createViewModel` takes an object with observable properties (model)
358and wraps a viewmodel around it. The viewmodel proxies all enumerable properties of the original model with the following behavior:
359
360- as long as no new value has been assigned to the viewmodel property, the original property will be returned.
361- 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.
362- 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.
363
364The viewmodel exposes the following additional methods, besides all the enumerable properties of the model:
365
366- `submit()`: copies all the values of the viewmodel to the model and resets the state
367- `reset()`: resets the state of the viewmodel, abandoning all local modifications
368- `resetProperty(propName)`: resets the specified property of the viewmodel
369- `isDirty`: observable property indicating if the viewModel contains any modifications
370- `isPropertyDirty(propName)`: returns true if the specified property is dirty
371- `changedValues`: returns a key / value map with the properties that have been changed in the model so far
372- `model`: The original model object for which this viewModel was created
373
374You 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.
375Note 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()`.
376
377### Parameters
378
379- `model` **T**
380
381### Examples
382
383```javascript
384class Todo {
385 @observable title = "Test"
386}
387
388const model = new Todo()
389const viewModel = createViewModel(model);
390
391autorun(() => console.log(viewModel.model.title, ",", viewModel.title))
392// prints "Test, Test"
393model.title = "Get coffee"
394// prints "Get coffee, Get coffee", viewModel just proxies to model
395viewModel.title = "Get tea"
396// prints "Get coffee, Get tea", viewModel's title is now dirty, and the local value will be printed
397viewModel.submit()
398// prints "Get tea, Get tea", changes submitted from the viewModel to the model, viewModel is proxying again
399viewModel.title = "Get cookie"
400// prints "Get tea, Get cookie" // viewModel has diverged again
401viewModel.reset()
402// prints "Get tea, Get tea", changes of the viewModel have been abandoned
403```
404
405## keepAlive
406
407MobX normally suspends any computed value that is not in use by any reaction,
408and lazily re-evaluates the expression if needed outside a reaction while not in use.
409`keepAlive` marks a computed value as always in use, meaning that it will always fresh, but never disposed automatically.
410
411### Parameters
412
413- `_1`
414- `_2`
415- `target` **[Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)** an object that has a computed property, created by `@computed` or `extendObservable`
416- `property` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** the name of the property to keep alive
417
418### Examples
419
420```javascript
421const obj = observable({
422 number: 3,
423 doubler: function() { return this.number * 2 }
424})
425const stop = keepAlive(obj, "doubler")
426```
427
428Returns **IDisposer** stops this keep alive so that the computed value goes back to normal behavior
429
430## keepAlive
431
432### Parameters
433
434- `_1`
435- `_2`
436- `computedValue` **IComputedValue&lt;any>** created using the `computed` function
437
438### Examples
439
440```javascript
441const number = observable(3)
442const doubler = computed(() => number.get() * 2)
443const stop = keepAlive(doubler)
444// doubler will now stay in sync reactively even when there are no further observers
445stop()
446// normal behavior, doubler results will be recomputed if not observed but needed, but lazily
447```
448
449Returns **IDisposer** stops this keep alive so that the computed value goes back to normal behavior
450
451## queueProcessor
452
453`queueProcessor` takes an observable array, observes it and calls `processor`
454once for each item added to the observable array, optionally debouncing the action
455
456### Parameters
457
458- `observableArray` **[Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)&lt;T>** observable array instance to track
459- `processor`
460- `debounce` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)** optional debounce time in ms. With debounce 0 the processor will run synchronously (optional, default `0`)
461
462### Examples
463
464```javascript
465const pendingNotifications = observable([])
466const stop = queueProcessor(pendingNotifications, msg => {
467 // show Desktop notification
468 new Notification(msg);
469})
470
471// usage:
472pendingNotifications.push("test!")
473```
474
475Returns **IDisposer** stops the processor
476
477## chunkProcessor
478
479`chunkProcessor` takes an observable array, observes it and calls `processor`
480once for a chunk of items added to the observable array, optionally deboucing the action.
481The maximum chunk size can be limited by number.
482This allows both, splitting larger into smaller chunks or (when debounced) combining smaller
483chunks and/or single items into reasonable chunks of work.
484
485### Parameters
486
487- `observableArray` **[Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)&lt;T>** observable array instance to track
488- `processor`
489- `debounce` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)** optional debounce time in ms. With debounce 0 the processor will run synchronously (optional, default `0`)
490- `maxChunkSize` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)** optionally do not call on full array but smaller chunks. With 0 it will process the full array. (optional, default `0`)
491
492### Examples
493
494```javascript
495const trackedActions = observable([])
496const stop = chunkProcessor(trackedActions, chunkOfMax10Items => {
497 sendTrackedActionsToServer(chunkOfMax10Items);
498}, 100, 10)
499
500// usage:
501trackedActions.push("scrolled")
502trackedActions.push("hoveredButton")
503// when both pushes happen within 100ms, there will be only one call to server
504```
505
506Returns **IDisposer** stops the processor
507
508## resetNowInternalState
509
510Disposes of all the internal Observables created by invocations of `now()`.
511
512The use case for this is to ensure that unit tests can run independent of each other.
513You should not call this in regular application code.
514
515### Examples
516
517```javascript
518afterEach(() => {
519 utils.resetNowInternalState()
520})
521```
522
523## now
524
525Returns the current date time as epoch number.
526The date time is read from an observable which is updated automatically after the given interval.
527So basically it treats time as an observable.
528
529The function takes an interval as parameter, which indicates how often `now()` will return a new value.
530If no interval is given, it will update each second. If "frame" is specified, it will update each time a
531`requestAnimationFrame` is available.
532
533Multiple clocks with the same interval will automatically be synchronized.
534
535Countdown example: <https://jsfiddle.net/mweststrate/na0qdmkw/>
536
537### Parameters
538
539- `interval` **([number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number) \| `"frame"`)** interval in milliseconds about how often the interval should update (optional, default `1000`)
540
541### Examples
542
543```javascript
544const start = Date.now()
545
546autorun(() => {
547 console.log("Seconds elapsed: ", (mobxUtils.now() - start) / 1000)
548})
549```
550
551## expr
552
553`expr` can be used to create temporary computed values inside computed values.
554Nesting computed values is useful to create cheap computations in order to prevent expensive computations from needing to run.
555In the following example the expression prevents that a component is rerender _each time_ the selection changes;
556instead it will only rerenders when the current todo is (de)selected.
557
558`expr(func)` is an alias for `computed(func).get()`.
559Please note that the function given to `expr` is evaluated _twice_ in the scenario that the overall expression value changes.
560It is evaluated the first time when any observables it depends on change.
561It 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.
562
563In the following example, the expression prevents the `TodoView` component from being re-rendered if the selection changes elsewhere.
564Instead, the component will only re-render when the relevant todo is (de)selected, which happens much less frequently.
565
566### Parameters
567
568- `expr`
569
570### Examples
571
572```javascript
573const Todo = observer((props) => {
574 const todo = props.todo
575 const isSelected = mobxUtils.expr(() => props.viewState.selection === todo)
576const TodoView = observer(({ todo, editorState }) => {
577 const isSelected = mobxUtils.expr(() => editorState.selection === todo)
578 return <div className={isSelected ? "todo todo-selected" : "todo"}>{todo.title}</div>
579})
580```
581
582## createTransformer
583
584Creates a function that maps an object to a view.
585The mapping is memoized.
586
587See the [transformer](#createtransformer-in-detail) section for more details.
588
589### Parameters
590
591- `transformer`
592- `arg2`
593- `onCleanup`
594
595## deepObserve
596
597Given an object, deeply observes the given object.
598It is like `observe` from mobx, but applied recursively, including all future children.
599
600Note that the given object cannot ever contain cycles and should be a tree.
601
602As benefit: path and root will be provided in the callback, so the signature of the listener is
603(change, path, root) => void
604
605The returned disposer can be invoked to clean up the listener
606
607deepObserve cannot be used on computed values.
608
609### Parameters
610
611- `target`
612- `listener`
613
614### Examples
615
616```javascript
617const disposer = deepObserve(target, (change, path) => {
618 console.dir(change)
619})
620```
621
622## ObservableGroupMap
623
624Reactively sorts a base observable array into multiple observable arrays based on the value of a
625`groupBy: (item: T) => G` function.
626
627This observes the individual computed groupBy values and only updates the source and dest arrays
628when there is an actual change, so this is far more efficient than, for example
629`base.filter(i => groupBy(i) === 'we')`. Call #dispose() to stop tracking.
630
631No guarantees are made about the order of items in the grouped arrays.
632
633The resulting map of arrays is read-only. clear(), set(), delete() are not supported and
634modifying the group arrays will lead to undefined behavior.
635
636NB: ObservableGroupMap relies on `Symbol`s. If you are targeting a platform which doesn't
637support these natively, you will need to provide a polyfill.
638
639### Parameters
640
641- `base` **[array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)** The array to sort into groups.
642- `groupBy` **[function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function)** The function used for grouping.
643- `options` Object with properties:
644 `name`: Debug name of this ObservableGroupMap.
645 `keyToName`: Function to create the debug names of the observable group arrays.
646
647### Examples
648
649```javascript
650const slices = observable([
651 { day: "mo", hours: 12 },
652 { day: "tu", hours: 2 },
653])
654const slicesByDay = new ObservableGroupMap(slices, (slice) => slice.day)
655autorun(() => console.log(
656 slicesByDay.get("mo")?.length ?? 0,
657 slicesByDay.get("we"))) // outputs 1, undefined
658slices[0].day = "we" // outputs 0, [{ day: "we", hours: 12 }]
659```
660
661## ObservableMap
662
663## defineProperty
664
665Base observable array which is being sorted into groups.
666
667## defineProperty
668
669The ObservableGroupMap needs to track some state per-item. This is the name/symbol of the
670property used to attach the state.
671
672## defineProperty
673
674The function used to group the items.
675
676## defineProperty
677
678This function is used to generate the mobx debug names of the observable group arrays.
679
680## defineProperty
681
682Disposes all observers created during construction and removes state added to base array
683items.
684
685## computedFn
686
687computedFn takes a function with an arbitrary amount of arguments,
688and memoizes the output of the function based on the arguments passed in.
689
690computedFn(fn) returns a function with the very same signature. There is no limit on the amount of arguments
691that is accepted. However, the amount of arguments must be constant and default arguments are not supported.
692
693By default the output of a function call will only be memoized as long as the
694output is being observed.
695
696The function passes into `computedFn` should be pure, not be an action and only be relying on
697observables.
698
699Setting `keepAlive` to `true` will cause the output to be forcefully cached forever.
700Note that this might introduce memory leaks!
701
702### Parameters
703
704- `fn`
705- `keepAliveOrOptions`
706
707### Examples
708
709```javascript
710const store = observable({
711a: 1,
712b: 2,
713c: 3,
714m: computedFn(function(x) {
715return this.a * this.b * x
716})
717})
718
719const d = autorun(() => {
720// store.m(3) will be cached as long as this autorun is running
721console.log(store.m(3) * store.c)
722})
723```
724
725## DeepMapEntry
726
727## DeepMap
728
729# Details
730
731## createTransformer in detail
732
733With `createTransformer` it is very easy to transform a complete data graph into another data graph.
734Transformation functions can be composed so that you can build a tree using lots of small transformations.
735The resulting data graph will never be stale, it will be kept in sync with the source by applying small patches to the result graph.
736This makes it very easy to achieve powerful patterns similar to sideways data loading, map-reduce, tracking state history using immutable data structures etc.
737
738`createTransformer` turns a function (that should transform value `A` into another value `B`) into a reactive and memoizing function.
739In 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.
740However, if A changes, the transformation will be re-applied so that B is updated accordingly.
741And last but not least, if nobody is using the transformation of a specific A anymore, its entry will be removed from the memoization table.
742
743The optional `onCleanup` function can be used to get a notification when a transformation of an object is no longer needed.
744This can be used to dispose resources attached to the result object if needed.
745
746Always use transformations inside a reaction like `observer` or `autorun`.
747
748Transformations will, like any other computed value, fall back to lazy evaluation if not observed by something, which sort of defeats their purpose.
749
750### Parameters
751
752- \`transformation: (value: A) => B
753- `onCleanup?: (result: B, value?: A) => void)`
754-
755
756`createTransformer<A, B>(transformation: (value: A) => B, onCleanup?: (result: B, value?: A) => void): (value: A) => B`
757
758## Examples
759
760This 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:
761
762### Tracking mutable state using immutable, shared data structures.
763
764This example is taken from the [Reactive2015 conference demo](https://github.com/mobxjs/mobx-reactive2015-demo):
765
766```javascript
767/*
768 The store that holds our domain: boxes and arrows
769*/
770const store = observable({
771 boxes: [],
772 arrows: [],
773 selection: null,
774})
775
776/**
777 Serialize store to json upon each change and push it onto the states list
778*/
779const states = []
780
781autorun(() => {
782 states.push(serializeState(store))
783})
784
785const serializeState = createTransformer((store) => ({
786 boxes: store.boxes.map(serializeBox),
787 arrows: store.arrows.map(serializeArrow),
788 selection: store.selection ? store.selection.id : null,
789}))
790
791const serializeBox = createTransformer((box) => ({ ...box }))
792
793const serializeArrow = createTransformer((arrow) => ({
794 id: arrow.id,
795 to: arrow.to.id,
796 from: arrow.from.id,
797}))
798```
799
800In this example the state is serialized by composing three different transformation functions.
801The autorunner triggers the serialization of the `store` object, which in turn serializes all boxes and arrows.
802Let's take closer look at the life of an imaginary example box#3.
803
8041. The first time box#3 is passed by `map` to `serializeBox`,
805 the serializeBox transformation is executed and an entry containing box#3 and its serialized representation is added to the internal memoization table of `serializeBox`.
8062. Imagine that another box is added to the `store.boxes` list.
807 This would cause the `serializeState` function to re-compute, resulting in a complete remapping of all the boxes.
808 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.
8093. 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.
810 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.
811 That's the `serializeState` transformation in this case.
812 `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.
8134. Finally, if box#3 is removed from `store.boxes`, `serializeState` will compute again.
814 But since it will no longer be using the application of `serializeBox` to box#3,
815 that reactive function will go back to non-reactive mode.
816 This signals the memoization table that the entry can be removed so that it is ready for GC.
817
818So effectively we have achieved state tracking using immutable, shared datas structures here.
819All boxes and arrows are mapped and reduced into single state tree.
820Each 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.
821
822### Transforming a datagraph into another reactive data graph
823
824Instead of returning plain values from a transformation function, it is also possible to return observable objects.
825This 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.
826
827Here is a small example that encodes a reactive file explorer that will update its representation upon each change.
828Data graphs that are built this way will in general react a lot faster and will consist of much more straight-forward code,
829compared to derived data graph that are updated using your own code. See the [performance tests](https://github.com/mobxjs/mobx/blob/3ea1f4af20a51a1cb30be3e4a55ec8f964a8c495/test/perf/transform-perf.js#L4) for some examples.
830
831Unlike the previous example, the `transformFolder` will only run once as long as a folder remains visible;
832the `DisplayFolder` objects track the associated `Folder` objects themselves.
833
834In the following example all mutations to the `state` graph will be processed automatically.
835Some examples:
836
8371. Changing the name of a folder will update its own `path` property and the `path` property of all its descendants.
8382. Collapsing a folder will remove all descendant `DisplayFolders` from the tree.
8393. Expanding a folder will restore them again.
8404. Setting a search filter will remove all nodes that do not match the filter, unless they have a descendant that matches the filter.
8415. Etc.
842
843```javascript
844import {extendObservable, observable, createTransformer, autorun} from "mobx"
845
846function Folder(parent, name) {
847 this.parent = parent;
848 extendObservable(this, {
849 name: name,
850 children: observable.shallow([]),
851 });
852}
853
854function DisplayFolder(folder, state) {
855 this.state = state;
856 this.folder = folder;
857 extendObservable(this, {
858 collapsed: false,
859 get name() {
860 return this.folder.name;
861 },
862 get isVisible() {
863 return !this.state.filter || this.name.indexOf(this.state.filter) !== -1 || this.children.some(child => child.isVisible);
864 },
865 get children() {
866 if (this.collapsed)
867 return [];
868 return this.folder.children.map(transformFolder).filter(function(child) {
869 return child.isVisible;
870 })
871 },
872 get path() {
873 return this.folder.parent === null ? this.name : transformFolder(this.folder.parent).path + "/" + this.name;
874 })
875 });
876}
877
878var state = observable({
879 root: new Folder(null, "root"),
880 filter: null,
881 displayRoot: null
882});
883
884var transformFolder = createTransformer(function (folder) {
885 return new DisplayFolder(folder, state);
886});
887
888
889// returns list of strings per folder
890var stringTransformer = createTransformer(function (displayFolder) {
891 var path = displayFolder.path;
892 return path + "\n" +
893 displayFolder.children.filter(function(child) {
894 return child.isVisible;
895 }).map(stringTransformer).join('');
896});
897
898function createFolders(parent, recursion) {
899 if (recursion === 0)
900 return;
901 for (var i = 0; i < 3; i++) {
902 var folder = new Folder(parent, i + '');
903 parent.children.push(folder);
904 createFolders(folder, recursion - 1);
905 }
906}
907
908createFolders(state.root, 2); // 3^2
909
910autorun(function() {
911 state.displayRoot = transformFolder(state.root);
912 state.text = stringTransformer(state.displayRoot)
913 console.log(state.text)
914});
915
916state.root.name = 'wow'; // change folder name
917state.displayRoot.children[1].collapsed = true; // collapse folder
918state.filter = "2"; // search
919state.filter = null; // unsearch
920```