Как изолировать состояния объектов, когда их можно открыть на нескольких вкладках (на уровне приложения) с помощью ngrx? - PullRequest
0 голосов
/ 22 апреля 2019

Мне нужно создать приложение, используя ngrx (требование) со следующими требованиями:

  • Несколько функций, назовем их A, B и C.
  • Вкладки на уровне приложения. Вкладка содержит функцию, и нет ограничений на количество вкладок, которые могут быть открыты.
  • Функция может быть открыта на любом количестве вкладок по желанию пользователя.
  • Состояние вкладки должно быть изолировано от других вкладок, даже если оно содержит ту же функцию.

Последний пункт - это то, где я застрял, и я серьезно теряю сон из-за этого, и я хотел бы вернуть свою жизнь. У кого-нибудь есть опыт, как это сделать?

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

  • Функция, которая будет содержаться во вкладке, должна иметь состояние (например, FeatureAState)
  • Это состояние будет храниться внутри карты состояний для этой функции (например, FeatureAStates), причем ключ является идентификатором вкладки, которой принадлежит это состояние.
  • Когда навигация по вкладкам завершена, мне нужно прослушать это действие во всех функциях, чтобы обновить их состояние, чтобы быть в курсе текущей вкладки.
  • Затем мы используем селектор, чтобы получить состояние функции для текущей активной вкладки.

Но у меня есть проблемы с решением:

1) Каждая функция должна учитывать тот факт, что они находятся на вкладках, и я должен дублировать selectedTabId в каждой функции (и не забывайте обновлять его каждый раз, когда пользователь перемещается по вкладкам). 2) Все подфункции должны быть обернуты в states: {[tabIdOfThisState: string]: SubFeatureState}, что затрудняет доступ к свойствам.

Как мне улучшить это решение?

Ниже вы можете найти, как выглядит код.

tabs.actions.ts

// Tab is an interface with an id, a matching url and a label.
export const addNewTab = createAction('[Tabs] Add New Tab', props<{ tab: Tab}>());
export const navigateToTab = createAction('[Tabs] Navigate To Tab', props<{ tab: Tab }>());
export const closeTab =  = createAction('[Tabs] Close Tab', props<{ tab: Tab }>());
export const all = union({ addNewTab, navigateToTab });
export type TabsActionsUnion = typeof all;

tabs.reducer.ts:

export interface State extends EntityState<Tab> { 
  selectedTabId: string;
}

export const adapter: EntityAdapter<Tab> = createEntityAdapter<Tab>({
  selectId: (tab: Tab) => tab.linkOrId,
});

export const initialState: State = adapter.getInitialState({ selectedTabId: undefined });

export function reducer(state = initialState, action: TabsActionsUnion) {
  switch (action.type) {
    case addNewTab.type: {
      return adapter.addOne(action.tab, state);
    }
    case navigateToTab.type: {
      return { ...state, selectedTabId: action.tab.id };
    }
    case closeTab.type: {
      // (todo): should handle the case where the tab being removed is the selected one
      return adapter.removeOne(action.tab.id, state);
    }
    default: {
      return state;
    }
  }
}

export const getSelectedTabId = (state: State) => state.selectedTabId;

Тогда у меня есть функция A. Моя функция A обрабатывает несколько подфункций, и в моем примере приложения есть одна под названием Counter. Вот как выглядит код:

counter.actions.ts:

export const increaseCounter = createAction('[Counter] Increase Counter', props<{ tabId }>());
export const decreaseCounter = createAction('[Counter] Decrease Counter');
const all = union({ initializeCounter, increaseCounter, decreaseCounter });
export type CounterActions = typeof all;

counter.reducer.ts

export interface CounterState {
  value: number; 
}

// this is the list of the different states, one per tab where the counter feature is used... 
export interface CounterStates {
  states: { [id: string]: CounterState };
  activeTabId: string; // this is the selected tab id, this is always the same one as the selectedTabId in the Tabs feature 
}

const initialStates = { states: {}, activeTabId: undefined };

const initialState = { value: 0 };

export function reducer(state: CounterStates = initialStates, action: CounterActions | TabsActionsUnion) {
  switch (action.type) {
    // when we add a new tab we need to initialize a new counter state for this tab. 
    // this also means that we cannot lazy load the store, because otherwise 
    // this reducer would not be initialized on the first addNewTab
    case addNewTab.type: {
      return {
        ...state,
        states: { ...state.states, [action.tab.id]: initialState },
        activeTabId: action.tab.id
      };
    }
    // we have to duplicate the activeTabId here because the reducer cannot access the
    // state of another feature...
    case navigateToTab.type: {
      return {
        ...state,
        activeTabId: action.tab.id
      };
    }
    // updating the value is painful because we need to make sure we modify only the right counter state...
    case increaseCounter.type: {
      return {
        ...state,
        states: {
          ...state.states,
          [action.tabId]: { ...state.states[action.tabId], value: state.states[action.tabId].value + 1 }
        }
      };
    }
    case decreaseCounter.type: {
      return {
        ...state,
        states: {
          ...state.states,
          [state.activeTabId]: { ...state.states[state.activeTabId], value: state.states[state.activeTabId].value - 1 }
        }
      };
    }
    default: {
      return state;
    }
  }
}

// selectors are ok to work with, this one I'm happy with
export const getValue = (state: CounterStates) => state.states[state.activeTabId].value;

и редуктор для всей функции, где используется Counter:

index.ts:

export interface FeatureAState {
  counter: fromCounter.CounterStates;
}

export interface State extends fromRoot.State {
  featureA: FeatureAState;
}

export const reducers = combineReducers({ counter: fromCounter.reducer });

export const getFeatureAStateState = createFeatureSelector<State, FeatureAStateState>('featureAState');

export const getFeatureAStateCounterState = createSelector(
  getFeatureAStateState,
  state => state.counter
);

export const getCounterValue = createSelector(
  getFeatureAStateState,
  fromCounter.getValue
);

1 Ответ

0 голосов
/ 27 апреля 2019

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

Я считаю, что имеется селектор для идентификатора активной вкладки (черезURL) является чистым решением 100

...