Рубрики
Без рубрики

Где инициировать нагрузку данных в NGRX

Обзор стратегий для инициирования нагрузки данных в угловом приложении NGRX. Tagged с помощью углового, ngrx, javascript.

В NGRX загрузка данных из источника данных, такого как API REST или DB, выполняется с использованием эффекта. Тем не менее, что -то должно отправить действие, которое в первую очередь запускает эффект. Я видел несколько разных предложений/подходов к этому.

В нашем примере мы будем загружать коллекцию Заказ субъекты из услуги. Мы представим два действия: LoaterdersRected и Западчики Анкет Первый из них инициирует нагрузку данных, а затем эффект выполнит нагрузку и отправляет A Западчики действие, которое поместит загруженные данные в магазин.

Эффект для обработки LoaterdersRected будет выглядеть так:

@Effect()
loadOrdersRequested$ = this.actions$.pipe(
  ofType(ActionTypes.LoadOrdersRequested),
  // Don't load if we've already loaded.
  withLatestFrom(this.store.select(getAllOrdersLoaded)),
  filter(([_, loaded]) => !loaded),
  // Don't handle more than one load request at a time.
  exhaustMap(() => this.ordersService.fetchAllOrders().pipe(
    map(result => new LoadOrders(result))
  ))
);

Теперь, чтобы инициировать нагрузку данных, нам нужно отправить LoaterdersRected действие откуда -то. Есть четыре основных варианта:

  1. Когда начинается приложение.
  2. Когда инициализируется компонент контейнера.
  3. Когда приложение перемещается по маршруту.
  4. Когда пользователь выполняет действие.

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

Когда начинается приложение

Плюсы:

  • Данные гарантированно загружаются.

Минусы:

  • Память/производительность касается, если есть много данных для загрузки.

В вашем AppComponent

Самый простой способ – отправить LoaterdersRected Действие от вашего AppComponent SINIC LIFECYCLE:

export class AppComponent implements OnInit  {
  constructor(private store: Store) {}

  ngOnInit() {
    this.store.dispatch(new LoadOrdersRequested());
  }
}

В эффекте

Ngrx предоставляет Init действие, которое отправляется, когда приложение запускается. Это кажется хорошим местом, чтобы инициировать нашу нагрузку данных, но есть проблема. Init Действие отправляется до того, как последствия будут подписаны, так что это не сработает:

@Effect()
init$ = this.actions$.pipe(
  ofType(INIT),
  map(() => new LoadOrdersRequested())
);

Вместо этого команда NGRX рекомендовал используя отсрочка Вместо этого оператор RXJS:

@Effect()
init$ = defer(() => new LoadOrdersRequested());

Однако, если мы хотим иметь наш эффект потенциально вызвать Другое Эффекты, этот подход не будет работать. Это потому, что, пока отсрочка Задерживает создание LoaterdersRected действие до init $ Наблюдаемое подписано на (во время инициализации модуля эффектов) действие будет отправлено до Инициализация завершена. Таким образом, наш эффект, который ищет LoaterdersRected не может быть зарегистрировано, в зависимости от порядка, в котором система эффектов подписалась на различные эффекты.

Возможно, мы можем смягчить эту проблему, переупорядочив эффекты, но лучшим решением является использование Asyncscheduler задержать отправку LoaterdersRected действие:

import { asyncScheduler, of } from 'rxjs';

...

@Effect()
$init = of(new LoadOrdersRequested, asyncScheduler);

Хотя Init Не работает, есть также встроенное действие, которое мы может Использование: Root_effects_init :

@Effect()
$init = this.actions$.pipe(
  ofType(ROOT_EFFECTS_INIT),
  map(() => new LoadOrdersRequested())
);

App_initializer

Угловой обеспечивает App_initializer В качестве способа запуска кода при запуске приложения, и вы можете отправить действия оттуда:

@NgModule({
  ...
  providers: [
    {
      provide: APP_INITIALIZER,
      useFactory: (store: Store) => {
        return () => {
          store.dispatch(new LoadOrdersRequested());
        };
      },
      multi: true,
      deps: [Store]
    }
  ]
})

Когда инициализируется компонент контейнера

Плюсы:

  • Вы загружаете только данные как и когда это необходимо.
  • Понятно из того, на каких данных он полагается.

Минусы:

  • Вам либо нужно много действий, либо отправить одно и то же действие в нескольких местах.
  • Компонент менее чистый, так как он имеет побочный эффект загрузки данных.
  • Вы можете забыть отправить действие из компонента, которому нужны данные. Эта ошибка может быть скрыта, если вы обычно достигаете компонента через другой компонент, который делает Инициировать нагрузку данных. Например. Обычно вы открываете страницу списка перед открытием страницы сведений. Затем, однажды, вы перемещаетесь прямо на страницу деталей и ломаются.
