2

That's a question about the fundamental things but I don't know how to return back response to the component depending on the action result: success or error, so I can execute related to them actions.

I have a component SignInComponent which collects all necessary information from the form and sends it to my AuthService which handles with my GraphQL requests (and for now redirecting and other things). For a scenario that request is successful, everything works fine so far, but if there is any error response from API I need to have the errors and information about them in my SignInComponent. (if wrong credentials I should inform the user about that and so on)

I tried to return the value like:

signIn.subscribe({
        next: // actions for successful response,
        error: (err) => {return 'errors'} // from here
      })

and also I tried to throw an Error like this:

signIn.subscribe({
        next: // actions for successful response,
        error: (err) => {throw new Error('Oops')} // The Error
      })

and tried to catch it like this in my SignInComponent:

      try {
        this.authService.signIn(params);
      } catch (e) {
        // handling the error accordingly 
      }

But all of that mentioned above didn't help me get response in my SignInComponent and be notified about the result status.

here is my code and I hope someone can tell me how to handle this situation using good practices.

auth.service.ts

import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { SignInMutation, SignInInput } from './sign-in.graphql';
import { SignUpMutation, SignUpInput } from './sign-up.graphql';
import { BehaviorSubject } from 'rxjs';

// TODO: To learn if observable is unsubscribed in services automatically. In Classes there is an Inteface OnDestroy for that

@Injectable({
  providedIn: 'root'
})
export class AuthService {

  auth: BehaviorSubject<boolean> = new BehaviorSubject(false);

  constructor(
    private router: Router,
    private signInMutation: SignInMutation,
    private signUpMutation: SignUpMutation
  ) {
    this.isSignedIn();
  }

  signIn(params: SignInInput) {

    this.signInMutation.mutate(params)
      .subscribe({

        next: ({ data }) => {

          const signIn = data.signIn;
          const token = signIn.token;

          const { payload } = JSON.parse(atob(token.split('.')[1]));
          const currentUser = payload.userData;

          const localData = {
            currentUser,
            token
          };

          this.setLocalData(localData);

          this.router.navigate(['/dashboard']);
          this.auth.next(true);

        },

        error: (err) => {
          throw new Error('Ooops...');
        }

      });



  }

  signUp(SignUpInput: SignUpInput) {

    this.signUpMutation.mutate({ SignUpInput })
      .subscribe({
        next: console.log,
        error: console.log,
      });

  }

  private getLocalData(name: string): null | object | string {
    const data = localStorage.getItem(name);
    if (!data) { return null; }

    if (/^(\{).*(\})$/i.test(data)) {
      return JSON.parse(data);
    } else {
      return data;
    }
  }

  private setLocalData(data: object) {

    for (const key in data) {
      if (data.hasOwnProperty(key)) {
        const value = data[key];

        if (typeof value === 'object') {
          localStorage.setItem(key, JSON.stringify(data[key]));
        } else {
          localStorage.setItem(key, data[key]);
        }

      }
    }

  }

  signOut() {
    localStorage.removeItem('token');
    localStorage.removeItem('currentUser');
    this.router.navigate(['/']);
    this.auth.next(false);
  }

  private isSignedIn() {
    const token = this.getLocalData('token') as string;
    const checkFormat = (token && token.split('.').length === 3) ? true : null;

    if (token && checkFormat) {
      this.auth.next(true);
    }

  }

}

signInComponent

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { AuthService } from '../auth.service';
import { emailFormat } from '../../_helpers/custom-validation';

@Component({
  selector: 'app-sign-in',
  templateUrl: './sign-in.component.html',
  styleUrls: ['./sign-in.component.scss']
})
export class SignInComponent implements OnInit {

  signInForm: FormGroup;
  submitted: boolean;

  constructor(
    private fb: FormBuilder,
    private authService: AuthService,
  ) { }

  ngOnInit(): void {
    this.signInForm = this.fb.group({
      email: ['', [emailFormat]],
      pwd: [''],
    });

    this.reset();
  }

  onSubmit(e) {
    e.preventDefault();
    this.submitted = true;

    if (this.signInForm.valid) {
      const params = {
        email: this.signInForm.value.email,
        password: this.signInForm.value.pwd
      };

      this.authService.signIn(params);

      this.reset();
    }

  }

  private reset() {
    this.signInForm.reset();
    this.submitted = false;
  }

}

Thank you all in advance!

2 Answers 2

2

So this is more of an rxjs issue rather than Angular issue.

this.signInMutation.mutate(params) returns an Observable which contains wrapped data of the server response. In your case, having signin() method subscribe to this observable is not a good idea.

Instead, a common practice would simply return the observable straight away:

signIn(params: SignInInput) {
  return this.signInMutation.mutate(params);
}

Now you might say there are some data post-processing work you want to do such as things you did in your original subscribe() clause.

Okay, let's move them into a pipe()

function signIn(params: SignInInput) {
  return this.signInMutation.mutate(params).pipe(
    tap(data => {
      const signIn = data.signIn;
      const token = signIn.token;

      const { payload } = JSON.parse(atob(token.split('.')[1]));
      const currentUser = payload.userData;

      const localData = {
        currentUser,
        token,
      };

      this.setLocalData(localData);

      this.router.navigate(['/dashboard']);
      this.auth.next(true);
    }),
  );
}

Okay, now let's move back to your component code

onSubmit(e) {
  // ...

  this.authService.signIn(params).subscribe({
    error: err => {
      // handle your err here, where the handler belongs
      this.errorMessage = 'oops, errors here! ' + err.message
    }
  })
}

Basically, this is one of the most common practices for handling rxjs based remote responses in Angular. Of course, there are a few other things to worry about, such as unsubscribe() to this subscription when your component gets destroyed. But I don't want to go out of topic.

Having your Angular services return observables directly also helps you share & compose them further with other services or rxjs based candidates, such as reactive forms.

Sign up to request clarification or add additional context in comments.

4 Comments

Thanks a lot! I thought it's better to subscribe in a service and then just give a response back to a component or any other service. Kind of encapsulation) Did you see any other issues on the code where I should have done in a different way? And one more thing concerning rxjs library. Is it a good practice to use it on backend with express?
Apart from the original issue I mentioned in the thread, everything else looks fine. Of course, there is room for further improvements such as using NGRX for local data manipulation and router redirect side effects. But handling them inside service is not a wrong approach. You can also using rxjs in backend, personally I would say it's less useful than frontend as backend process more "stateless" data, most of the time Promise is more than enough to deal with it.
Is NGRX worth using, basing on your experience? I've never used it before but I guess it originally came from React Eco-system (Redux). That's where it's widely used.
@Valaryo yes you are correct. My advice is do not use it until you clearly know how it's going to help you build a better app. Otherwise don't use it. You can read a few tutorials, write a couple of demos, and start from there.
1

As you have already a auth boolean flag in your AuthService, you can opt to add a error flag with the error message.

Your components, or other services could subscribe to it to retrieve the current state.

This approach (in a state management way), give your more flexibility, to retrieve the different state (auth yes/no, error/no error...). You could also add a loading flag, which is set to yes during authentication process.

export class AuthService {
  auth = new BehaviorSubject<boolean>(false);
  error = new BehaviorSubject<string>(null);

  auth$ = this.auth.asObservable();
  error$ = this.error.asObservable();

  ...

  signIn(params: SignInInput) {
    this.auth.next(false);

    this.signInMutation.mutate(params).subscribe(
      data => {
        ...
        this.auth.next(true);
        this.error.next(null);
      },

      error => {
        this.error.next('error message');
      }
    );
  }
} 

inside component.ts, we can subscribe to error message to do some specific actions, but it's not required. Just use async pipe inside template is ok to display a simple message :

export class AppComponent {
  error$ = this.authService.error$;
  ...

  constructor(...) {
    // manually subscribe to do some other tasks...
    // but it's not required to only display inside template (see below)
    this.error$.subscribe(error => {
      if (error) {

      }
    });

    // becareful to manage unsubscription (not done here to keep code simple)
  }  
}

Inside template, we can display a message in case of error:

 <p *ngIf="error$ | async as error">{{ error }}</p>

1 Comment

Thanks a lot for help!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.