1.1. Модуль регистрации (NgRx)

Начальная структура модуля:

Базовая структура auth-module
Базовая структура auth-module, которая еще содержит только первый модуль, экшн и т.п.
  1. В папке Store хранится весь NgRx (все файлы, которые к нему относятся)
    Файл Store/Actions/actionTypes.ts содкржит енам с названиями type для экшенов, которые используются в данном модуле (пример ниже в разделе “Настраиваем работу NgRx для этого модуля”).
  2. В папке Types находится вся остальная типизация, где каждый интерфейс вынесен в отдельный файл. 
  3. В папке auth\store\actions экшены, также разнесенные по отдельным файлам.

1. Настройка роутинга

1) Если при созданиии проекта через cli включить роутинг, то создастся главный роутинг-модуль проекта – файл app/app-routing.module.ts:

const routes: Routes = [];

@NgModule({
  imports: [RouterModule.forRoot(routes)], //в главном модуле используем для регистрации роутов метод forRoot
  exports: [RouterModule],
})
export class AppRoutingModule {}

2)  Этот AppRoutingModule импортируется  в app.module.ts, как и все остальные модули – просто добавляется внутрь массива imports:

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, AppRoutingModule, AuthModule, HttpClientModule],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

3) Для того, чтобы наш модуль был полностью автономным и при удалении папки auth ничего не надо было нигде подчищать (касается всех модулей в проектах), роутинг прописываем  не в app-routing.module.ts (там тоже б сработало), а в локальном модуле auth.module.ts:

const routes: Routes = [{path: 'register', component: RegisterComponent}]; //наш роутинг

@NgModule({
  imports: [
    CommonModule,
    RouterModule.forChild(routes), //здесь подключаем и используем метод forChild
    ReactiveFormsModule,
    StoreModule.forFeature('auth', reducer),
  ],
  declarations: [RegisterComponent],
  providers: [AuthService],
})
export class AuthModule {}

2. Разметка формы и подключение реактивных форм

1) Внутри нашего html в register.component.html создаем форму, в которой один fieldset оборачивает кнопку и 3 других fieldset(а), в которых находятся инпуты для username, email и password.

2) В auth.module.ts импортируем ReactiveFormsModule.

3)  В register.component.ts:

  • в конструкторе подключаем FormBuilder;
  • в ngOnInit запускаем метода для инициализации форм (this.initializeForm), объявляем форму (FormGroup) и создаем сам этот метод. Метод group на вход принимает объект, свойства которого являются полями нашей формы. Каждое свойство содержит массив, где первым аргументом идет значение поля, а вторым необязательным – валидатор.

4) В register.component.html связываем нашу форму в html с созданной formGrouop. А также на форму вешаем экшн onSubmit, и создаем соответствующую функцию внутри класса.

Вот так выглядит форма в register.component.html:

<form [formGroup]="form" (ngSubmit)="onSubmit()">
         <fieldset>
           <fieldset >
             <input
               type="text"
               placeholder="Username"
               formControlName="username"
             />
           </fieldset>

           <fieldset class="form-group">
             <input
               type="text"
               placeholder="Email"
               formControlName="email"
             />
           </fieldset>

           <fieldset class="form-group">
             <input
               type="password"
               placeholder="Password"
               formControlName="password"
             />
           </fieldset>

           <button
             class="btn btn-lg btn-primary pull-xs-right"
             type="submit"
           >
             Sing up
           </button>
         </fieldset>
       </form>

register.component.ts:

export class RegisterComponent implements OnInit {
  public form!: FormGroup;

  constructor(private fb: FormBuilder) {}

  ngOnInit(): void {
    this.initializeForm();
  }

  public onSubmit(): void {
    this.store.dispatch(registerAction(this.form.value)); //это из будущего функционала (диспатчинг экшна)
  }

  private initializeForm(): void {
    this.form = this.fb.group({
      username: ['', Validators.required],
      email: '',
      password: '',
    });
    console.log(this.form.valid);
  }
}

3. Установка NgRx

Описано здесь: Установка NgRx и начало работы с ним

4. Настраиваем работу NgRx для этого модуля

1) Создаем файл app\auth\store\actions\actionTypes.ts, где через енам прописываем типы экшенов, в названиях которых в квадратных скобках идет префикс, указывающий на принадлежность к этому модулю:

