Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I store multiple versions of the same state by ID with @ngrx/store?

I am working on an enterprise application using Angular 5 and @ngrx/store in an Nx workspace.

One of the requirements of our application is to support having multiple tabs open - meaning from our side menu, a user can select multiple features or the same feature and open them in separate tabs within our application. We need to be able to separate the state of these tabs, as the user can open the same type of search in multiple tabs that should be entirely separate. For example, they can open an Account Search tab and search by account ID 123456, and another Account Search tab where they search on 234567. Obviously we don't want the state from one tab to conflict with another.

I am not sure what the best approach to this would be. An example state for our account search sub-feature would be something like

libs/accounts/src/search/+state/search.reducer.ts

export interface AccountSearchState {
  accounts: { [id: string]: Account };
  metadata: Metadata;
  ids: string[];
  loading: boolean;
  query: AccountQuery;
  ...
}

It has a top level feature state above it, like:

libs/accounts/src/+state/index.ts

export interface AccountState {
  search: fromSearch.AccountSearchState
  ...
}

One solution I can think of would be to store the above sub-feature state under a tab ID.

libs/accounts/src/search/+state/search.reducer.ts

export interface AccountSearchState {
  states: { [id: string]: SubState }
}

libs/accounts/src/+state/index.ts

export interface AccountState {
  search: fromSearch.AccountSearchState
  tab: fromTabs.TabState
  ...
}

.. which would require a separate TabState that can be used in all of our feature modules, and would at minimum contain this:

libs/tabs/src/+state/index.ts

export interface TabState {
  selectedId: string;
}

Then we could create selectors which would use both these states:

libs/accounts/src/+state/index.ts

export const getAccountState = createFeatureSelector<AccountState>('account');

export const getTabState = createSelector(getAccountState, (state: AccountState) => state.tab);

export const getAccountSearchState = createSelector(getAccountState, (state: AccountState) => state.search);

export const getAccounts = createSelector(getTabState, getAccountSearchState, (tab, search) => search.states[tab.selectedId].accounts);

When the user clicks on a different tab, we'd have to emit an event that would change the selected tab ID, which would need to be handled by having the tabs reducer in each feature look for the action and update the feature state so that it is aware of the current tab. This means that for all of our features we would need to have this sort of structure - when I would really like to somehow abstract that out. I am just not sure how else to accomplish this. I'd like to not have to store the selected tab id in each feature store as well - instead it would be in the root state, but I'm not entirely sure how I could pass that value to the feature state to return the proper data.

Anyone have any pointers for a better approach to this?

Thanks,

Dan

like image 641
Dan Wnuk Avatar asked Nov 07 '22 13:11

Dan Wnuk


1 Answers

My thought is to have the tab component provide its own slice of the store to its descendants, so that components within the tab can inject that slice without caring whether other tabs exist.

I'm used to working with ngrx/store via ng-app-state (which I wrote), so my example code will use that, but it is just a wrapper around ngrx/store so you should be able to use the same concept without it.

@Injectable()
export class TabStore extends AppStore<TabState> implements OnDestroy {
  constructor(store: Store<any>) {
    super(store, uniqueId('tabState'), new TabState());
  }

  ngOnDestroy() {
    this.delete(); // when the tab is destroyed, clean up the store
  }
}

@Component({providers: [TabStore]})
export class TabComponent {
  constructor(tabStore: TabStore) {
    // tabStore holds state that is unique to me
  }
}

@Component()
export class ChildOfTabComponent {
  constructor(tabStore: TabStore) {
    // I can also access the same store from descendants
  }
}

uniqueId there is just a way to generate a unique root-level key for each tab's state. It is in underscore, lodash, and in my case micro-dash - but any way to generate a unique key per tab is fine.

like image 169
Eric Simonton Avatar answered Nov 14 '22 22:11

Eric Simonton