В Visual Studio Code по умолчанию стоит настройка, которая отображает на владке Explorer вложенные папки таким образом:

VSC - Default View

Не знаю, чем руководствовались разработчики VSC, когда устанавливали такую настройку, но мне она кажется крайне неудачной и неудобной. Я предпочитаю “классическое” отображение вложенности папок в файловой системе.

В VSC практически все поддается настройкам и этот момент также легко исправить. В Settings редактора переводим отображение настроек в режим JSON и добавляем строку:

"explorer.compactFolders: false

… вот так:

VSC - Custom View Settings

… и тогда вложенные папки принимают удобочитаемый вид:

VSC - Custom View


Для управления конкурентностью при общении с api.

mergeMap

… будет триггерить все запросы, конкурентно создавая гонку.

exhaustMap

… будет игнорировать все запросы, пока не завершится активный запросы.

concatMap

… будет создавать очередь запросов, где будет только один запрос активный, а остальные запросы будут ждать своей очереди.

switchMap

… отменит предыдущий запрос, гарантируя один активный запрос.

Наиболее популярные кейсы на практике для их использования

В кейсе формы создания какой-то сущности с последующим редиректом, exhaustMap, чтобы не отправлять тысячу запросов, если юзер будет кликать по кнопке, а она не дизейблится

В кейсе, когда у нас есть поиск и нам необходимо отправлять запрос на бэк, лучше подойдет switchMap, так как он будет отменять предыдущий запрос, если он не успел завершится и отправлять новый, с актуальными данными

Если мы патчим какую-то сущность, тогда в большинстве случаев лучше будет concatMap, чтобы дожидаться когда бэк пропатчит данные в первый раз, а потом отправлять второй запрос, чтобы сохранять консистентность данных (конечно это больше задача бэкенда, но иногда легче это делать на фронте, что не очень Юзер Френдли конечно)

В случаях где порядок выполнения запросов не важен, можно использовать mergeMap, он никого не ждет и с каждым новым значением в основном потоке, открывает новую подписку, но в данном случае сложнее следить за текущим состоянием (например loading)

Разбор примера

export const productsLoadEffect = createEffect(
  (actions$ = inject(Actions), productsService = inject(ProductsService)) =>
    actions$.pipe(
      ofType(ProductsActions.loadProducts),
      exhaustMap(() =>
        productsService.getAll().pipe(
          map((products) =>
            ProductsAPIActions.productsLoadedSuccess({ products }),
          ),
          catchError((error: Error) =>
            of(
              ProductsAPIActions.productsLoadedFail({ message: error.message }),
            ),
          ),
        ),
      ),
    ),
  { functional: true },
);

export const productsAddEffect = createEffect(
  (actions$ = inject(Actions), productsService = inject(ProductsService)) =>
    actions$.pipe(
      ofType(ProductsActions.addProduct),
      mergeMap(({ product }) =>
        productsService.add(product).pipe(
          map((product) => ProductsAPIActions.addProductSuccess({ product })),
          catchError((error: Error) =>
            of(ProductsAPIActions.addProductFail({ message: error.message })),
          ),
        ),
      ),
    ),
  { functional: true },
);

export const productsUpdateEffect = createEffect(
  (actions$ = inject(Actions), productsService = inject(ProductsService)) =>
    actions$.pipe(
      ofType(ProductsActions.updateProduct),
      concatMap(({ product }) =>
        productsService.update(product).pipe(
          map((product) =>
            ProductsAPIActions.updateProductSuccess({ product }),
          ),
          catchError((error: Error) =>
            of(
              ProductsAPIActions.updateProductFail({ message: error.message }),
            ),
          ),
        ),
      ),
    ),
  { functional: true },
);

export const productsRemoveEffect = createEffect(
  (actions$ = inject(Actions), productsService = inject(ProductsService)) =>
    actions$.pipe(
      ofType(ProductsActions.removeProduct),
      mergeMap(({ id }) =>
        productsService.delete(id).pipe(
          map((product) => ProductsAPIActions.removeProductSuccess({ id })),
          catchError((error: Error) =>
            of(
              ProductsAPIActions.removeProductFail({ message: error.message }),
            ),
          ),
        ),
      ),
    ),
  { functional: true },
);
  1. так как нет никаких параметров (фильтры, сортировка, пагинация), то нет смысла делать несколько запросов для загрузки одних и тех же данных
  2. Добавление новых продуктов не требует жесткого порядка, нам без разницы какой запрос на добавление нового продукта выполнится раньше, первый или второй, главное чтобы они добавились, поэтому mergeMap
  3. Для сохранение консистентности данных, для апдейта продукта используется concatMap, на самом деле реализация здесь не очень хорошая, так как concatMap будет использоваться для всех продуктов, а не для единственного, но если мы будем обновлять один и тот же продукт, существует вероятность, что второй запрос обработается раньше и данные из первого запроса это перетрут (так как на сервере конкурентные потоки и если ими плохо управляют, то такая ситуация вполне возможна), соответственно понимаем, что порядок в данном случае важен, поэтому используется concatMap
  4. Ну как и во втором пункте, при удалении продуктов, нам не важно в каком порядке выполнятся запросы на удаление продуктов, поэтому можем пускать их в параллель, поэтому mergeMap

Если бы в первом случае была динамическая фильтрация или сортировка, то switchMap был бы лучшим решением Если бы была пагинация в виде lazy loading, то ее обычно выносят в отдельный эффект и используют concatMap так как в данном случае порядок тоже важен (в случае с сортировкой)

Ссылки по теме

Краткое описание метода

На вход оператора передается список потоков, он на них всех подписывается получает по последнему значению; потом берет значение из родительского потока и соединяет это все в один поток.

Практическое применение

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

Пример схематичный:

hello$.pipe(
  withLatestFrom(this.toggleService.isEnabled('some-feature'))
)

… то есть - в данном случае, если значение пришло из родительского потока hello$ - оператор withLatestFrom заберет значение из метода сервиса isEnabled и вернет - поток из этих двух значений.

Пример реальный, из эффекта NgRx:

export const saveCounter = createEffect(
  () =>
    inject(Actions).pipe(
      ofType(CounterActions.incrementCounter, CounterActions.decrementCounter),
      withLatestFrom(inject(Store).select(countSelect)),
      tap(([payload, count]) => {
        localStorage.setItem(ECounterStorage.CounterKey, count.toString());
      })
    ),
  { dispatch: false, functional: true }
);

… здесь запись ofType(CounterActions.incrementCounter, CounterActions.decrementCounter), трактуется как ИЛИ; то есть, оператор ofType будет фильтровать поток actions$ на экшен incrementCounter или экшен decrementCounter.

Оператор withLatestFrom здесь также - подписан и слушает родительский поток actions$. Если фильтр ofType сработает - из потока actions$ прийдет евент в оператор withLatestFrom, поэтому в свою очередь оператор withLatestFrom заберет значение из потока countSelect, объединит оба значения и вернет новый поток - в евентом, содержащим оба эти значения - ([payload, count]).

Полезные ссылки

Генерация (добавление) environment в Angular 17:

ng generate environments

CREATE src/environments/environment.ts (31 bytes)
CREATE src/environments/environment.development.ts (31 bytes)
UPDATE angular.json (3799 bytes)

Настроить в Angular ChangeDetectionStrategy.OnPush в качестве стратегии по умолчанию:

ng config schematics.@schematics/angular.component.changeDetection OnPush