export enum ActionTypes {
  REGISTER = `[Auth] Register`,
  REGISTER_SUCCESS = `[Auth] Register success`,
  REGISTER_FAILURE = `[Auth] Register failure`,
}

*START в первом элементе не пишем, поэтому просто REGISTER .
**Такой подход именно с енамом – это просто методика Oleksandr Kocherhin, а не какие-то оф. рекомендации.

Создаем экшены

2) Создаем сами экшены для регистрации при помощи метода createAction в папке auth\store\actions. Метод createAction принимает 2 параметра – тип и пропсы. При этом пропсы документация говорит передавать в виде функции, а не просто объектом. Дженериком у пропса будет не просто интерфейс, а объект (хотя вроде работает, если и просто интерфейсом оставить).
app\auth\store\actions\register.action.ts:

export const registerAction = createAction(
  ActionTypes.REGISTER,
  props<{request: RegisterRequestInterface}>() //* интерфейсы на самом деле мы еще не создавали – он описан ниже. Можно передать сюда объект. Но все равно потом менять на интерфейс
);
//понадобиться аж в пункте 9
export const registerSuccessAction = createAction(
  ActionTypes.REGISTER_SUCCESS,
  props<{currentUser: CurrentUserInterface}>()
);

//понадобиться аж в пункте 9-10
export const registerFailureAction = createAction(
  ActionTypes.REGISTER_FAILURE,
  props<{errors: BackendErrorsInterface}>()
);

*В названиях экшенов не забываем в конце указывать, какая это сущность – registerAction, в данном случае.

3) Теперь наш экшн можно задиспатчить. Для этого в компоненте в конструктор добавляем Store, и теперь в onSybmit можно вызвать специальный метод у стора dispathc, передавая туда наш только что созданный registerAction и в качестве его аргументов поля формы, которые он ожидает.

...

  constructor(private fb: FormBuilder, private store: Store) {}

...

  public onSubmit(): void {
    this.store.dispatch(registerAction(this.form.value)); // на самом деле наш бекенд ожидает объект user: {} это пофиксим позже
  }

4) Подключаем наш модуль стора в главный модуль app.module.ts:

...
imports: [
   BrowserModule,
   AppRoutingModule,
   AuthModule,
   StoreModule.forRoot({}), //внутрь передаются редьюсеры, но у нас их пока еще нет
   HttpClientModule,
 ],
...

5. Пишем первые интерфейсы

1) Интерфейсы, которые мы будим использовать во многих местах приложения, создаем в папке app/shared/types. Например, в нашем случае это интерфейс для данных юзера. Для этого создаем отдельный файл в данной папке – currentUser.interface.ts. В названии интерфейса также указываем в конце тип сущности и у нас получается CurrentUserInterface. В полях, где может приходить null, лучше отдавать предпочтение записи bio: string | null вместо применения вопросительного знака (bio?: string), обозначающего возможный undefined.

Current user interface

2) Все реквесты и респонсы стоит тоже покрывать интерфейсами. Создаем теперь уже не в шередах, а локально файл app\auth\types\registerRequest.interface.ts, в котором прописываем интерфейс для полей нашей формы регистрации:

registerRequest.interface.ts

Теперь этот интерфейс можно использовать в нашем ранее созданном экшене для регистрации (app\auth\store\actions\register.action.ts) в пропсах, вместо указания целого объекта (в примере этого файла выше на самом деле этот интерфейс уже указан был, чтобы сразу показать правильный код).

3) Создаем в app\auth\types\authState.interface.ts простой интерфейс с булевым значеием для initialState будущего редьюсера, который будет менять в этом стейте местами true и false, пока идет регистрация и когда она закончилась (т.е. пришел ответ с сервера):

authState

4) Это мы создали наш локальный объект. Но он по факту является частью нашего глобального стора. Поэтому создадим интерфейс для нашего глобального стора в шередах (хотя понадобиться нам он аж в пункте 7 при создании селекторов). Для этого создадим файл app\shared\types\appState.interface.ts и там будет наш интерфейс AppStateInterface, который будет состоять из частичек – локальных интерфейсов. Пока только из AuthStateInterface.

import {AuthStateInterface} from 'src/app/auth/types/authState.interface';

