1.1. Модуль регистрации (NgRx)
Начальная структура модуля:
- В папке Store хранится весь NgRx (все файлы, которые к нему относятся)
Файл Store/Actions/actionTypes.ts содкржит енам с названиями type для экшенов, которые используются в данном модуле (пример ниже в разделе «Настраиваем работу NgRx для этого модуля»). - В папке Types находится вся остальная типизация, где каждый интерфейс вынесен в отдельный файл.
- В папке 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.
2) Все реквесты и респонсы стоит тоже покрывать интерфейсами. Создаем теперь уже не в шередах, а локально файл app\auth\types\registerRequest.interface.ts, в котором прописываем интерфейс для полей нашей формы регистрации:
Теперь этот интерфейс можно использовать в нашем ранее созданном экшене для регистрации (app\auth\store\actions\register.action.ts) в пропсах, вместо указания целого объекта (в примере этого файла выше на самом деле этот интерфейс уже указан был, чтобы сразу показать правильный код).
3) Создаем в app\auth\types\authState.interface.ts простой интерфейс с булевым значеием для initialState будущего редьюсера, который будет менять в этом стейте местами true и false, пока идет регистрация и когда она закончилась (т.е. пришел ответ с сервера):
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, где мы могли бы просто написать вот так:
Но как известно, методы 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}, иначе в браузере будет ошибка (мемори лик), так как будет ожидаться диспатч какого-то экшена.