NgRx Workshop: Part 8 - Folder Structure

Published on

Following the LIFT principle:

  • Locating our code is easy
  • Identify code at a glance
  • Flat file structure for as long as possible
  • Try to stay DRY - don’t repeat yourself

Key Takeaways

  • Put state in a shared place separate from features
  • Effects, components, and actions belong to features
  • Some effects can be shared
  • Reducers reach into modules’ action barrels


Folder structure followed in the workshop

├─ books\
│     actions\
│         books-api.actions.ts
│         books-page.actions.ts
│         index.ts                  // Includes creating names for the exports
│      books-api.effects.ts
├─ shared\
│       state\
│          {feature}.reducer.ts     // Includes state interface, initial interface, reducers and local selectors
│          index.ts
  • The index file in the actions folder was using action barrels like the following:
import * as BooksPageActions from './books-page.actions'
import * as BooksApiActions from './books-api.actions'

export { BooksPageActions, BooksApiActions }
  • This made easier and more readable at the time of importing it:
import { BooksPageActions } from "app/modules/book-collection/actions";


Folder structure followed in example app from @ngrx

├─ books\
│     actions\
│         books-api.actions.ts
│         books-page.actions.ts
│         index.ts                  // Includes creating names for the exports
│     effects\
|         books.effects.spec.ts
|         books.effects.ts
|     models\
|         books.ts
│     reducers\
|         books.reducer.spec.ts
|         books.reducer.ts
|         collection.reducer.ts
|         index.ts
├─ reducers\
│        index.ts  /// Defines the root state and reducers
  • The index file under the reducers folder is in charge of setting up the reducer and state
import * as fromSearch from '@example-app/books/reducers/search.reducer'
import * as fromBooks from '@example-app/books/reducers/books.reducer'
import * as fromCollection from '@example-app/books/reducers/collection.reducer'
import * as fromRoot from '@example-app/reducers'

export const booksFeatureKey = 'books'

export interface BooksState {
  [fromSearch.searchFeatureKey]: fromSearch.State
  [fromBooks.booksFeatureKey]: fromBooks.State
  [fromCollection.collectionFeatureKey]: fromCollection.State
}

export interface State extends fromRoot.State {
  [booksFeatureKey]: BooksState
}

/** Provide reducer in AoT-compilation happy way */
export function reducers(state: BooksState | undefined, action: Action) {
  return combineReducers({
    [fromSearch.searchFeatureKey]: fromSearch.reducer,
    [fromBooks.booksFeatureKey]: fromBooks.reducer,
    [fromCollection.collectionFeatureKey]: fromCollection.reducer,
  })(state, action)
}
  • The index file under app/reducers/index.ts defines the meta-reducers, root state, and reducers
/**
 * Our state is composed of a map of action reducer functions.
 * These reducer functions are called with each dispatched action
 * and the current or initial state and return a new immutable state.
 */
export const ROOT_REDUCERS = new InjectionToken<ActionReducerMap<State, Action>>(
  'Root reducers token',
  {
    factory: () => ({
      [fromLayout.layoutFeatureKey]: fromLayout.reducer,
      router: fromRouter.routerReducer,
    }),
  }
)

Personally, I like how the example-app is organized. One of the things that I will add is to have all the folders related to ngrx in a single folder:

├─ books\
│     store\
│        actions\
│            books-api.actions.ts
│            books-page.actions.ts
│            index.ts                  // Includes creating names for the exports
│        effects\
|            books.effects.spec.ts
|            books.effects.ts
|        models\
|            books.ts
│        reducers\
|            books.reducer.spec.ts
|            books.reducer.ts
|            collection.reducer.ts
|            index.ts
├─ reducers\
│        index.ts  /// Defines the root state and reducers