@Component({ ... })
export class OrdersComponent implements OnInit {
  order$: Observable;

  constructor(private store: Store) {
    this.order$ = this.store.select(getOrder);
  }

  ngOnInit() {
    this.store.dispatch(new LoadOrdersRequested());
  }
}

Когда приложение перемещается по маршруту

Плюсы:

  • Меньше дублирования. Один охранник в корне иерархии маршрута может загружать данные для всех дочерних маршрутов, даже если они перемещаются напрямую.
  • Компоненты более чисты, так как они отображают только от выбранного состояния до своего шаблона.

Минусы:

  • Довольно тупой: охранник запустит нагрузку данных для любой Дочерний маршрут, даже если его компонент не нужен.
  • Менее очевидно, если смотреть на компонент, какие данные ему нужно работать. Если он будет перемещен где -то еще в иерархии маршрутизатора, это сломается.
  • Менее полезны, если маршруты, требующие некоторых конкретных данных, распределены по всей иерархии маршрутизатора, так как вам нужно включить охранник в разные места.

Маршрутизатор

@Injectable()
export class OrdersGuard implements CanActivate {
  constructor(private store: Store) {}

  canActivate(): Observable {
    return this.store.pipe(
      select(getAllOrdersLoaded),
      tap(loaded => {
        if (!loaded) {
          this.store.dispatch(new LoadOrdersRequested());
        }
      }),
      filter(loaded => loaded),
      first()
    );
  }
}


const ROUTES: Route[] = [
  {
    path: 'orders',
    component: OrdersList,
    canActivate: [OrdersGuard],
    children: [
      ...
    ]
  }
]

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

Router Resolver

@Injectable()
export class OrdersResolver implements Resolve {
  constructor(private store: Store) { }

  resolve(): Observable {
    return this.store.pipe(
      select(allDatasetsLoaded),
      tap(loaded => {
        if (!loaded) {
          this.store.dispatch(new AllDatasetsRequested());
        }
      }),
      filter(loaded => loaded),
      first()
    );
  }
}

https://stackblitz.com/edit/angular-ngrx-initiate-load-router-resolve

Использование резолюра работает очень аналогично охраннику. Основное отличие состоит в том, что решимость выполняется при несколько иных обстоятельствах для охранника, и, как предполагается, возвращает объект, который будет объединен в данные активированного маршрута. Тем не менее, мы не должны этого делать, поскольку компоненты должны получать данные из магазина, не Из активированного маршрута. Таким образом, решимость должна просто вернуть логическое.

Эффект действия маршрутизатора

@Effect()
loadOrders$ = this.actions$.pipe(
  ofType(ROUTER_NAVIGATION),
  withLatestFrom(this.store.select(allOrdersLoaded)),
  filter(([action, loaded]) => 
    action.payload.routerState.url.includes('/orders') && !loaded
  ),
  map(() => new LoadOrdersRequested())
);

Плюсы:

  • Держит вещи в NGRX, поэтому чувствует себя более идиоматическим.

Минусы:

  • Требуется проверить, соответствует ли новый маршрут, а не полагаться на сам маршрутизатор, чтобы сделать это, как это делает охранник. Это может привести к загрузке ошибок, если кто -то изменит путь в вашей конфигурации маршрутизатора, но забывает сделать это в ваших последствиях.

Отправить действие из проверки внутри селектора

export function getAllOrders(store: Store) { 
  return createSelector(
    getOrders,
    state => {
      if (!state.allOrdersLoaded) {
        store.dispatch(new LoadOrdersRequested());
      }

      return state.orders;
    }
  );
}

Я на самом деле не видел этого в дикой природе, но это подход, который пришел для меня.

Плюсы:

  • Гарантирует загрузить данные в то время и только тогда, когда их запрашивали для использования.

Минусы:

  • Нарушает принцип, что селекторы должны быть чистыми функциями.
  • Если вы не строго относитесь к повторному использованию и объединению своих селекторов, вы можете получить некоторые, которые запускают нагрузку, а некоторые – нет, потому что они полагают селектор, который вызывает нагрузку.

Будущие возможности

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

Вывод

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

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

Однако, если у вас есть большое приложение, разделенное на разные функции, причем каждая функция должна загружать данные из отдельного источника, лучше всего использовать охрану в корне иерархии маршрута каждой функции для отправки нагрузочного действия для его требований Анкет

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

Оригинал: “https://dev.to/jonrimmer/where-to-initiate-data-load-in-ngrx-358l”