export interface AppStateInterface {
  auth: AuthStateInterface;
}

6. Создаем редьюсер

В app\auth\store\reducers.ts создаем наш initialState, который типизируем созданным ранее интерфейсом и потом при помощи функции createReducer создаем сам редьюсер. В него передаем initialState и метод on (импортируем из @ngrx/store, что важно не перепутать). Эта функция on принимает наш экшн registerAction (объект с полями формы регистрации) и вторым аргументом функцию, принимающую state (глобальный стейт) и возвращающую его часть – наш AuthStateInterface. И в возвращаемом этой второй функцией объекте при помощи spread оператора делаем копию стейта и меняем значение поля isSubmitting на то, что пришло в экшене.

import {createReducer, on, Action} from '@ngrx/store';
import {registerAction} from 'src/app/auth/store/actions/register.action';
import {AuthStateInterface} from 'src/app/auth/types/authState.interface';

const initialState: AuthStateInterface = {
  isSubmitting: false,
};

export const authReducer = createReducer(
  initialState,
  on(
    registerAction,
    (state): AuthStateInterface => ({...state, isSubmitting: true})
  )
);

export const authFeatureKey = 'auth';

 

Ранее была проблема с экспортом редьюсара – мы не могли напрямую экспортировать редьюсеры. И согласно документации для этого создавалась отдельная функция. Это связано с тем, что продакшн-билде const не работал  правильно с импортом. Поэтому для экспорта редьюсеров внизу файла использовали специальную функцию, которую потом передавали в импорты модуля.

auth.module.ts

Старый синтаксис:

export function reducer(state: AuthStateInterface, action: Action) {
  return authReducer(state, action);
}

//импорт в app\auth\auth.module.ts:
import {reducer} from 'src/app/auth/store/reducers';
...
 imports: [
    CommonModule,
    RouterModule.forChild(routes),
    ReactiveFormsModule,
    StoreModule.forFeature('auth', reducer), //здесь подключаем редьюсер
  ],

export const authFeatureKey = 'auth'; //название ключа экспортируем с этого файла, иначе будет ошибка!!!

Теперь можно без дополнительной функции, а экспортировать прямо const с редьюсерами.

Теперь по-новому пишем так:

app\auth\auth.module.ts:

export const authReducer = createReducer(...

import * as authReducers from 'src/app/auth/store/reducers';
...
   CommonModule,
    RouterModule.forChild(routes),
    ReactiveFormsModule,
    StoreModule.forFeature(authReducers.authFeatureKey, authReducers.authReducer), //здесь подключаем редьюсеры
  ],

*В app.module передаваемый объект (StoreModule.forRoot({})) остается пустым (можно было бы подключить и туда, но так правильнее).

7. Создаем селектор и получаем по нему данные в компоненте

1) В новом файле app\auth\store\selectors.ts создаем authFeatureSelector, который будет хранить функцию createFeatureSelector, возвращающую по ключу authReducers.authFeatureKey (тот что и в модуль передавали из файла с редьюсером) типизированную функцию для возвращения части стейта. Она нужна для следующего шага.

Ранее, чтобы это сделать более правильно и понимать написанное нужно было передавать в дженерик этой функции интерфейс глобального стейта. Теперь достаточно только локального.

2) Теперь, используя нашу созданную функцию authFeatureSelector, объявим ниже переменную  isSubmittingSelector. В ней будет лежать функция  из NgRx createSelector, принимающая ранее созданную authFeatureSelector и вторым параметром стрелочную функцию, возвращающую уже непосредственно значение конкретного поля из стейта.

app\auth\store\selectors.ts

import {createFeatureSelector, createSelector} from '@ngrx/store';

import {AuthStateInterface} from 'src/app/auth/types/authState.interface';
import * as authReducers from 'src/app/auth/store/reducers';

const authFeatureSelector = createFeatureSelector<AuthStateInterface>(
  authReducers.authFeatureKey
);

export const isSubmittingSelector = createSelector(
  authFeatureSelector,
  (authState: AuthStateInterface) => authState.isSubmitting
);

Теперь мы можем в любом месте приложения с помощью isSubmittingSelector получить  значение isSubmitting.

