Understanding Observable: not updating subscriber for an array - javascript

I am trying to wrap my head around observables. I know when a value changes in observer, observable notify all its subscriber that something has changed. I am not sure why below code doesn't work. my understading is that once i add another element in array, subscriber should log new value or maybe log all values.
can someone please explain why is that?
import { Observable } from 'rxjs';
var numbers = [5, 1, 2, 7, 10];
// let source = Observable.from(numbers);
let source = Observable.create(observer => {
for (let n of numbers)
observer.next(n);
observer.complete();
});
source.subscribe(
value => console.log(`value is ${value}`),
error => console.log(`error is ${error}`),
() => console.log(`completed!`)
);
setTimeout(function () {
console.log("pushing new value");
numbers.push(33);
}, 3000);
I tried commenting observer.complete() as I thought that might be the culprit.

The observable from operator ( which is similar to the observable you've created ) does not 'listen' the array for it's onPush event ( no array has one in the default implementation ). The values you will get from the stream will be the one inside the array just before the subscribe call. The observable per se is just a function, it will just "copy" those values and emit them one by one, your array does not know how to "push" values to the stream. In the reactive world, everithing is a stream, most of the values you would store inside an array are stored in a stream of those values.

Related

variable inside Observable subscription gets empty value

I know that Observables take some time to get data while javascript keeps running the others codes and that is troubling me a lot.
I have used ngrx in my angular project. Here, I am trying to fetch some data from the store which is working fine. Then, I convert this data stream into string[] which is also working fine.
To use this string[] me subscribeto this observable. And inside subscription I try to assign the value to other values named filterSizeValues.
Here, the problem comes. If I console.logthis filterSizeValuesinitially I got and empty array. When the observable finishes his job filterSizeValues variable is filled with data.
But I can not effort filterSizeValues variable to be empty array initially. What can I do?
I have already searched the solution in the internet but nothing is working out.
Help me out please. And Many Many Thanks in advance.
Here is my code;
this.sizeTargetingStore$.dispatch(SizeTargetingActions.getSizeTargeting({
campaignId: this.campaignId,
lineItemId: this.lineItemId
}));
Here I am accessing the store to get data.
this.sizeTargeting$
.pipe(switchMap(sizes=>{
let temporary:string[] = [];
sizes.forEach(eachSize=>{
temporary.push(eachSize.name);
})
this.filterSizeValues$ = of(temporary);
return this.filterSizeValues$;
}))
.subscribe(size_name=>{
this.filters.set('size_name', size_name);
})
Here, I am trying to set the filter values.
I also tried this way also.
this.sizeTargeting$
.pipe(switchMap(sizes=>{
let temporary:string[] = [];
sizes.forEach(eachSize=>{
temporary.push(eachSize.name);
})
this.filterSizeValues$ = of(temporary);
return this.filterSizeValues$;
}))
.subscribe(size_name=>{
this.filterSizeValues = size_name
})
this.filters.set('size_name', this.filterSizeValues);
But all ways filters set to an empty array.
Anyone can help me out please?
From my understanding, you have 2 possibilities, either filter out the empty values or skip the first value. You can do so with the filter and skip rxjs operator respectively.
Also I believe that you are misusing the switchMap operator, since you are not using asynchronous operations within your switchMap we can use the map operator instead, so below I have a simplified version of your code with your 2 options to fix your problem.
Option 1:
this.sizeTargeting$.pipe(
filter(sizes => sizes.length > 0), // filter out empty array values
map(sizes => sizes.map(size => size.name)) // perform your remap
).subscribe(sizes => {
this.filterSizeValues = size_name; // Only arrays with values will reach this step
});
Option 2:
this.sizeTargeting$.pipe(
skip(1), // skip the first value
map(sizes => sizes.map(size => size.name)) // perform your remap
).subscribe(sizes => {
this.filterSizeValues = size_name; // Only arrays with values will reach this step
});
Normally when I subscribe to something that I am waiting on to return what I do is I set up a Subject:
private componentDestroyed$ = new Subject<void>();
then in the Observable piping and subscription I do it as:
this.sizeTargeting$
.pipe(takeUntil(this.componentDestroyed$))
.subscribe((sizes: YourTypeHere[]) => {
if(sizes) {
//Do what I need to do with my sizes here, populate what I need,
//dispatch any other actions needed.
}
})

Struggling with flatMap vs concatMap in rxJs

I am struggling to understand the difference between the flatMap and concatMap in rxJs.
The most clear answer that I could understand was that here difference-between-concatmap-and-flatmap
So I went and tried things out by my self.
import "./styles.css";
import { switchMap, flatMap, concatMap } from "rxjs/operators";
import { fromFetch } from "rxjs/fetch";
import { Observable } from "rxjs";
function createObs1() {
return new Observable<number>((subscriber) => {
setTimeout(() => {
subscriber.next(1);
subscriber.complete();
}, 900);
});
}
function createObs2() {
return new Observable<number>((subscriber) => {
setTimeout(() => {
subscriber.next(2);
//subscriber.next(22);
//subscriber.next(222);
subscriber.complete();
}, 800);
});
}
function createObs3() {
return new Observable<number>((subscriber) => {
setTimeout(() => {
subscriber.next(3);
//subscriber.next(33);
//subscriber.next(333);
subscriber.complete();
}, 700);
});
}
function createObs4() {
return new Observable<number>((subscriber) => {
setTimeout(() => {
subscriber.next(4);
subscriber.complete();
}, 600);
});
}
function createObs5() {
return new Observable<number>((subscriber) => {
setTimeout(() => {
subscriber.next(5);
subscriber.complete();
}, 500);
});
}
createObs1()
.pipe(
flatMap((resp) => {
console.log(resp);
return createObs2();
}),
flatMap((resp) => {
console.log(resp);
return createObs3();
}),
flatMap((resp) => {
console.log(resp);
return createObs4();
}),
flatMap((resp) => {
console.log(resp);
return createObs5();
})
)
.subscribe((resp) => console.log(resp));
console.log("hellooo");
I have used that playground here playground example
Questions
1)
From my understanding the use of flatMap should mix the outputs so that the console logs are like (1,3,2,4,5). I have tried more than 30 times and always come on the same row (1, 2, 3, 4, 5)
What am I doing wrong or have undestood wrong?
2)
If on createObs2() and createObs3() you remove the comments and include the code with multiple emitted events then things get messy. Even if you change to concatMap it messes things and results come mixed. Multiple numbers that I expect only once come multiple times. The result can be (1, 2, 33, 3, 2, 22, 3, 33, 4, 5, 4, 3, 4, 5) Why this happens?
How I test the example on playground. I just remove only 1 letter from the last console.log("hello"). Only one change for example console.log("heloo") and is then observed and project is compiled again and output printed in console.
Edit: The reason I have gone to flatMap and concatMap was to find a replacement for nested subscriptions in angular using the http library.
createObs1().subscribe( (resp1) => {
console.log(resp1);
createObs2().subscribe( (resp2) => {
console.log(resp2);
createObs3().subscribe( (resp3) => {
console.log(resp3);
createObs4().subscribe( (resp4) => {
console.log(resp4);
createObs5().subscribe( (resp5) => {
console.log(resp5);
})
})
})
})
})
Your test scenario is not really sufficient to see the differences between these two operators. In your test case, each observable only emits 1 time. If an observable only emits a single value, there is really no different between concatMap and flatMap (aka mergeMap). The differences can only be seen when there are multiple emissions.
So, let's use a different scenario. Let's have a source$ observable that simply emits an incrementing integer every 1 second. Then, within our "Higher Order Mapping Operator" (concatMap & mergeMap), we will return an observable that emits a variable number of times every 1 second, then completes.
// emit number every second
const source$ = interval(1000).pipe(map(n => n+1));
// helper to return observable that emits the provided number of times
function inner$(max: number, description: string): Observable<string> {
return interval(1000).pipe(
map(n => `[${description}: inner source ${max}] ${n+1}/${max}`),
take(max),
);
}
Then let's define two separate observables based on the source$ and the inner$; one using concatMap and one using flatMap and observe the output.
const flatMap$ = source$.pipe(
flatMap(n => inner$(n, 'flatMap$'))
);
const concatMap$ = source$.pipe(
concatMap(n => inner$(n, 'concatMap$'))
);
Before looking the differences in the output, let's talk about what these operators have in common. They both:
subscribe to the observable returned by the passed in function
emit emissions from this "inner observable"
unsubscribe from the inner observable(s)
What's different, is how they create and manage inner subscriptions:
concatMap - only allows a single inner subscription at a time. As it receives emissions, it will only subscribe to one inner observable at a time. So it will initially subscribe to the observable created by "emission 1", and only after it completes, will it subscribe to the observable created by "emission 2". This is consistent with how the concat static method behaves.
flatMap (aka mergeMap) - allows many inner subscriptions. So, it will subscribe to the inner observables as new emissions are received. This means that emissions will not be in any particular order as it will emit whenever any of its inner observables emit. This is consistent with how the merge static method behaves (which is why I personally prefer the name "mergeMap").
Here's a StackBlitz that shows the output for the above observables concatMap$ and mergeMap$:
Hopefully, the above explanation helps to clear up your questions!
#1 - "use of flatMap should mix the outputs"
The reason this wasn't working as you expected was because only one emission was going through the flatMap, which means you only ever had a single "inner observable" emitting values. As demonstrated in the above example, once flatMap receives multiple emissions, it can have multiple inner observables that emit independently.
#2 - "...and include the code with multiple emitted events then things get messy."
The "things get messy" is due to having multiple inner subscription that emit values.
For the part you mention about using concatMap and still getting "mixed" output, I would not expect that. I have seen weird behavior in StackBlitz with observable emissions when "auto save" is enabled (seems like sometimes it doesn't completely refresh and old subscriptions seem to survive the auto refresh, which gives very messy console output). Maybe code sandbox has a similar problem.
#3 - "The reason I have gone to flatMap and concatMap was to find a replacement for nested subscriptions in angular using the http library"
This makes sense. You don't want to mess around with nested subscriptions, because there isn't a great way to guarantee the inner subscriptions will be cleaned up.
In most cases with http calls, I find that switchMap is the ideal choice because it will drop emissions from inner observables you no longer care about. Imagine you have a component that reads an id from a route param. It uses this id to make an http call to fetch data.
itemId$ = this.activeRoute.params.pipe(
map(params => params['id']),
distinctUntilChanged()
);
item$ = this.itemId$.pipe(
switchMap(id => http.get(`${serverUrl}/items/${id}`)),
map(response => response.data)
);
We want item$ to emit only the "current item" (corresponds to the id in the url). Say our UI has a button the user can click to navigate to the next item by id and your app finds itself with a click-happy user who keeps smashing that button, which changes the url param even faster than the http call can return the data.
If we chose mergeMap, we would end up with many inner observables that would emit the results of all of those http calls. At best, the screen will flicker as all those different calls come back. At worst (if the calls came back out of order) the UI would be left displaying data that isn't in sync with the id in the url :-(
If we chose concatMap, the user would be forced to wait for all the http calls to be completed in series, even though we only care about that most recent one.
But, with switchMap, whenever a new emission (itemId) is received, it will unsubscribe from the previous inner observable and subscribe to the new one. This means it will not ever emit the results from the old http calls that are no longer relevant. :-)
One thing to note is that since http observables only emit once, the choice between the various operators (switchMap, mergeMap, concatMap) may not seem to make a difference, since they all perform the "inner observable handling" for us. However, it's best to future-proof your code and choose the one that truly gives you the behavior you would want, should you start receiving more than a single emission.
Every time the first observable emits, a second observable is created in the flatMap and starts emitting. However, the value from the first observable is not passed along any further.
Every time that second observable emits, the next flatMap creates a third observable, and so on. Again, the original value coming into the flatMap is not passed along any further.
createObs1()
.pipe(
flatMap(() => createObs2()), // Merge this stream every time prev observable emits
flatMap(() => createObs3()), // Merge this stream every time prev observable emits
flatMap(() => createObs4()), // Merge this stream every time prev observable emits
flatMap(() => createObs5()), // Merge this stream every time prev observable emits
)
.subscribe((resp) => console.log(resp));
// OUTPUT:
// 5
So, it's only the values emitted from createObs5() that actually get emitted to the observer. The values emitted from the previous observables have just been triggering the creation of new observables.
If you were to use merge, then you would get what you may have been expecting:
createObs1()
.pipe(
merge(createObs2()),
merge(createObs3()),
merge(createObs4()),
merge(createObs5()),
)
.subscribe((resp) => console.log(resp));
// OUTPUT:
// 5
// 4
// 3
// 2
// 1

RxJS: How to combine multiple nested observables with buffer

Warning: RxJS newb here.
Here is my challenge:
When an onUnlink$ observable emits...
Immediately start capturing values from an onAdd$ observable, for a maximum of 1 second (I'll call this partition onAddBuffer$).
Query a database (creating a doc$ observable) to fetch a model we'll use to match against one of the onAdd$ values
If one of the values from the onAddBuffer$ observable matches the doc$ value, do not emit
If none of the values from the onAddBuffer$ observable matches the doc$ value, or if the onAddBuffer$ observable never emits, emit the doc$ value
This was my best guess:
// for starters, concatMap doesn't seem right -- I want a whole new stream
const docsToRemove$ = onUnlink$.concatMap( unlinkValue => {
const doc$ = Rx.Observable.fromPromise( db.File.findOne({ unlinkValue }) )
const onAddBuffer$ = onAdd$
.buffer( doc$ ) // capture events while fetching from db -- not sure about this
.takeUntil( Rx.Observable.timer(1000) );
// if there is a match, emit nothing. otherwise wait 1 second and emit doc
return doc$.switchMap( doc =>
Rx.Observable.race(
onAddBuffer$.single( added => doc.attr === added.attr ).mapTo( Rx.Observable.empty() ),
Rx.Observable.timer( 1000 ).mapTo( doc )
)
);
});
docsToRemove$.subscribe( doc => {
// should only ever be invoked (with doc -- the doc$ value) 1 second
// after `onUnlink$` emits, when there are no matching `onAdd$`
// values within that 1 second window.
})
This always emits EmptyObservable. Maybe it's because single appears to emit undefined when there is no match, and I'm expecting it not to emit at all when there is no match? The same thing happens with find.
If I change single to filter, nothing ever emits.
FYI: This is a rename scenario with file system events -- if an add event follows within 1 second of an unlink event and the emitted file hashes match, do nothing because it's a rename. Otherwise it's a true unlink and it should emit the database doc to be removed.
This is my guess how you could do this:
onUnlink$.concatMap(unlinkValue => {
const doc$ = Rx.Observable.fromPromise(db.File.findOne({ unlinkValue })).share();
const bufferDuration$ = Rx.Observable.race(Rx.Observable.timer(1000), doc$);
const onAddBuffer$ = onAdd$.buffer(bufferDuration$);
return Observable.forkJoin(onAddBuffer$, doc$)
.map(([buffer, docResponse]) => { /* whatever logic you need here */ });
});
The single() operator is a little tricky because it emits the item that matches the predicate function only after the source Observable completes (or emits an error when there're two items or no matching items).
The race() is tricky as well. If one of the source Observables completes and doesn't emit any value race() will just complete and not emit anything. I reported this some time ago and this is the correct behavior, see https://github.com/ReactiveX/rxjs/issues/2641.
I guess this is what went wrong in your code.
Also note that .mapTo(Rx.Observable.empty()) will map each value into an instance of Observable. If you wanted to ignore all values you can use filter(() => false) or the ignoreElements() operator.

How to ignore all from an observable if the other observable has data in RxJS?

I have two observables, one receives data from the browser localstorage and the other is from the database through WebAPI.
I want to subscribe to them so if the observable from the localstorage has data, don't initiate the one to get the data from the database.
If the observable from the localstorage does not have any
data, invoke the ajax call to get the data from the WebAPI.
In the following example, I should get only 20, 40, 60, 80, 100 because the first observable has data. The second observable did not run because the first observable started emitting data.
The local storage observable needs some way to signal that there is no data. If it just "hangs" and never completes, then you might use a timer to complete it:
// Use .amb() instead of .race() if your rxjs version is old
const timer = Observable.timer(1000).ignoreElements();
const lsObservable2 = Observable.race(lsObservable, timer);
This will start a timer and if the local storage observable does not produce a value within 1s, it will end the stream.
If your local storage observable will complete on its own if there is no data, then you can use it as is:
const lsObservable2 = lsObservable;
At this point, we'd really like to use defaultIfEmpty, because that has the semantics you want. Unfortunately it only supports a scalar value for the default, when instead you want to yield a different observable stream. So lets write our own version of defaultIfEmpty which produces a new stream using Observable.defer. We use defer so that each time someone subscribes, we can create a new closure variable (hasValue) and monitor whether or not the source observable produces a value for this subscription
Observable.prototype.defaultObservableIfEmpty = function(defaultObservable) {
const source = this;
return Observable.defer(() => {
let hasValue = false;
// create a deferred observable that will evaluate to
// defaultObservable if we have not seen any values, or
// empty observable if we have seen any values.
const next = Observable.defer(() => hasValue ? Observable.empty() : defaultObservable);
// now use do() to set hasValue to true if we see a value from
// the source observable
const sourceSetsValue = source.do(v => hasValue = true);
// now we can can just concat this sourceSetsValue
// with out "next" observable. When the first observable
// finishes, it will subscribe to "next", which will then
// either produce the defaultObservable or an empty observable
return sourceSetsValue.concat(next);
});
}
Next, let's assume you've setup your db Observable to not issue the ajax call until it is actually subscribed. This is an important step. Again you can use something like defer:
const dbObservable = Observable.defer(() => makeDbCall());
Then we can use your new operator like so:
const data = lsObservable2.defaultObservableIfEmpty(dbObservable);
So your application code looks like this (once you've added the new operator to your library):
const timer = Observable.timer(1000).ignoreElements();
const lsObservable2 = Observable.race(lsObservable, timer);
const dbObservable = Observable.defer(() => makeDbCall());
const data = lsObservable2.defaultObservableIfEmpty(dbObservable);
You can use skipWhile and check for the data and return true or false.
observableObject.skipWhile((data)=> {
if(data){
return false;
}
});

Deep autorun on MobX array of objects

I've looked at this issue on Github and this question on stackoverflow but am still unsure how to trigger an autorun for the data structure I have. I get the data from storage as a json object.
// Object keys do not change
const sampleData =
[
{
"title": "some-title",
"isActive": true,
"isCaseSensitive": false,
"hidePref": "overlay",
"tags": ["tag1", "tag2"]
},
{
"title": "all-posts",
"isActive": false,
"isCaseSensitive": true,
"hidePref": "overlay",
"tags": ["a", "b", "c"]
}
];
class Store {
#observable data;
constructor() {
this.data = getDataFromStorage();
if (this.data === null) {
this.data = sampleData;
}
}
}
const MainStore = new Store();
autorun(() => {
console.log("autorun");
sendPayloadToAnotherScript(MainStore.data);
})
How do I get autorun to run every time a new object is added to the data array, or any of the field values in the objects are changed?
The easiest way to get this working is to use JSON.stringify() to observe all properties recursively:
autorun(() => {
console.log("autorun");
// This will access all properties recursively.
const json = JSON.stringify(MainStore.data);
sendPayloadToAnotherScript(MainStore.data);
});
Mobx-State-Tree getSnapshot also works. And even though I can't find it right now: I read that getSnapshot is super-fast.
import { getSnapshot } from 'mobx-state-tree'
autorun(() => {
console.log("autorun");
// This will access all properties recursively.
getSnapshot(MainStore.data);
sendPayloadToAnotherScript(MainStore.data);
});
JSON.stringify() or getSnapshot() is not the right way to do it. Because that has a big cost.
Too Long => First part explains and shows the right way to do it. After it, there is also the section that shows how the tracking and triggering work through objects and arrays observables. If in a hurry Make sure to skim. And to check those sections: Track arrays like a hero show direct examples that work well. Code Illustration speaks better shows what works and what doesn't. Know that the starting explanation clear all. The last section is for the depth people who want to get a slight idea about how mobx manages the magic.
From the documentation you can read how reactivity work. What autorun() or reaction() react to and trigger.
https://mobx.js.org/understanding-reactivity.html
To resume it:
Object or arrays. Will make an autorun react if you make an access to it. mobx track accesses and not changes
That's why if you do
autorun(() => {
console.log(someObservable)
})
it wouldn't react on a change on the someObservable.
Now the confusing thing for people. Is ok but I'm making an access on the observable that holds the array property (in the question example that's MainStore object). And that should make it trackable (MainStore.data is being tracked.).
And yes it is
However. For a change to trigger a reaction. That change should come from an assignment or mutation that is tracked by the observable proxy. In arrays that would be a push, splice, assignment to an element by index and in objects that only can be an assignment to a prop value.
And because our observable that holds the array is an object. So to have a change for the array property to trigger the reaction through that object observable it needs to come from an assignment store.myArray = [].
And we don't want to do that. As a push is more performing.
For this reason. U need to rather make the change on your array. Which is an observable. And to track your array rather than the prop of the object. You have to make access in the array. You can do that through array.length or array[index] where index < length a requirement (because mobx doesn't track indexes above the length).
Note that:
observableObject.array = [...observableObject.array, some] or observableObject.array = observableObject.array.slice() //... (MainStore.data when making change. Instead of MainStore.data.push(). MainStore.data = [...MainStore.data, newEl]) would work and trigger the reaction. However using push would be better. And for that better we track the array. And so
Track arrays like a hero (.length)
autorun(() => {
console.log("autorun");
// tracking
MainStore.data.length
// effect
sendPayloadToAnotherScript(MainStore.data);
})
// or to make it cleaner we can use a reaction
reaction(
() => [MainStore.data.length], // we set what we track through a first callback that make the access to the things to be tracked
() => {
sendPayloadToAnotherScript(MainStore.data);
}
)
A reaction is just like an autorun. With the granulity of having the first expression that allow us to manage the access and what need to be tracked.
All is coming from the doc. I'm trying to explain better.
Code Illustration speaks better
To illustrate the above better let me show that through examples (both that works and doesn't work):
Note: autorun will run once a first time even without any tracking. When we are referring to NOT trigger reaction is talking about when change happen. And trigger at that point.
Array observable
const a = observable([1, 2, 3, 4])
reaction(
() => a[0], // 1- a correct index access. => Proxy get() => trackable
() => {
console.log('Reaction effect .......')
// 3- reaction will trigger
}
)
a.push(5) // 2- reaction will trigger a[0] is an access
const a = observable([1, 2, 3, 4])
reaction(
() => a.length, // 1- a correct length access. => Proxy get() => trackable
() => {
console.log('Reaction effect .......')
// 3- reaction will trigger
}
)
a.push(5) // 2- reaction will trigger a.length is a trackable access
const a = observable([1, 2, 3, 4])
reaction(
() => a.push, // an Access but. => Proxy get() => not trackable
() => {
console.log('Reaction effect .......')
// reaction will NOT trigger
}
)
a.push(5) // reaction will NOT trigger a.push is not a trackable access
Object observable
const o = observable({ some: 'some' })
autorun(() => {
const some = o.some // o.some prop access ==> Proxy get() => trackable
console.log('Autorun effect .........')
})
o.some = 'newSome' // assignment => trigger reaction (because o.some is tracked because of the access in autorun)
const o = observable({ some: 'some' })
autorun(() => {
const somethingElse = o.somethingElse // access different property: `somethingElse` prop is tracked. But not `some` prop
console.log('Autorun effect .........')
})
o.some = 'newSome' // assignment => reaction DOESN'T run (some is not accessed so not tracked)
const o = observable({ some: 'some' })
autorun(() => {
console.log(o) // NO ACCESS
console.log('Autorun effect .........')
})
o.some = 'newSome' // assignment => reaction DOESN'T Run (No access was made in the reaction)
let o = observable({ some: 'some' })
autorun(() => {
const some = o.some // Access to `some` prop
console.log('Autorun effect .........')
})
o = {} // assignment to a variable => reaction DOESN'T Run (we just replaced an observable with a new native object. No proxy handler will run here. yes it is stupid but I liked to include it. To bring it up. As we may or some may forget themselves)
Object observables with arrays observables as props
const o = observable({ array: [0,1,2,3] }) // observable automatically make
// every prop an observable by default. transforming an Arry to an ObservableArray
autorun(() => {
const arr = o.array // tracking the array prop on the object `o` observable
console.log('Autorun effect .........')
})
o.array.push(5) // will NOT trigger the rection (because push will trigger on the array observable. Which we didn't set to be tracked. But only the array prop on the o Observable)
o.array = [...o.array, 5] // assignment + ref change => Will trigger the reaction (Assignment to a prop that is being tracked on an object)
o.array = o.array.slice() // assignment + ref change => Will trigger the reaction (Assignment to a prop that is being tracked on an object)
o.array = o.array // Assignment => But DOESN'T RUN the reaction !!!!!!!!!
// the reference doesn't change. Then it doesn't count as a change !!!!!
const o = observable({ array: [0,1,2,3] })
autorun(() => {
const arr = o.array // tracking the array prop on the object `o` observable
const length = o.array.length // We made the access to .length so the array observable is trackable now for this reaction
console.log('Autorun effect .........')
})
o.array.push(5) // will trigger reaction (array is already tracked .length)
o.array = [...o.array, 5] // assignment => Will trigger the reaction too (Assignment to a prop that is being tracked on an object)
// Push however is more performing (No copy unless the array needs to resize)
With that, you have a great perception. Normally the whole cases are well covered.
What about with react-rerendering
The same principle apply with observer() higher order component factory. And the how the tracking happen within the render function of the component.
Here some great examples from an answer I wrote for another question
https://stackoverflow.com/a/73572516/7668448
There is code examples and playgrounds too where u can easily test for yourself on the fly.
How the tracking and the triggering happen
Observables are proxy objects. By making an access to a prop => we trigger the proxy methods that handle that operation. store.data will trigger the get() method of the proxy handler. And at that handler method. The tracking code run and that way mobx can follow all accesses. Same thing for store.data = []. That would trigger the set() method of the handler. Same for store.data[0] = 'some', however this would happen at the store.data proxy object (array observable) rather then the store itself. store.data.push() would trigger the get() method of the proxy. And then it would run the condition that check that it's push prop.
new Proxy(array, {
get(target, prop, receiver) {
if (prop === 'push') {
// handle push related action here
return
}
// ....
// handle access tracking action here
}
set(obj, prop, value) {
// if prop was an observable make sure the new value would be too
// Don't know how mobx does it. Didn't take the time to check the code
if (isObservable(obj[prop]) && !isObservable(value)) {
value = observable(value)
}
obj[prop] = value
// handle assignment change neededactions
}
})
Actually, I got a bit curious. And went and checked the actual mobx implementation. And here are some details:
For the arrayObservable here are the details:
The proxy handler are defined in arrayTraps
src/types/observablearray.ts#L84
We can see every target (element that was made an observable). Have a $mobx property that have an
As shown here src/types/observablearray.ts#L86
$mobx if curious it's just a Symbol export const $mobx = Symbol("mobx administration")src/core/atom.ts#L17
And you can see how the administration object is used to handle all. That handle the actions and magic of tracking.
const arrayTraps = {
get(target, name) {
const adm: ObservableArrayAdministration = target[$mobx]
if (name === $mobx) {
// if $mobx (requiring the administration) just forward it
return adm
}
if (name === "length") {
// if length ==> handle it through administration
return adm.getArrayLength_()
}
if (typeof name === "string" && !isNaN(name as any)) {
// all proxy names are strings.
// Check that it is an index. If so handle it through the administration
return adm.get_(parseInt(name))
}
// Up to now ==> if $mobx, length or 0, 1, 2 ....
// it would be handled and for the two later through administration
if (hasProp(arrayExtensions, name)) {
// If it's one of the extension function. Handle it through
// the arrayExtensions handlers. Some do extra actions like
// triggering the process of handling reactions. Some doesn't
return arrayExtensions[name]
}
// if none of the above. Just let the native array handling go
return target[name]
},
set(target, name, value): boolean {
const adm: ObservableArrayAdministration = target[$mobx]
if (name === "length") {
// Handle length through administration
adm.setArrayLength_(value)
}
if (typeof name === "symbol" || isNaN(name)) {
target[name] = value
} else {
// numeric string
// handle numeric string assignment through administration as well
adm.set_(parseInt(name), value)
}
return true
},
preventExtensions() {
die(15)
}
}
Through arrayExtensions. There is those that are handled through the same simpleFunc.
src/types/observablearray.ts#L513
And you can see how calling such calls and through the proxy (get handler). Those calls signal that the observable is being observed and the dehancedValues are managed that are recovered from the administration object. src/types/observablearray.ts#L545
For the other arrayExtensions that have special handling like push we can see how it's triggering the process of signaling change. src/types/observablearray.ts#L457
First we can see that push is using splice handling of the administration. Making push and splice to be handled by the same handler. reusability
You can check this part that seems to be the part for push or splice that trigger and handle interceptors src/types/observablearray.ts#L238
Interceptors as by doc ref
For change notification and reaction triggering this code here
src/types/observablearray.ts#L261 does that handling. You can see it through this line src/types/observablearray.ts#L337
this.atom_.reportChanged()
// conform: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/observe
if (notify) {
notifyListeners(this, change)
}
src/types/observablearray.ts#L256 show how the length is tracked and used and it check if the internal array is modified. To show that mobx does track many things. Or does check.
If you are more curious. You can dig further. But through the above you can already have a good idea how mobx manage it's magic. And understanding how mobx track (still not fully in depth) and also how the observable that are proxies and how proxies works. All that help well.
Here extra links:
packages/mobx/src/types/observablearray.ts
packages/mobx/src/types/observableobject.ts
packages/mobx/src/types/dynamicobject.ts
packages/mobx/src/types/observablemap.ts
packages/mobx/src/types/observableset.ts
packages/mobx/src/types/observablevalue.ts
packages/mobx/src/utils/utils.ts
If you check those links. You would notice that ObservableMap, ObservableSet, ObservableValue all doesn't use proxies. And make a direct implementation. Make sense for Set and Map. As you would just make a wrapper that keep the same methods as the native one. Proxies are for overriding operators. Like property access. And assignment operations.
You would notice too. That ObservableObject have the administration implementation that account for both proxies and annotation. And only the ObservableDynamicObject that implement the proxy, . And using the ObservableObjectAdministration. you can find the proxy traps there too.
Again, you can dig further if you want.
Otherwise, that's it. I hope that explained the whole well and went to some depth.

Categories

Resources