question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

using fromPromise inside computed and trigger manual retry

See original GitHub issue

Hi,

I found it a very nice pattern to combine @computed with fromPromise to achieve lazy data fetching. Data is fetched only once it’s needed somewhere in a visible UI component (without explicitly triggering the data fetching via some router or component hooks) and automatically reloads the data once some fetching relevant param changed.

For example see the small example below:

class TodoStore {
  constructor(private _accountStore: AccountStore) {}
  @computed
  get todosPromised() {
    return fromPromise(fetch(`/${this._accountStore.currentAccountId}/todos`));
  }
}

const TodoView = ({todoStore}:{todoStore: TodoStore}) => todoStore.todosPromised.case({
  pending: () => <div>loading...</div>,
  rejected: error => <div>Error...</div>, 
  fulfilled: todos => renderTodos(todos)
});

In this case the data fetching gets only triggered once the todosPromised property is used somewhere in a component and this component is mounted / visible. Also the computed function will automatically refetch data if the currentAccountId changes.

Now the open question I have is how do I trigger a manual re-evaluation of the computed function (e.g. the user clicks a refresh button). This is useful if the data hasn’t been refreshed for a while or if a previous fetch attempt led to an error.

I was playing a bit around and found for example the following as “solution” even though it’s a bit ugly:

class TodoStore {
  constructor(private _accountStore: AccountStore) {}
  
  @observable
  retryRequests = 0;
  
  @action.bound
  retry(){
    this.retryRequests++;
  }
  
  @computed
  get todosPromised() {
    this.retryRequests;
    return fromPromise(fetch(`/${this._accountStore.currentAccountId}/todos`));
  }
}

const TodoView = ({ todoStore }: { todoStore: TodoStore }) => todoStore.todosPromised.case({
  pending: () => <div>loading...</div>,
  rejected: error => <div>Error...<Button onClick={todoStore.retry}>Retry</Button></div>,
  fulfilled: todos => renderTodos(todos)
});

You can see that I’m using the retryRequests observable prop as a dummy trigger. Since this solution is kinda ugly I tried to extend the fromPromise utility so that it exposes a retry method that can be used to trigger a follow up reaction which creates a new Promise etc… To trigger the follow up reaction I’m using an Atom.

It looks the following:

export type IPromiseBasedObservableWithRetry<T> = IPromiseBasedObservable<T> & {
  retry(): Promise<T>;
}

export function fromPromiseWithRetry<T>(promise: PromiseLike<T>):  IPromiseBasedObservableWithRetry<T> {

  const promiseBasedObservable = fromPromise(promise);

  const retryAtom = new Atom('Retry');
  retryAtom.reportObserved();
  (promiseBasedObservable as any).retry = () => retryAtom.reportChanged();

  return promiseBasedObservable as any;
}


class TodoStore {
  constructor(private _accountStore: AccountStore) {}
  
  @computed
  get todosPromised() {
    return fromPromiseWithRetry(fetch(`/${this._accountStore.currentAccountId}/todos`));
  }
}

const TodoView = ({ todoStore }: { todoStore: TodoStore }) => todoStore.todosPromised.case({
  pending: () => <div>loading...</div>,
  rejected: error => <div>Error...<Button onClick={todoStore.todosPromised.retry}>Retry</Button></div>,
  fulfilled: todos => renderTodos(todos)
});

This actually works, but I’m wondering if this is advisable from a performance and (possible memory leak) perspective. In particular I’m a bit wondering what impact it has that I’m creating a new Atom on every fromPromiseWithRetry invocation (during a reaction) and immediately call the reportObserved. Any thoughts @mweststrate and whether this is good or no and what may be a better solution?

btw in part this is related to https://github.com/mobxjs/mobx/issues/307

Thanks and Best Regards Christian

Issue Analytics

  • State:closed
  • Created 6 years ago
  • Comments:11 (1 by maintainers)

github_iconTop GitHub Comments

3reactions
geekflyercommented, Dec 6, 2017

I haven’t tried @mayorovp’ solution but he performs a side effect / mutation of an external observable inside a computed method which supposed to be side-effect free, which looks odd to me.

Anyways, I actually built in the meantime a custom implementation of the whole thing which more or less takes the API of fromPromise and augments it with refresh and a few other methods. In principle works under the hood like the library https://github.com/danielearwicker/computed-async-mobx/issues/13 but instead of having a complete custom API it stays consistent with fromPromise wherever possible.

Here’s my implementation:

import { action, computed, observable } from 'mobx';
import { fromPromise, IPromiseBasedObservable, FULFILLED, PENDING, REJECTED } from 'mobx-utils';

export type IBaseAsyncComputed<T> = {
  refresh(): void;
  case<U>(handlers: { pending?: () => U; fulfilled?: (t: T) => U; rejected?: (e: any) => U }): U;
};

export interface IPendingAsyncComputed<T> extends IBaseAsyncComputed<T> {
  readonly state: 'pending';
  readonly pending: true;
  readonly rejected: false;
  readonly fulfilled: false;
}

export interface IRejectedAsyncComputed<T> extends IBaseAsyncComputed<T> {
  readonly state: 'rejected';
  readonly pending: false;
  readonly rejected: true;
  readonly fulfilled: false;
  readonly error: any;
}

export interface IFulfilledAsyncComputed<T> extends IBaseAsyncComputed<T> {
  readonly state: 'fulfilled';
  readonly pending: false;
  readonly rejected: false;
  readonly fulfilled: true;
  readonly value: T;
}

export type IAsyncComputed<T> = IPendingAsyncComputed<T> | IFulfilledAsyncComputed<T> | IRejectedAsyncComputed<T>;

class AsyncComputed<T> {
  @observable private refreshCallCount = 0;

  @computed
  get state() {
    return this._internalObservable.state;
  }

  constructor(private readonly computeFn: () => PromiseLike<T>) {}

  @computed
  get pending() {
    return this.state === PENDING;
  }

  @computed
  get rejected() {
    return this.state === REJECTED;
  }

  @computed
  get fulfilled() {
    return this.state === FULFILLED;
  }

  @computed
  get value() {
    if (this._internalObservable.state === FULFILLED) {
      return this._internalObservable.value;
    } else {
      return undefined;
    }
  }

  @computed
  get error() {
    if (this._internalObservable.state === REJECTED) {
      return this._internalObservable.value;
    } else {
      return undefined;
    }
  }

  @action.bound
  refresh() {
    this.refreshCallCount++;
  }

  @computed
  get case() {
    return this._internalObservable.case;
  }

  @computed
  private get _internalObservable(): IPromiseBasedObservable<T> {
    this.refreshCallCount;
    const observablePromise = fromPromise(this.computeFn());
    // handle rejections etc. because they'll otherwise bubble up and crash node.js
    (observablePromise as any).catch(e => undefined);
    return observablePromise;
  }
}

export default function asyncComputed<T>(computeFn: () => PromiseLike<T>) {
  return new AsyncComputed(computeFn) as IAsyncComputed<T>;
}

usage example:

import asyncComputed from './asyncComputed';

class ThingStore {

     @observable thingIdToDisplay = 0;

     thing = asyncComputed(() => fetchStuff('./foo/bar/${this.thingIdToDisplay}')

}


// and somewhere in your event handler when a user clicks the `refresh` button manually:
thingStore.thing.refresh();

Let me know if you find that useful - I can publish this implementation on npm if it is.

2reactions
geekflyercommented, Jun 17, 2018

@dbrody Unfortunately not. I’ve created a github repo just now though to publish the internal version of this utility: https://github.com/solvvy/mobx-async-computed . Since it’s really just 1 file which contains all the code you could technically simply copy it into your project to try it out: https://github.com/solvvy/mobx-async-computed/blob/master/asyncComputed.ts . Let me know if you find that useful. If so I may be more motivated to actually publish it on npm 😉

If you don’t use typescript here’s the pre-compiled code:

"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Object.defineProperty(exports, "__esModule", { value: true });
const mobx_1 = require("mobx");
const mobx_utils_1 = require("mobx-utils");
class AbstractAsyncComputed {
    get pending() {
        return this.state === mobx_utils_1.PENDING;
    }
    get rejected() {
        return this.state === mobx_utils_1.REJECTED;
    }
    get fulfilled() {
        return this.state === mobx_utils_1.FULFILLED;
    }
    // TODO move this combinator to AbstractAsyncComputed perhaps
    mapSync(mapFn) {
        return new DerivedAsyncComputed(this, mapFn);
    }
    // TODO this to AbstractAsyncComputed
    case(handlers) {
        switch (this.state) {
            case mobx_utils_1.PENDING:
                return handlers.pending && handlers.pending();
            case mobx_utils_1.REJECTED:
                return handlers.rejected && handlers.rejected(this.error);
            case mobx_utils_1.FULFILLED:
                return handlers.fulfilled && handlers.fulfilled(this.value);
        }
    }
}
__decorate([
    mobx_1.computed
], AbstractAsyncComputed.prototype, "pending", null);
__decorate([
    mobx_1.computed
], AbstractAsyncComputed.prototype, "rejected", null);
__decorate([
    mobx_1.computed
], AbstractAsyncComputed.prototype, "fulfilled", null);
class AsyncComputed extends AbstractAsyncComputed {
    constructor(computeFn) {
        super();
        this.computeFn = computeFn;
        this.refreshCallCount = 0;
    }
    get state() {
        return this._internalObservable.state;
    }
    get value() {
        if (this._internalObservable.state === mobx_utils_1.FULFILLED) {
            return this._internalObservable.value;
        }
        else {
            return undefined;
        }
    }
    get error() {
        if (this._internalObservable.state === mobx_utils_1.REJECTED) {
            return this._internalObservable.value;
        }
        else {
            return undefined;
        }
    }
    refresh() {
        this.refreshCallCount++;
    }
    case(caseImpl) {
        return this._internalObservable.case(caseImpl);
    }
    get _internalObservable() {
        this.refreshCallCount;
        const observablePromise = mobx_utils_1.fromPromise(this.computeFn());
        // handle rejections etc. because they'll otherwise bubble up and crash node.js
        observablePromise.catch(e => undefined);
        return observablePromise;
    }
}
__decorate([
    mobx_1.observable
], AsyncComputed.prototype, "refreshCallCount", void 0);
__decorate([
    mobx_1.computed
], AsyncComputed.prototype, "state", null);
__decorate([
    mobx_1.computed
], AsyncComputed.prototype, "value", null);
__decorate([
    mobx_1.computed
], AsyncComputed.prototype, "error", null);
__decorate([
    mobx_1.action.bound
], AsyncComputed.prototype, "refresh", null);
__decorate([
    mobx_1.computed
], AsyncComputed.prototype, "_internalObservable", null);
const PASS_THROUGH_PROPS = ['pending', 'refresh', 'error', 'state'];
class DerivedAsyncComputed extends AbstractAsyncComputed {
    constructor(baseAsyncComputed, derivationFn) {
        super();
        this.baseAsyncComputed = baseAsyncComputed;
        this.derivationFn = derivationFn;
        PASS_THROUGH_PROPS.forEach(prop => {
            Object.defineProperty(this, prop, {
                enumerable: true,
                configurable: true,
                get() {
                    return baseAsyncComputed[prop];
                }
            });
        });
    }
    get value() {
        return this.baseAsyncComputed.fulfilled ? this.derivationFn(this.baseAsyncComputed.value) : undefined;
    }
    case(handlers) {
        switch (this.state) {
            case mobx_utils_1.PENDING:
                return handlers.pending && handlers.pending();
            case mobx_utils_1.REJECTED:
                return handlers.rejected && handlers.rejected(this.error);
            case mobx_utils_1.FULFILLED:
                return handlers.fulfilled && handlers.fulfilled(this.value);
        }
    }
}
__decorate([
    mobx_1.computed
], DerivedAsyncComputed.prototype, "value", null);
class CombinedAsyncComputed extends AbstractAsyncComputed {
    constructor(asyncComputeds) {
        super();
        this.asyncComputeds = asyncComputeds;
    }
    get state() {
        const allComputeds = this.asyncComputeds;
        if (allComputeds.some(computed => computed.rejected)) {
            return mobx_utils_1.REJECTED;
        }
        else if (allComputeds.every(computed => computed.fulfilled)) {
            return mobx_utils_1.FULFILLED;
        }
        else {
            return mobx_utils_1.PENDING;
        }
    }
    refresh() {
        this.asyncComputeds.forEach(asyncComputeds => asyncComputeds.refresh());
    }
    get value() {
        return this.asyncComputeds.every(computed => computed.fulfilled)
            ? this.asyncComputeds.map(computed => computed.value)
            : undefined;
    }
    get error() {
        const erroredComputed = this.asyncComputeds.find(computed => computed.error);
        if (erroredComputed) {
            return erroredComputed.error;
        }
        else {
            return undefined;
        }
    }
}
__decorate([
    mobx_1.computed
], CombinedAsyncComputed.prototype, "state", null);
__decorate([
    mobx_1.action.bound
], CombinedAsyncComputed.prototype, "refresh", null);
__decorate([
    mobx_1.computed
], CombinedAsyncComputed.prototype, "value", null);
__decorate([
    mobx_1.computed
], CombinedAsyncComputed.prototype, "error", null);
function combineAsyncComputeds(...asyncComputeds) {
    return new CombinedAsyncComputed(asyncComputeds);
}
exports.combineAsyncComputeds = combineAsyncComputeds;
function asyncComputed(computeFn) {
    return new AsyncComputed(computeFn);
}
exports.default = asyncComputed;
Read more comments on GitHub >

github_iconTop Results From Across the Web

computed fromPromise with nested promises does not ...
this.selectedHouseholdId is updated by a <select> field in the UI, but I noticed that the list of customerProducts is not reevaluated when that ......
Read more >
Observable | RxJS API Document - ReactiveX
Combines multiple Observables to create an Observable whose values are calculated from the values, in order, of each of its input Observables.
Read more >
How To Pass Promise Output As Prop Using Mobx - ADocLib
using fromPromise inside computed and trigger manual retry #90 btw in part this is related to mobxjs/mobx#307 REJECTED } from 'mobxutils'; export type ......
Read more >
vtrak 12110/8110 user manual - Promise Technology
In order to use Parallel ATA disk drives in VTrak, you must first install a PATA-to-. SATA adapter available from Promise Technology. Figure...
Read more >
Reactive Programming with RxJS
Real-time and asynchronous web applications pose a huge challenge in web ... a clear roadmap for learning reactive programming with RxJS with practical...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found