3) Получим значение из стора, подписавшись на созданный селектор, в app\auth\components\register\register.component.ts. Для этого в компоненте создаем свойство isSubmitting$ для Observable и передадим boolean в него, так как приходить нам будет значение этого типа.
В ngOnInit добавляем метод initalizeValues, в котором ниже инициализируем наш обсервебл, получая его из Store при помощи пайпа select.
Теперь в темплейте при помощи pipe async мы можем получать значение стейта и, например, блокировать кнопку пока идет авторизация.

register.component.ts

export class RegisterComponent implements OnInit {
...
  public isSubmitting$!: Observable<boolean>;

  constructor(private fb: FormBuilder, private store: Store) {}

  ngOnInit(): void {
    this.initializeForm();
    this.initalizeValues();
  }

  public onSubmit(): void {
//переменная request нужна, так как сервер ожидает объект user, а не просто поля
   const request: RegisterRequestInterface = {
      user: this.form.value,
    };
    this.store.dispatch(registerAction(request));
  }

  private initializeForm(): void {
  ...
  }

  private initalizeValues(): void {
    this.isSubmitting$ = this.store.pipe(select(isSubmittingSelector)); //здесь по селектроу получаем данные, передавая созданный ранее селектор в аргументы пайпа
  }
}

register.component.html

...
 <button
   type="submit"
   [disabled]="isSubmitting$ | async"
  >
    Sing up
  </button>
...

8. Создаем сервис

Выше мы уже создали RegisterRequestInterface, который мы передаем нашему API (тело запроса). А также CurrentUserInterface – объект юзера, который хотим хранить на фронтенде.

1) Теперь надо создать интерфейс для ответа с сервера после регистрации / авторизации. Этот интерфейс будет использоваться только в модуле auth, поэтому создаем его локально.

app\auth\types\authResponse.interface.ts (логично было бы назвать RegisterResponse по аналогии с RegisterRequestInterface, но такое более универсальное название название из-за того, что этот объект приходит не только при регистрации, но и при авторизации):

import {CurrentUserInterface} from 'src/app/shared/types/currentUser.interface';

export interface AuthResponseInterface {
  user: CurrentUserInterface;
}

2) Такие вещи, как base url можно и нужно вынести в переменное окружение – src\environments\environment.ts:

export const environment = {
  production: false,
  apiUrl: 'https://api.realworld.io/api/users/api',
};

3) Создадим сервис auth.service.ts. Так как пока мы не знаем сколько всего будет функций в этом модуле, то поэтому нет смысла пока разбивать его на несколько сервисов. Поэтому проще всего назвать, как и модуль. Важно не забыть указать оператор @Injectable (если без cli вручную создавать файл).

Мепим (используем pipe map) данные из респонса по причине, что нам приходит объект user: {...}, а нам нужен только сам объект для удобства.

import ...

@Injectable()
export class AuthService {
  constructor(private http: HttpClient) {}

  register(data: RegisterRequestInterface): Observable<CurrentUserInterface> {
    const url = environment.apiUrl + 'users';

    return this.http.post<AuthResponseInterface>(url, data).pipe(
      map((response: AuthResponseInterface) => {
        return response.user;
      })
    );
  }
}

Теперь в register.component.ts мы могли бы даже уже без проблем использовать наш сервис при submit. Но будем мы его использовать с помощью эффектов, как и положенно с ngRx.

Также надо добавить этот сервис в providers модуля (ниаче будут ошибки в браузере). В нашем случае локально в auth.module.ts, так как он нужен нам будет только здесь. Ну и httpClient не забыть тоже заимпортить в главный модуль.

9. Добавляем сайд эффекты регистрации отправки запроса и обработки ответов

Наш компонент ничего не должен знать ни о сервисах, ни о подписках, ни о том как сервис работает. Поэтому нам нужны эффекты. До этого у нас были только синхронные операции, что видно через дебаг в консоли браузера во вкладке redux. Но теперь будем асинхронно менять стейт после получения ответа от сервера.

Идея эффектов по факту такая, что при обращении к API тригерим в начале один экшн (в данном случае registerAction), а в конце другой (registerSuccessAction или registerFailureAction). В итоге наше приложение будет работать только с экшинами и приложение будет думать, что оно синхронно. А асинхронными остануться только эффекты.

1) Устанавливаем эффектыnpm install @ngrx/effects --save

2) Аналогично экшенам создаем папку и файл для эффектов в локальном модуле
app\auth\store\effects\register.effect.ts:

import ...

@Injectable()
export class RegisterEffect {
  register$ = createEffect(() =>
    this.actions$.pipe(
      ofType(registerAction),
      switchMap(({request}) => {
        return this.authService.register(request).pipe(
          map((currentUser: CurrentUserInterface) => {
            return registerSuccessAction({currentUser});
          }),
          catchError((errorResponse: HttpErrorResponse) => {
            return of(
              registerFailureAction({errors: errorResponse.error.errors})
            );
          })
        );
      })
    )
  );

  constructor(private actions$: Actions, private authService: AuthService) {}
}

this.actions$ – это абсолютно все экшены, которые вылетают в приложении. Все они попадают в этот стрим. При помощи ofType мы указываем, на какой экшн здесь реагировать. Когда этот экшн происходит, то при помощи switchMap мы обрабатываем приходящие пропсы этого экшна (данные с формы регистрации). А конкретнее, мы запускаем функцию из сервиса, подключенного в конструкторе, для отправки запроса на сервер. Нам возвращается обсервбл, как и надо для switchMap. 

После запроса мы при удачном получении ответа выполняем экшн registerSuccessAction, передавая в ему юзера с ответа, а в случае ошибки внутри catchError – registerFailureAction, передавая в него ошибки.

Эта сложная конструкция для эффекта будет аналогичной практически всегда!

3) Подключаем эффекты в модули:

В локальном модуле auth.module.ts в массив imports добавляем вот такую конструкцию:

...
EffectsModule.forFeature([RegisterEffect])
...

А в app.module.ts вот такую:

...
EffectsModule.forRoot([])
...

Теперь при сабмите формы все должно работать и в компоненте нет сайд-эффектов (API – это сайд-эффект), как при обычном подходе без ngRx. А все что есть в компоненте – диспатчи и подписки.

10. Добавляем новые редьюсеры (для удачной и неудачной регистрации)

1) Создаем интерфейс для ошибок в шередах, так как ошибки везде в нас будут иметь одинаковую структуру.

app\shared\types\backendErrors.interface.ts:

export interface BackendErrorsInterface {
  [key: string]: string[];
}

2) И теперь этот интерфейс можнно использовать в экшене registerFailureAction в файле app\auth\store\actions\register.action.ts. Но на самом деле он уже выше в примере этого файла (пункт 4) был прописан наперед. Поэтому здесь вот просто дублирование самого экшна без остального файла:

...

export const registerFailureAction = createAction(
  ActionTypes.REGISTER_FAILURE,
  props<{errors: BackendErrorsInterface}>()
);

3) В эффектах из полученных ошибок от сервера и типизированных как errorResponse: HttpErrorResponse извлекаем errorResponse.error.errors, как они к нам приходят из сервера, и прокидываем в наш экшн. Это тоже уже есть в примере файла register.effect.ts в предыдущем пункте (забежали там наперед).

Добавляем новые редьюсеры

4) расширяем наши редьюсеры в reducers.ts. Сейчас у нас там только одно поле isSubmiting, которого недостаточно. Поэтому сразу добавим остальные нужные поля интерфейса AuthStateInterface в authState.interface.ts:

export interface AuthStateInterface {
  isSubmitting: boolean;
  currenUser: CurrentUserInterface | null;
  isLoggedIn: boolean | null;
  validationErrors: BackendErrorsInterface | null;
}

И теперь дополняем наш файл с редьюсерами app\auth\store\reducers.ts обработкой новых двух экшенов:

import ... 

const initialState: AuthStateInterface = {
  isSubmitting: false,
  currenUser: null,
  isLoggedIn: null,
  validationErrors: null,
};

export const authReducer = createReducer(
  initialState,
  on(
    registerAction,
    (state): AuthStateInterface => ({
      ...state,
      isSubmitting: true,
      validationErrors: null,
    })
  ),

  on(
    registerSuccessAction,
    (state, action): AuthStateInterface => ({
      ...state,
      isSubmitting: false,
      isLoggedIn: true,
      currenUser: action.currentUser,
    })
  ),

  on(
    registerFailureAction,
    (state, action): AuthStateInterface => ({
      ...state,
      isSubmitting: false,
      validationErrors: action.errors,
    })
  )
);

export const authFeatureKey = 'auth';

И по факту теперь можно эту информацию использовать в любой точке приложения, чтобы проверить залогинен юзер или нет.

11. Создаем модуль для отрисовки ошибок валидации

В данном случае наши ошибки должны выглядеть как массив строк, по которому мы проходимся циклом и их рендерим. Если в некоторых полях больше одного сообщения, то выводим их через запятую в одну строку. И так как этот компонент используется на разных страницах (sign up, sign in), то он будет хранится в шередах.

1) Добавим в файл app\auth\store\selectors.ts еще один селектор. Он, как и остальные, нужен для извлечения полей со стора. Первый аргумент в нем authFeatureSelector, так как это поле находится в authState (AuthStateInterface), ну а вторым параметром функция, возвращающая validationErrors.

import...

import * as authReducers from 'src/app/auth/store/reducers';

const authFeatureSelector = createFeatureSelector<AuthStateInterface>(
  authReducers.authFeatureKey
);

//ранее созданные селектор
export const isSubmittingSelector = createSelector(
  authFeatureSelector,
  (authState: AuthStateInterface) => authState.isSubmitting
);

//новый селектор
export const validationErrorsSelector = createSelector(
  authFeatureSelector,
  (authState: AuthStateInterface) => authState.validationErrors
);

2) Получаем через селектор данные ошибки валидации из стейте в компоненте register.component.ts, аналогично тому, как получали isSubmitting$ – забайндив переменную backendErrors$ со стейтом.

export class RegisterComponent implements OnInit {
  public form!: FormGroup;
  public isSubmitting$!: Observable<boolean>;
  backendErrors$!: Observable<BackendErrorsInterface | null>;

  constructor(private fb: FormBuilder, private store: Store) {}

  ngOnInit(): void {
    this.initializeForm();
    this.initalizeValues();
  }

//............ other code .........

  private initalizeValues(): void {
    this.isSubmitting$ = this.store.pipe(select(isSubmittingSelector));
    this.backendErrors$ = this.store.pipe(select(validationErrorsSelector));
  }
}

3) Создаем компонент, куда будем прокидывать эти ошибки для рендеринг.

Вот так выводим его в темплейте register.component.html (хотя самого компонента еще не создали):

.... 
<div class="col-md-6 offset-md-3 col-xs-12">
        <h1 class="text-xs-center">Sign Up</h1>
        <p class="text-xs-center">
          <a [routerLink]="['/login']">Have an account?</a>
        </p>

        <mc-backend-error-messages
          *ngIf="backendErrors$ | async"
          [backendErrors]="backendErrors$ | async"
        >
        </mc-backend-error-messages>

        <form [formGroup]="form" (ngSubmit)="onSubmit()">
          <fieldset>
            <fieldset class="form-group">
              <input
                type="text"
                class="form-control form-control-lg"
                placeholder="Username"
                formControlName="username"
              />
            </fieldset>
....

В папке shared у нас будет папка modules, содержащая компоненты, у которых у каждого свой модуль, чтобы можно было импортировать их какие нужно и куда нужно в приложении. Вот там и будет лежать app\shared\modules\backendErrorMessages\backendErrorMessages.component.ts:

import ...

export class backendErrorMessagesComponenr implements OnInit {
  @Input('backendErrors') backendErrorsProps!: BackendErrorsInterface;

  errorMessages!: string[];

  ngOnInit(): void {
    this.errorMessages = Object.keys(this.backendErrorsProps).map(
      (name: string) => {
        const messages = this.backendErrorsProps[name].join('');
        return `${name} ${messages}`;
      }
    );
  }
}

Обратите внимание, что @Input здесь написан через алиас, а не как обычно делают. Это зделано для того, чтобы видно было в коде переменную, пришедшую из родителя (благодаря слову Props в названии).

backendErrorMessages.component.html:

<ul class="error-messages">
  <li *ngFor="let errorMessage of errorMessages">
    {{ errorMessage }}
  </li>
</ul>

4) Импортируем модуль backendErrorMessagesModule в auth.module.ts.

12. Создаем сервис, позволяющий клсть и брать (set и get) токен в локалсторедж

