Nilo
Nilo

Reputation: 113

A way to avoid explicitly passing the "this" context?

While working on a new product I have created a back-end and front-end project. For front-end I am using Angular framework with Typescript. Below is a question due to me being new to the language (a few days old). My question is around callbacks and how to avoid the explicit pass with the "this" context. There are a few resources I have read which I will link.

Below I am implementing a wrapper for the HttpClient. The quick version is flow control with modals that follow a plugin architecture(backed by angular routing) is best complimented with a central delegation using observers and subscribers to broadcast the errors like 401 for a graceful re-entry(in my opinion) - we won't get into that though but was mention as context may help.

Here are the bare bones of my code: The Wrapper =>

export class WebService {

  constructor(private httpClient: HttpClient,
              private exceptionService: ExceptionService<Exception>) { }

  public post<T>(url: string, dataToPost: any, callBack: (responseData: T) => 
                void, callBackInstance: any): void {

    this.httpClient.post<T>(url, dataToPost).subscribe(
      (data: T) =>  {
        callBack.call(callBackInstance, data);
      },

      (error: HttpErrorResponse) => {
        this.exceptionService.notify(error);
      }
    );

Now I am able to explicitly manage the "this" context for the callback using the .call() to insert it. I don't mind using this in any of your suggestions - however looking at the signature, you will find that the method requires you to pass in the "this" context you want(callbackInstance). This pushes some responsibility onto the caller of the method that I do not want. To me a class is very much like an array with the "this" as an initial displacement - since I am passing in the method for the callback; is there really no way to inspect that method to derive the appropriate "this"? Something along the lines of: callbackInstance = callback.getRelativeContext(); callBack.call(callBackInstance, data); This would eliminate the extra param making the method less error prone for my team to use.

Links to resources are welcome - but please try to narrow it down to the relevant part if possible.

Links:

For updating the "this" context

Parameter callbacks

EDIT: From accepted answer I derived and placed in test case:

const simpleCallback = (response) => {holder.setValue(response); };
service.post<LoginToken>(Service.LOGIN_URL, '', simpleCallback);

Upvotes: 2

Views: 473

Answers (1)

Marian
Marian

Reputation: 4079

If you need to pass the context to the callback, then the callback itself will rely on that context:

function explicitContext(callback, context) {
    const arg = 1;
    callback.call(context, arg);
}

function implicitContext(callback) {
    const arg = 1;
    const someCleverContext = {importantVal: 42, importantFunc: () => {}};
    callback.call(someCleverContext, arg);
}

Consider the usage, if we need to actually access the context in the callback:

function explicitUsage() {
    const someCleverContext = {importantVal: 42, importantFunc: () => {}};
    const callback = function(arg) {this.importantFunc(arg);}
    explicitContext(callback, someCleverContext);
}

function implicitUsage() {
    const callback = function(arg) {this.importantFunc(arg);}
    implicitContext(callback);
}

In both cases we are actually leaking the details about the context and forcing some responsibility on the consumer! Now, there isn't a magic way to go around it if we really need to pass the context. The good news is, we probably don't need to pass the context in the first place.

export class WebService {

    constructor(
        private httpClient: HttpClient,
        private exceptionService: ExceptionService<Exception>)
    { }

    public post<T>(url: string, dataToPost: any, callBack: (responseData: T) => void): void {

        this.httpClient.post<T>(url, dataToPost).subscribe(
            (data: T) => {
                callBack(data);
            },

            (error: HttpErrorResponse) => {
                this.exceptionService.notify(error);
            },
        );
    }
}

This way we can let the client code only care about the responseData, and if they need some clever context, they are free to bind it themselves:

function usage() {
    let webService: WebService;
    const simpleCallback = (response) => {console.log(response);} // can inline too
    webService.post('/api', {data: 1}, simpleCallback);

    const cleverContextCallback = function(response) {this.cleverLog(response)};
    const cleverContext = {cleverLog: (data) => console.log(data)};
    const boundCallback = cleverContextCallback.bind(cleverContext);
    webService.post('/api', {data: 1}, boundCallback );
}

Having said all that, I would definitely recommend just returning the observable from your services.

export class WebService {

    constructor(
        private httpClient: HttpClient,
        private exceptionService: ExceptionService<Exception>)
    { }

    public post<T>(url: string, dataToPost: any, callBack: (responseData: T) => void): Observable<T> {

        const observable = this.httpClient.post<T>(url, dataToPost);
        // Note that httpClient.post automatically completes.
        // If we were using some other library and we would want to close the observable ourselves,
        // you could close the observable yourself here after one result:

        if ('we need to close observable ourselves after a result') {
            return observable.pipe(take(1));
        }

        if ('we need to handle errors') {
            return observable.pipe(
                catchError(error => {
                    this.exceptionService.notify(error);
                    if ('We can fallback') {
                        return of('Fallback Value');
                    } else {
                        throw new Error('OOPS');
                    }
                }),
            );
        }

        return observable;
    }
}

Dealing with errors, closing and other chores inside the service would let the consumer of the service focus on the data from the response.

Upvotes: 1

Related Questions