Mobx: clarity request - providing a custom equivalence fn for `@computed`

Created on 4 Jun 2017  路  5Comments  路  Source: mobxjs/mobx

Hi, mobx and typescript newbie here and I want to clarify the idiomatic way to handle this situation. Essentially my @computed returns instances of Date and I want to teach it how to compare Dates as it is currently informing observers even if the returned Date is equivalent to the previous one.

Given the following (forgive the hacking):

class MyClass {
    now: Date;
    @computed get nowMinute() {
        const now = this.now;
        const nowMinute = new Date(now.getTime());
        nowMinute.setSeconds(0);
        nowMinute.setMilliseconds(0);
        console.log('running: now', now, 'returning:', nowMinute);
        return nowMinute;
    }

The test:

    it('nowMinutes works', () => {
        const expected = new Date(1, 1, 1, 1, 1, 0, 0);
        const store = new UIStore(Pages.Report);
        store.now = new Date(1, 1, 1, 1, 1, 10);
        expect(store.nowMinute).toEqual(expected);

        // changing now shouldn't make any difference though...
        let captured = false;
        const capturer = autorun(() => {
            console.log('capturing', store.nowMinute);
            captured = true;
        });

        // THIS SHOULDN'T CAPTURE as nowMinute is equivalent 
        store.now.setSeconds(20);
        // THAT SHOULDN'T CAPTURE as nowMinute is equivalent 
        expect(store.nowMinute).toEqual(expected);
        // THIS FAILS
        expect(captured).toBe(false);
        capturer();
    });

I assume that because the two dates aren't equivalent references that @computed is letting the value escape, so what is my best option here?

I did read https://mobx.js.org/refguide/modifiers.html but I couldn't see how it relates to this use case.

Thanks!

EDIT: I realise I could use one of the bazillion debounce or throttling libs but this is just as much about learning mobx as it is getting stuff done ;-).

All 5 comments

@computed.struct should do the trick
For custom comparator there is PR #951

Hi, thanks for the suggestion, but unfortunately that didn't work, the autorun still fires.

Until my PR is merged, you might have more luck with the workaround I posted here?

(You won't be able to use this as a decorator without some extra work)

So I am learning a whole bunch of stuff doing this. My original code was flawed because setting .seconds on a Date doesn't cause autorun to fire because the autorun isn't noticing it. Changing the property to point to a new Date is noticed by the autorun, as you would expect.

The following sandbox (I won't go so far as to claim this is real code :-)):

import { observable, autorun } from 'mobx';
import { Page } from '../model/Page';

export class UIStore {
    @observable currentUser: User;
    @observable currentPage: Page;
    // this fires all the time so store the last value and set it when it
    // differs - sigh.
    @observable now: Date;
    @observable nowMinute: Date;


    constructor(defaultPage: Page) {
        this.currentPage = defaultPage;
        this.now = new Date()
        this.nowMinute = new Date(this.now.getTime());
        this.nowMinute.setSeconds(0);
        this.nowMinute.setMilliseconds(0);


        autorun(() => {
            console.log('now is', this.now + ', nowMinute:' + this.nowMinute);
            const newDate = new Date(this.now.getTime());
            newDate.setSeconds(0);
            newDate.setMilliseconds(0);
            if (this.nowMinute.getTime() != newDate.getTime()) {
                console.log('setting nowMinute to ', newDate);
                this.nowMinute = newDate;
            }
        });
    }

    logUserIn(user: User) {
        this.currentUser = user;
    }

    changePage(page: Page) {
        this.currentPage = page;
    }
}

with test:

    it('nowMinutes works', () => {
        console.log('RUNNING')
        const expected = new Date(1, 1, 1, 1, 1, 0, 0);
        const store = new UIStore(Pages.Report);
        store.now = new Date(1, 1, 1, 1, 1, 10);
        expect(store.nowMinute).toEqual(expected);

        console.log('STARTING CAPTURE');
        // changing now shouldn't make any difference though...
        let captured;
        const capturer = autorun(() => {
            console.log('capturing', store.nowMinute);
            captured = store.nowMinute;
        });
        // this shouldn't be set again when we change `now`
        captured = undefined;
        console.log('setting no-op 20');
        let d = new Date(store.now.getTime());
        d.setSeconds(20);
        store.now = d;
        console.log('setting no-op 30');
        d = new Date(store.now.getTime());
        d.setSeconds(30);
        store.now = d;
        expect(store.nowMinute).toEqual(expected);
        expect(captured).toBe(undefined);
        // now set it so it does running
        console.log('THIS SHOULD CHANGE');
        d = new Date(store.now.getTime());
        d.setMinutes(10);
        store.now = d;

        const expected2 = new Date(d.getTime());
        expected2.setSeconds(0);
        expected2.setMilliseconds(0);
        expect(captured).toEqual(expected2);
        capturer();
    });

exhibits the (at least my) expected effects.

Thanks @jamiewinder. That is almost certainly what I will end up with, thanks.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

mehdi-cit picture mehdi-cit  路  3Comments

etinif picture etinif  路  3Comments

rodryquintero picture rodryquintero  路  3Comments

bb picture bb  路  3Comments

geohuz picture geohuz  路  3Comments