Токен авторизации приходит с юзером после регистрации или авторизации. Мы будем его хранить в локалстредже и передавать в заголовках с каждым запросом.

Запись в локалсторедж – это сайд эффект, так как мы работаем не скодом приложения, а с window. Поэтому лучшее место для записи в локалсторедж это файл с эффектами app\auth\store\effects\register.effect.ts, где мы могли бы просто написать вот так:

Простой неправильный способ сохранения токена
Следующая строка под стрелочкой – это правильная альтернатива (создание и подключение отдельного сервиса). Здесь она пока лишняя для этого примера, а понадобиться в пунутк 3

Но как известно, методы setItem и getItem в js работают не так удобно и надежно, как хотелось бы. Ведь при setItem нам нужно объект стрнгифаить, а потом при чтении с помощью getItem нужно парсить его. Кроме того эти методы могут упасть, что приведет к эрору без его обработки.

1) Чтобы обойти эти недостатки мы создадим отдельный сервис persistance.service.ts, в котором будут сет и гет методы для обработки того, что передаем на вход (пока это токен, но сервис с методами будет универсальным). Этот сервис разместив в шередах, чтобы можно было использовать его везде. Создадим в шередах для этого папку services, которой еще не было: app\shared\services\persistance.service.ts

import {Injectable} from '@angular/core';

@Injectable()
export class PersistanceService {
  set(key: string, data: any): void {
    try {
      localStorage.setItem(key, JSON.stringify(data));
    } catch (e) {
      console.error('Error saving to localStorage', e);
    }
  }

  get(key: string): any {
    try {
      return JSON.parse(localStorage.getItem(key) as string);
    } catch (e) {
      console.error('Error getting data from localStoragem', e);
      return null;
    }
  }
}

Как видим мы передаем в set и возвращаем в get any. Это редкое исключение, которое здесь позволено, так чтобы сделать метод универсальным. В get при ошибке мы возвращаем null, чтобы в случае ошибки приложение не падало, а продолжало работать.

Благодаря такому подходу мы получаем еще и обработку ошибок, если не получилось что-то засейвить в локалсторедж или получить из него.

2) Теперь наш сервис мы добавляем в providers модуля, где собираемся использовать (сейчас это app\auth\auth.module.ts).

3) Теперь мы мы можем зайти в наши эффекты register.effect.ts, подключить persistanceService и вызвать метод set для сохранения токена (это видно на скрине в начале раздела).

13. Редирект ползователя на главную после успешной регистрации

Варианты реализации:

1) Мы могли бы сделать подписку на роуты внутри компонента. Но для этого нам надо было бы с эффектов перенести обработку экшена this.action$.pipe(ofType(registerAction)… в сам компонент, чтобы знать когда, произошла регистрация. Никто не запрещает так делать, но это не очень хорошо, так как эффекты более подходящее место для этой логики.

2) Добавлять какое-то поле в наш  редьюсер, например, isSuccessfullySubmiting и потом при саксесе ставить поле тру и реагировать на него в компоненте

3) Но легче и правильнее всего создать еще один эффект в файле с эффектами. Для этого все в том же файле auth\store\effects\register.effect.ts добавдяем redirectAfterSubmit$, который будет отрабатывать на registerSuccessAction. Но здесь при обработке уже будет нужен метод tap вместо switchMap, как в предыдущем эффекте. Причина в том, что здесь нам не надо возвращать никакого экшена в конце, как в предыдущем эффекте при помощи switchMap, а будет просто редирект. Ничего не возвращать позволяет сделать именно функция tap, в которой можно прописать что нужно сделатью без диспатча нового экшена.

//предыдущий эффект register$ 
   )
  );

  redirectAfterSubmit$ = createEffect(
    () =>
      this.actions$.pipe(
        ofType(registerSuccessAction),
        tap(() => {
          console.log('success');
          this.router.navigateByUrl('/');
        })
      ),
    {dispatch: false}
  );

  constructor(
    private actions$: Actions,
    private authService: AuthService,
    private persistanceService: PersistanceService,
    private router: Router
  ) {}
}

Важно не забыть вторым параметром в createEffect передать {dispatch: false}, иначе в браузере будет ошибка (мемори лик), так как будет ожидаться диспатч какого-то экшена.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *