Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to implement an asynchronous dependency between properties #872

Closed
DanielSWolf opened this issue Mar 6, 2017 · 6 comments
Closed

How to implement an asynchronous dependency between properties #872

DanielSWolf opened this issue Mar 6, 2017 · 6 comments

Comments

@DanielSWolf
Copy link

I have a*:

  1. Question: Feel free to just state your question. For a quick answer, there are usually people online at our Gitter channel

First consider the following simple, synchronous code. You can run it in JSFiddle.

const { observable, computed, autorun } = mobx;

class NumberInfo {
    @observable value = 0;
    @computed get square() {
    	return this.value * this.value;
    }
}

const numberInfo = new NumberInfo();

autorun(() => console.log(`Value changed to ${numberInfo.value}.`));
autorun(() => console.log(`Square changed to ${numberInfo.square}.`));

numberInfo.value = 2;
numberInfo.value = 3;

Property square depends on property value. Changing value results in a new value for square, and through the magic of MobX, all updates are logged to the console.

Now let's assume that calculating the square of a number is an asynchronous operation: We have to make a server request, perform a database lookup or something similar. But I still want square to update (asynchronously) whenever value changes, and I still want to get the same log output.

Here is the best solution I could come up with. You can run it in JSFiddle. It's working, but there are a number of aspects I don't like.

const { observable, computed, autorun } = mobx;

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

class NumberInfo {
  @observable value = 0;
  @observable square = 0;

  constructor() {
    autorun(async () => this.square = await this.getSquare());
  }

  async getSquare() {
    const value = this.value;
    // This could be a server request, database lookup, or similar
    await sleep(500);
    return value * value;
  }
}

const numberInfo = new NumberInfo();

autorun(() => console.log(`Value changed to ${numberInfo.value}.`));
autorun(() => console.log(`Square changed to ${numberInfo.square}.`));

numberInfo.value = 2;
numberInfo.value = 3;

So here's what I don't like about my solution:

  1. MobX can only track the synchronous part of getSquare, that is, the code before the first await. In my code, I'm explicitly accessing this.value before the await. If I weren't doing this, MobX wouldn't understand that getSquare depends on value, so it wouldn't reliably call my autoruns. That's a source of errors, so it would be great if there was a cleaner way.
  2. Memory leaks: I'm using autorun in the constructor to keep square updated. Given that JavaScript has no lifecycle management (destructors) for simple objects, there is no way for me to call the disposer. I'm not sure, but that probably creates some sort of memory leak within MobX.
  3. Performance: Using autorun in the constructor to keep square updated has another downside: square will now be updated whenever value changes, even if there is no code listening to square. That goes against the rule that only relevant values should be evaluated.

Is there a better way to implement an asynchronous dependency between properties?

@spion
Copy link

spion commented Mar 6, 2017

Regarding 2 and 3, you can use mobx-utils fromPromise and just mark the field as computed

@computed get square() { 
  code here
} 

Unfortunately es7 is being needlessly conservative, so no async getters. You'll have to return fromPromise(realGetter()) for the code.

(1) is probably not fixable without switching to a custom promise implementation.

@DanielSWolf
Copy link
Author

So if I understand you correctly, I could do

  @computed get observableSquare() { 
    return fromPromise((async () => {
      const value = this.value;
      await sleep(500);
      return value * value;
    })());
  } 

  @computed get square() {
    return this.observableSquare.value;
  }

This would give me a property observableSquare which depends on value (via getSquare) and has an observable property value. And if I only want the result, I can then introduce the property value, which depends on observableValue. Correct?

-- I just tried to set up a fiddle, but something's not working right yet.

@spion
Copy link

spion commented Mar 7, 2017

Seems to be working fine. Once you change the value, expect that the computed will no longer have a value property until the whole thing completes (which takes 0.5s). So the first state change of square will be "undefined", then the square.

this. observableSquare.case({pending: ..., fulfilled: ..., rejected: ...}) will let you handle all promise states independently.

@DanielSWolf
Copy link
Author

You're right, it's working. :-)

I just expanded the test scenario a bit to cover different timing issues (like an old promise finishing after a newer one), but it all seems to be working correctly.

There's just one thing now that I'm having difficulty with: As you said (and understandably), square reverts to undefined during calculation. I understand that I can use this.observableSquare.case({ pending: ... }) to specify an alternative intermediate value. But is there a simple way to just retain the old value of square while the promise is pending?

@spion
Copy link

spion commented Mar 7, 2017

Yes.

this.observableSquare.case({
  pending: () => this.oldValue, 
  rejected: e => this.oldValue, 
  fulfilled: v => this.oldValue = v
})

(oldValue doesn't need to be observable)

Sometimes it might be a good idea to indicate in the UI that the value is stale, or that the request is in progress, so thats why fromPromise defaults to asking the user to cover all cases for the general case...

@DanielSWolf
Copy link
Author

Thank you, @spion!

Introducing a new property would certainly work. However, now we've got quite some overhead for a simple asynchronous dependency.

I found a package called computed-async-mobx. It's very similar to fromPromise, but has a number of options to determine how to deal with updates. The default mode is to keep the old value. This seems to be exactly what I've been looking for!

Using computed-async-mobx, my code gets shorter and more expressive (here's the fiddle):

class NumberInfo {
  @observable value = 0;

  observableSquare = computedAsync({
    init: 0,
    fetch: async () => {
      const value = this.value;
      await sleep(500);
      return value * value;
    }
  });

  @computed get square() {
    return this.observableSquare.value;
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants