T
TanStack7mo ago
sensitive-blue

Disable refresh and first request

I want to disable the first request made, as I am using tanstack angular experimental together with ngx store, which would be based on actions, and because of that I don't want it to perform the requests when the service is injected, and the only adaptation I verified is to put an enabled: false and then modify the status.
@Injectable()
export class AuthService {
public useAuth = injectQuery(() => ({
queryKey: [ ...routes.common.auth.index ] as const,
queryFn: async () => {
return this.getAuth([ ...routes.common.auth.index ]);
},

}));
}
@Injectable()
export class AuthService {
public useAuth = injectQuery(() => ({
queryKey: [ ...routes.common.auth.index ] as const,
queryFn: async () => {
return this.getAuth([ ...routes.common.auth.index ]);
},

}));
}
checkAuth$ = createEffect(() => this.actions$.pipe(
ofType(AuthActions.checkAuth),
switchMap(() => {
const { isSuccess } = this.authApi.useAuth();
this.toastr.info('Verificando autenticação...');

if (!isSuccess) {
return of(AuthActions.checkAuthFailure());
}
return of(AuthActions.checkAuthSuccess());
}),
));
checkAuth$ = createEffect(() => this.actions$.pipe(
ofType(AuthActions.checkAuth),
switchMap(() => {
const { isSuccess } = this.authApi.useAuth();
this.toastr.info('Verificando autenticação...');

if (!isSuccess) {
return of(AuthActions.checkAuthFailure());
}
return of(AuthActions.checkAuthSuccess());
}),
));
5 Replies
conscious-sapphire
conscious-sapphire7mo ago
I don't know enough about your code to specifically answer your question but these general principles might apply: - When authenticating a user which would create a session on the server or set a cookie, use a mutation. A mutation is called imperatively, usually from an event handler such as clicking a login button. - Try not to mix client and server state. I.e. avoid syncing server state to a client state management solution. It's fine of course to use client state as input for your query key etc. - When performing a one off data fetch when working with promise based APIs such as Angular Router guards or resolvers consider using QueryClient functions directly such as queryClient.ensureQueryData or fetchQuery.
QueryClient | TanStack Query Docs
QueryClient The QueryClient can be used to interact with a cache: tsx import { QueryClient } from '@tanstack/react-query' const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime:...
sensitive-blue
sensitive-blueOP7mo ago
@Injectable()
export class AuthEffect {

constructor(
private actions$: Actions,
private router: Router,
private authApi: AuthApiService,
private toastr: ToastrService,
) {
}

login$ = createEffect(() => this.actions$.pipe(
ofType(AuthActions.login),
switchMap(({ email, password }) => {
const { mutateAsync } = this.authApi.useAuthLogin;

return from(mutateAsync({ email, password })).pipe(
map((data) => AuthActions.loginSuccess({ token: data.token, device_id: data.device_id })),
catchError((error) => of(AuthActions.loginFailure({ error }))),
);
}),
));

loginSuccess$ = createEffect(
() => this.actions$.pipe(
ofType(AuthActions.loginSuccess),
map(({ token, device_id }) => {
localStorage.setItem('wb-token', token);
localStorage.setItem('wb-device_id', device_id);

this.toastr.success('Logado com sucesso!.');
this.router.navigate([ '/panel' ]);
}),
), { dispatch: false });

loginFailure$ = createEffect(
() => this.actions$.pipe(
ofType(AuthActions.loginFailure),
map(({ error }) => {
}),
), { dispatch: false });

checkAuth$ = createEffect(() => this.actions$.pipe(
ofType(AuthActions.checkAuth),
switchMap(() => {
const { isSuccess } = this.authApi.useAuth;
this.toastr.info('Verificando autenticação...');

if (!isSuccess) {
return of(AuthActions.checkAuthFailure());
}
return of(AuthActions.checkAuthSuccess());
}),
));

checkAuthSuccess$ = createEffect(() => this.actions$.pipe(
ofType(AuthActions.checkAuthSuccess),
map(() => {
}),
), { dispatch: false });

checkAuthFailure$ = createEffect(() => this.actions$.pipe(
ofType(AuthActions.checkAuthFailure),
map(() => {
localStorage.removeItem('wb-token');
localStorage.removeItem('wb-device_id');

this.router.navigate([ '/auth/login' ]);
}),
), { dispatch: false });

logout$ = createEffect(() => this.actions$.pipe(
ofType(AuthActions.logout),
map(() => {
localStorage.removeItem('wb-token');
localStorage.removeItem('wb-device_id');

this.router.navigate([ '/auth/login' ]);
}),
), { dispatch: false });
}
@Injectable()
export class AuthEffect {

constructor(
private actions$: Actions,
private router: Router,
private authApi: AuthApiService,
private toastr: ToastrService,
) {
}

login$ = createEffect(() => this.actions$.pipe(
ofType(AuthActions.login),
switchMap(({ email, password }) => {
const { mutateAsync } = this.authApi.useAuthLogin;

return from(mutateAsync({ email, password })).pipe(
map((data) => AuthActions.loginSuccess({ token: data.token, device_id: data.device_id })),
catchError((error) => of(AuthActions.loginFailure({ error }))),
);
}),
));

loginSuccess$ = createEffect(
() => this.actions$.pipe(
ofType(AuthActions.loginSuccess),
map(({ token, device_id }) => {
localStorage.setItem('wb-token', token);
localStorage.setItem('wb-device_id', device_id);

this.toastr.success('Logado com sucesso!.');
this.router.navigate([ '/panel' ]);
}),
), { dispatch: false });

loginFailure$ = createEffect(
() => this.actions$.pipe(
ofType(AuthActions.loginFailure),
map(({ error }) => {
}),
), { dispatch: false });

checkAuth$ = createEffect(() => this.actions$.pipe(
ofType(AuthActions.checkAuth),
switchMap(() => {
const { isSuccess } = this.authApi.useAuth;
this.toastr.info('Verificando autenticação...');

if (!isSuccess) {
return of(AuthActions.checkAuthFailure());
}
return of(AuthActions.checkAuthSuccess());
}),
));

checkAuthSuccess$ = createEffect(() => this.actions$.pipe(
ofType(AuthActions.checkAuthSuccess),
map(() => {
}),
), { dispatch: false });

checkAuthFailure$ = createEffect(() => this.actions$.pipe(
ofType(AuthActions.checkAuthFailure),
map(() => {
localStorage.removeItem('wb-token');
localStorage.removeItem('wb-device_id');

this.router.navigate([ '/auth/login' ]);
}),
), { dispatch: false });

logout$ = createEffect(() => this.actions$.pipe(
ofType(AuthActions.logout),
map(() => {
localStorage.removeItem('wb-token');
localStorage.removeItem('wb-device_id');

this.router.navigate([ '/auth/login' ]);
}),
), { dispatch: false });
}
import { injectMutation, injectQuery, type QueryKey } from '@tanstack/angular-query-experimental';
import { Injectable } from '@angular/core';
import { routes } from '_api/routes';
import { AuthRes, LoginReq, LoginRes, RecoverPasswordReq, ValidatePasswordResetTokenRes } from '_types/api/common/auth';
import { parseKey } from '_utils/swr';
import { Async } from '_types/index';
import { Collection } from '_types/laravel';
import { firstValueFrom } from 'rxjs';
import { unwrap } from '_utils/http';
import { HttpService } from '_services/api';
import { parallel } from '_utils/obj';
import { apiSwr } from '_api/swr';
import { ToastService } from '_services/toast.service';
import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http';
import { ToastrService } from 'ngx-toastr';

@Injectable()
export class AuthApiService {
constructor(
private httpService: HttpService,
private toastService: ToastService,
private toastr: ToastrService,
) {
}

// public useAuth() {
// const swr = injectQuery(() => ({
// queryKey: [ ...routes.common.auth.index ] as const,
// queryFn: async () => {
// return this.getAuth([ ...routes.common.auth.index ]);
// },
// }));
// const auth = swr.data;
//
// return { ...swr, swr };
// }

public useAuth = injectQuery(() => ({
queryKey: [ ...routes.common.auth.index ] as const,
queryFn: async () => {
return this.getAuth([ ...routes.common.auth.index ]);
},

}));

public useAuthLogin = injectMutation(() => ({
mutationFn: async (payload: LoginReq) => {
return this.loginUser([ ...routes.common.auth.login ], payload);
},
onSuccess: (d) => parallel(
apiSwr.invalidateQueries({ queryKey: [ ...routes.common.auth.index ] }),
),
onError: (err: HttpErrorResponse) => {
const { status, headers } = err;
if (status === HttpStatusCode.TooManyRequests) {
const retryAfter = this.toastService.parseRetryAfter(headers);
const minutes = ~~(retryAfter / 60);
const time = minutes ? `${minutes} minutos` : `${retryAfter} segundos`;

this.toastr.error(
'Você errou suas credenciais muitas vezes!',
`Por favor, aguarde mais ${time} antes de tentar novamente.`,
{ timeOut: 15e3 },
);
return;
}

this.toastService.requestError(err);
},
}));
}
import { injectMutation, injectQuery, type QueryKey } from '@tanstack/angular-query-experimental';
import { Injectable } from '@angular/core';
import { routes } from '_api/routes';
import { AuthRes, LoginReq, LoginRes, RecoverPasswordReq, ValidatePasswordResetTokenRes } from '_types/api/common/auth';
import { parseKey } from '_utils/swr';
import { Async } from '_types/index';
import { Collection } from '_types/laravel';
import { firstValueFrom } from 'rxjs';
import { unwrap } from '_utils/http';
import { HttpService } from '_services/api';
import { parallel } from '_utils/obj';
import { apiSwr } from '_api/swr';
import { ToastService } from '_services/toast.service';
import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http';
import { ToastrService } from 'ngx-toastr';

@Injectable()
export class AuthApiService {
constructor(
private httpService: HttpService,
private toastService: ToastService,
private toastr: ToastrService,
) {
}

// public useAuth() {
// const swr = injectQuery(() => ({
// queryKey: [ ...routes.common.auth.index ] as const,
// queryFn: async () => {
// return this.getAuth([ ...routes.common.auth.index ]);
// },
// }));
// const auth = swr.data;
//
// return { ...swr, swr };
// }

public useAuth = injectQuery(() => ({
queryKey: [ ...routes.common.auth.index ] as const,
queryFn: async () => {
return this.getAuth([ ...routes.common.auth.index ]);
},

}));

public useAuthLogin = injectMutation(() => ({
mutationFn: async (payload: LoginReq) => {
return this.loginUser([ ...routes.common.auth.login ], payload);
},
onSuccess: (d) => parallel(
apiSwr.invalidateQueries({ queryKey: [ ...routes.common.auth.index ] }),
),
onError: (err: HttpErrorResponse) => {
const { status, headers } = err;
if (status === HttpStatusCode.TooManyRequests) {
const retryAfter = this.toastService.parseRetryAfter(headers);
const minutes = ~~(retryAfter / 60);
const time = minutes ? `${minutes} minutos` : `${retryAfter} segundos`;

this.toastr.error(
'Você errou suas credenciais muitas vezes!',
`Por favor, aguarde mais ${time} antes de tentar novamente.`,
{ timeOut: 15e3 },
);
return;
}

this.toastService.requestError(err);
},
}));
}
Sorry, some information was missing, I will give more context about how I'm trying to use it. - First, I use the service to make requests and pass any different parameters, such as useAuth which verifies that the user is logged in by getting the token from localStorage and making an API query, while the second being the mutate for when the user is logging in for the first time. - Using ngx store effects, in my component I am only triggering events for such actions. For example, every time they enter the site, I will trigger the checkAuth event, which will be responsible for making the GET request to verify if they are already logged in.
conscious-sapphire
conscious-sapphire7mo ago
Setting enabled to false while authentication status shouldn't be checked yet is a good solution IMO
sensitive-blue
sensitive-blueOP7mo ago
checkAuth$ = createEffect(() => this.actions$.pipe(
ofType(AuthActions.checkAuth),
switchMap(() => from(this.authApi.useAuth.refetch())),
switchMap(({ isError }) => {
this.toastr.info('Verificando autenticação...');
if (isError) {
return of(AuthActions.checkAuthFailure());
}
return of(AuthActions.checkAuthSuccess());
}),
));
checkAuth$ = createEffect(() => this.actions$.pipe(
ofType(AuthActions.checkAuth),
switchMap(() => from(this.authApi.useAuth.refetch())),
switchMap(({ isError }) => {
this.toastr.info('Verificando autenticação...');
if (isError) {
return of(AuthActions.checkAuthFailure());
}
return of(AuthActions.checkAuthSuccess());
}),
));
I'm not sure if this would be the most correct approach, but I ended up doing it this way, by default the enabled is set to false.
conscious-sapphire
conscious-sapphire7mo ago
It's a bit too complex to my taste and too much RxJS where observables aren't great to manage state. TanStack Query and Angular signals allow for very elegant reactive state management. But when integrating with RxJS based state management perhaps in a code base that already uses that I think this is a fine solution.

Did you find this page helpful?