Search code examples
javascripttypescriptpromisees6-promiseq

Can I resolve a promise when a variable change without using deferr?


I am converting some Q-promise based typescript code to ES6-promises.

At a certain point, I used Q.defer and in my migration I just rewritten defer as an ES6 promise like explained in this comment: https://stackoverflow.com/a/49825137/13116953

I was trying to get rid of this defer approach, if possible, and was looking for alternative solutions.

Reasons of my question are:

  1. Deferred promises are considered an anti-pattern, in general
  2. Want to know if this scenario is one of the few where deferred are really the only way

Here's my scenario:

// app start, this is my low level API
init() {
    commService.subscribe("myRecordId", {
        onRecordAdd: this.onAdd
    });
}

...

private _myRecord: MyRecordObj | null = null;
private _recordReceivedDef = newDeferred(); // was Q.defer()

// callback will be called when record is received
private readonly onAdd = (recordObj: MyRecordObj) => {
    this._myRecord = recordObj;
    this._recordReceivedDef.resolve(recordObj);
}

...

// here's my async code that requires the deferred
public readonly doStuff = async () => {
    // ...
    const myRec = await this._recordReceivedDef.promise;
    // use myRef
}

My question is: is there a way I can get rid of this defer? I was thinking of something that resolves when _myRecord changes, but have no idea how to do it.

Side note: I use MobX in other parts of our app, thus having

await when(() => this._myRecord); // of course _myRecord must be @observable

would be handy, but unfortunately I cannot use MobX in this particular piece of code.

Any help is much appreciated.

Thanks a lot!


Solution

  • Assuming init is called before doStuff, the proper way would be

    init() {
        this._myRecordPromise = new Promise((resolve, reject) => {
            commService.subscribe("myRecordId", {
                onRecordAdd: (recordObj: MyRecordObj) => {
                    // callback will be called when record is received
                    resolve(this._myRecord = recordObj);
                }
            });
        });
    }
    
    …
    
    private _myRecord: MyRecordObj | null = null;
    private _myRecordPromise: Promise<MyRecordObj>;
    
    …
    
    public readonly doStuff = async () => {
        …
        const myRec = await this._myRecordPromise;
        // use myRef
    }
    

    You might even drop the _myRecord completely and keep only the _myRecordPromise.

    However, you might want to consider not constructing your instance at all before the record is received, see Is it bad practice to have a constructor function return a Promise?.

    If init is called at some arbitrary time, you will need some kind of defer pattern, but you don't need newDeferred() for that. Just write

    init() {
        commService.subscribe("myRecordId", {
            onRecordAdd: this.onAdd
        });
    }
    
    …
    
    private _myRecordPromise: Promise<MyRecordObj> = new Promise(resolve => {
        this.onAdd = resolve;
    });
    private readonly onAdd: (recordObj: MyRecordObj) => void;