MobX computed runs before item is actually inserted into array - javascript

I'm using React in combination with MobX. I use a store with an observable array (conversations) and would like to offer a sorted version of this array as a computed property. When adding a new conversation, the computed property sortedConversations is evaluated before the conversation is added to the array. In the small example below, 'Reordering conversations' is always logged before 'Added conversation'. Am I doing something wrong?
class Store {
...
#observable conversations = [];
addConversation(conversation) {
this.conversations.push(conversation);
console.log('Added conversation');
}
#computed
get sortedConversations() {
console.log('Reordering conversations');
return _.orderBy(this.conversations.slice(), ['lastUpdated'], ['asc']);
}
}

You are not doing anything wrong. The MobX API looks like regular JavaScript, but every time an observable is updated, all its observers are updated synchronously under the hood. This will not be an issue in this case, but you could wrap the contents of addConversation in a transaction:
addConversation(conversation) {
transaction(() => {
this.conversations.push(conversation);
console.log('Added conversation');
});
}
You could also make the addConversation into an action which also is a transaction:
#action
addConversation(conversation) {
this.conversations.push(conversation);
console.log('Added conversation');
}

Related

Avoid subscribing in Angular Service using RxJS?

I am working on an angular project that needs to load up pages and then display them one by one / two by two.
As per this article and some other sources, subscribing in services is almost never necessary. So is there a way to rewrite this in pure reactive style using RxJS operators?
Here's what I have (simplified) :
export class NavigationService {
private pages: Page[] = [];
private mode = Mode.SinglePage;
private index = 0;
private currentPages = new BehaviorSubject<Page[]>([]);
constructor(
private pageService: PageService,
private view: ViewService,
) {
this.pageService.pages$.subscribe(pages => {
this.setPages(pages);
});
this.view.mode$.subscribe(mode => {
this.setMode(mode);
});
}
private setPages(pages: Page[]) {
this.pages = pages;
this.updateCurrentPages();
}
private setMode(mode: Mode) {
this.mode = mode;
this.updateCurrentPages();
}
private updateCurrentPages() {
// get an array of current pages depending on pages array, mode & index
this.currentPages.next(...);
}
public goToNextPage() {
this.index += 1;
this.updateCurrentPages();
}
public get currentPages$() {
return this.currentPages.asObservable();
}
}
I've tried multiple solutions and didn't manage to get it right. The closest I got was using scan(), but it always reset my accumulated value when the outer observables (pages, mode) got updated.
Any help is appreciated, thanks !
You can use merge to create reducer functions from observables. These functions will update part of a state maintained by the service. They are past along to the scan operator which will update the prior state from the reducer. After the reducer is run, currentPages is set on the new state and that new state is returned.
export class NavigationService {
private readonly relativePageChangeSubject = new Subject<number>();
readonly state$ = merge(
this.pageService.pages$.pipe(map((pages) => (vm) => ({ ...vm, pages }))),
this.relativePageChangeSubject.pipe(map((rel) => (vm) => ({ ...vm, index: vm.index + rel }))),
this.view.mode$.pipe(map((mode) => (vm) => ({ ...vm, mode })))
).pipe(
startWith((s) => s), // if necessary, force an initial value to be emitted from the initial value in scan.
scan((s, reducer) => {
const next = reducer(s);
// update currentPages on the next state here.
return next;
}, { currentPages: [], index: 0, mode: Mode.SinglePage, pages: [] }),
shareReplay(1)
)
readonly currentPages$ = this.state$.pipe(
map(x => x.currentPages),
distinctUntilChanged()
);
constructor(private pageService: PageService, private view: ViewService) { }
goToNextPage() {
this.relativePageChangeSubject.next(1);
}
}
Notes:
Instead of having a nextPage Subject, a more flexible relative change subject is used that will modify the index from the value in the prior state.
The currentPage$ observable isn't necessary, as a consumer could just attach to the main state$ and map as needed. Feel free to make state$ private or remove currentPage$.
Let's first detail what you are doing:
You subscribe to pageService.pages$ and view.mode$
Those subscriptions take the values and put them in a private variabkle
Then fire a function to use those two variables
Finally, trigger a value push.
All this can be done in a simple pipeline. You'd need to include the index as an observable (behaviour subject in our case) to react to that change too.
Use combineLatest, this will subscribe to all observables we want, and trigger the pipe WHEN ALL have fired once, and every time one changes afterwards. You may want to use .pipe(startWith("something")) on observables that should have a default value so your observable pipe triggers asap.
CombineLatest will then provide an object as value, with each value in the object key passed when created. Here pages, mode and index. I've used a switchMap to demo here if updateCurrentPages passes an observable, but you could use a map if there is no async task to be done.
export class NavigationService {
readonly currentPages$:Observable<Pages[]>;
constructor(
private pageService: PageService,
private view: ViewService,
) {
this.paginator = new Paginator(this.pageService.pages$);
this.currentPages$ = combineLatest({
pages:this.pageService.pages$,
mode:this.view.mode$,
index:this.this.paginator.pageChange$
}).pipe(
switchMap(({pages,mode,index})=>{
return this.updateCurrentPages(pages,mode);
}),
);
}
private updateCurrentPages() {
// get an array of current pages depending on pages array, mode & index
this.currentPages.next(...);
}
public goToNextPage() {
this.paginator.next();
}
}
class Paginator{
pageChange$ = combineLatest({
total:this.pages$.pipe(map(pages=>pages.length)),
wanted:this.pageMove$}).pipe(map({total,wanted}=>{
// Make sure it is between 0 and maximum according to pages.
return Math.max(Math.min(total-1,wanted),0);
}),
// Do not emit twice the same page (pressing next when already at last)
dinstinctUntilChanged());
);
pageMove$ = new BehaviorSubject<number>(0);
constructor(pages$: Observable<Pages[]>){
}
next(){
this.pageMove$.next(this.pageMove$.value()+1);
}
previous(){
this.pageMove$.next(this.pageMove$.value()-1);
}
to(i:number){
this.pageMove$.next(i);
}
}
Beware tough about stating that subscribe is never needed. You may want to subscribe to some events for some reason. It is just that combining everything in a pipeline makes things easier to handle... In the example above, the observables will be unsubscribed to when the consumer of your service unsubscribes to currentPages$. So one thing less to handle.
Also, note that if multiple consumers subscribe to this service's currentPages$ the pipeline will be duplicated and unecessary work will be done, once for each subscriber. While this MAY be good, you might want to have everyone subscribe to the same "final" observable. This is easily do-able by adding share() or shareReplay(1) at the end of your pipeline. Share will make sure the same observable pipeline will be used for the new subscriber, and they will receive new values starting from then. Using shareReplay(1), will do the same but also emit the latest value directly on subscribe (just like BehaviourSubject) the 1 as parameter is indeed the number of replays to send out...
Hope this helps! When you master the RxJS you'll see that things will get easier and easier (see the difference in code amount!) but getting the hang of it take a little bit of time. Do not worry, just perseverate you'll get there. (Hint to get better, using outside variables/properties are the evil of handling pipelines)

When pushing a new value inside an array it gets totally override - VUEX

Hello so I am creating a filter search and I 'm trying to collect all the key (tags) that the user press, inside an array, however every time that a new value is push it does override the entire array. So I tried a couple of things, like spread syntax, concat, etc. But with no luck.
So my action looks like this:
const setCurrentFilters = async (context, payload) => {
if (payload) {
context.commit('setCurrentFilter');
}
}
My state
state:{
filters: JSON.parse(sessionStorage.getItem('currentFilters') || '[]'),
}
The mutation
setCurrentFilter(state, payload) {
state.filters.push(payload);
sessionStorage.setItem('currentFilters', JSON.stringify(payload));
}
And my getter
currentFilters(state) {
return state.filters;
},
Thank you in advance for any help : )
This is simply because you set const filters = []; which means that the next condition if (filters.length) will always return false (as you just created this array) and therefore the else statement will execute.
in the else statement you basically push the new payload to the empty array you just initialized - which makes your array always hold only the new value
i believe that you just need to remove the const filters = []; line, and access the filters property that exists in your state

MobX and deep observability

I'm trying to understand deep observability in MobX.
In particular, in the following code I'd like the autorun to be called every time I run setCommentCountForPost, but currently it isn't.
How should I fix this code? And, observable on a property of Post is enough to activate the autorun when I read the list in which the post is contained? (as I'm doing in the autorun)
I'm using MobX 5.
Edit: I discovered the code is working properly if I use the following call inside the autorun: console.log(toJS(p.getPosts()));.
This is interesting, but why, and how should I do if I only want to call getPosts()?
This is the code
import { useStrict, configure, autorun } from 'mobx';
import { toJS, observable, action, computed } from 'mobx';
configure({ enforceActions: true });
class Post {
#observable commentCount = 0;
setCommentCount(c) {
this.commentCount = c;
}
}
class PostList {
#observable _posts = {};
#action createPost(id) {
this._posts[id] = new Post();
}
#action setCommentCountForPost(id, c) {
this._posts[id].setCommentCount(c);
}
getPosts() {
return this._posts;
}
}
let p = new PostList();
p.createPost(1);
autorun(function test () {
console.log(p.getPosts());
});
p.setCommentCountForPost(1, 22);
MobX tracks property access, not value
in your example, the autorun function only tracking the _posts, but not the property of _posts, so if you change the _posts value the tracking function will worked
console.log(toJS(p.getPosts())) worked bacause of the toJS function in order to convert the observable value to normal value , it access the property of _posts.
if you hope the p.getPosts() worked, you should iteration access the property of _posts.

How to rerun a Vuex Getter

Maybe I have misunderstood what a getter is in Vuex, but say I have a getter that gets the size of a DOM element, a div for example. I would do something like this :
const getters = {
getContainerWidth (state) {
return document.getElementById('main-content').clientWidth;
}
}
Now when I start my app, all the getters seem to be run straight away. What if the div isn't available at startup? How do I rerun a getter?
I run the getter like this at the moment :
import store from '#/store'
store.getters['myModule/getContainerWidth']
I thought maybe this would work :
store.getters['myModule/getContainerWidth']()
But since store.getters is an object containing properties and values, and the values not being functions, I can't rerun them.
Any ideas?
Getters should depend on state field to be reactive. It you want to observe clientWidth changes - it does not work.
If you want to use it like function then just return function from getter:
const getters = {
getContainerWidth (state) {
return () => {
let container = document.getElementById('main-content');
return container ? container.clientWidth : 0
};
}
}
and use it like getContainerWidth()

Mobx listen to a value changes with computed does not work

I am trying to listen to a value changes with mobx computed expression, but I don't see any changes when I push a new value to the observed expression.
class List {
#observable values = [];
constructor() {
computed(() => this.values).observe(changes => {
console.log(changes);
})
}
add(item) {
this.values.push(Math.random());
}
}
const list = new List();
list.add();
Why doesn't it work?
Note that computed will only track data it actually accesses. The only data accessed in your computed is the changes, a pointer to an array. Pushing a new value to that array will not change the pointer.
Remember: computeds produce values, reactions & autoruns produce side effects.
Your computed never produces a new value, so never triggers the observer.
computed is used when you want to derive a new value from other observables. You could use observe instead:
Example (JSBin)
class List {
#observable values = [];
constructor() {
observe(this.values, (change) => {
if (change.added) {
console.log(`${change.added} got added to values`);
}
});
}
add(item) {
this.values.push(Math.random());
}
}
const list = new List();
setInterval(() => {
list.add();
}, 1000);

Categories