This article series will show how to create an application using Angular 2 or later, and leverage ngrx store for application state. The back-end REST layer will leverage Microsoft Asp.Net MVC 5 / Web API. All of this is backed by a Github repo with all the source code. This project is written with Visual Studio Community as an MVC web application, and uses NPM, Gulp, SystemJS, and NuGet. The “app” angular code folder could be easily copied and used within a Webstorm, VS Code, and other environments without modification. The front end angular code is structured following the angular style guide.
This is a basic ride sharing admin application, as a sample showing how to create a similar app with angular. This application will start from the admin user side of the interface, with possibly a later addition for a slick end-user search interface.
The admin app has a tabbed interface per area of interest with search on the first tab, and any opened record appearing on another tab. Also includes a plus button in the tab row to add a new tab as an item create.
The app consists of planned trips, departure and arrival locations, and people that are driving and have space for others or people that need to catch a ride to a destination. This is primarily intended of for one off trips, such as a college student coming home over a weekend. Repeat ride-sharing such as for commuting is not the focus; A student may have the same destination each time but on an irregular schedule.
In the end this is meant as a sample application and may be rough around some corners for the sake of keeping the sample less complicated.
Managing state or data in an application for me starts at defining the data structures needed to support the requirements. I think of the application state to be similar in concept to a database; there is one top-level application state that equates to a database, and multiple application area states that each equate to a table. Your application state won’t match up 100% with the data structures you use for your back-end code, because the front end also needs state for things like actively selected items in a list and other things that should be remembered as you navigate around your application and back again.
Define state for each top-level component that deals with data or remembering actions. Local component variables are fine for small things, but if it needs to be remembered as you route between components in your application and come back, then ngrx store is the way to do it.
Design your state incrementally. Start with an interface containing a property for your main data element(s) and you can always add more over time. For instance in a component that has search capabilities, you may have a property containing the search results as an array where each result likely is what is returned by your REST service or is a display-ready mapping from that source data. In our example, the REST API returns data display-ready, so the search results match the API search result.
Start with the necessary imports including ngrx store, and your models of the data related to the component that will be stored in state. This code also imports a type-cache to prevent naming collisions with other states in the application.
import { Action } from '@ngrx/store'; import { tassign } from 'tassign'; //improvement over Object.assign import { Person, PersonHolder, PersonCriteria } from './person.model'; import { Tab } from '../app-shared/tabset/tab.model'; import { type } from '../app-shared/type-cache';
Define your state container as an interface. In this case we need a search criteria storing what was most recently searched for, the results from that search, a list of open items from any of the search results, and a few boolean flags used to display various sections of the component.
export interface PersonState { criteria: PersonCriteria, isSearching: boolean, results: Person[], hasResults: boolean, openList: PersonHolder[], activeTabId: string, nextNewId: number }
This next step of defining your actions with constants is purely optional, but it does get rid of fragile ‘magic’ strings in your application store, and in this case it uses the ‘type’ function from a custom type-cache to ensure multiple states don’t define colliding state action names. A duplicate state action name would result in both actions being run when only one of them was meant to, in other words it’s not good for your sanity.
export const PersonActionTypes = { SEARCH: type('[Person] Search'), SEARCH_COMPLETE: type('[Person] Search Complete'), OPEN: type('[Person] Open'), TAB_ACTIVATE: type('[Person] Tab Activate'), CLOSE: type('[Person] Close'), INSERT_COMPLETE: type('[Person] Insert Complete'), UPDATE_COMPLETE: type('[Person] Update Complete') }
By default ngrx only requires you to define your reducer(s) as a function accepting two arguments, a state object and an action with a name and an ‘any’ payload type. This can be quite confusing when invoking a state change when you don’t remember what type a given action requires. To remedy this, you can define your actions as objects to give yourself some type checking by the typescript compiler. You will also need to define a ‘union type‘ (ie. PersonActions) that combines all of these under one definition to use in your reducer.
export class PersonSearchAction implements Action { type = PersonActionTypes.SEARCH; constructor(public payload: PersonCriteria) { } } export class PersonSearchCompleteAction implements Action { type = PersonActionTypes.SEARCH_COMPLETE; constructor(public payload: Person[]) { } } ... other actions omitted here ... export type PersonActions = PersonSearchAction | PersonSearchCompleteAction; //others omitted for clarity
The next all important step, and required by ngrx is to define your reducer function (not a class). The first time it is called ngrx passes in an empty state. You need to define an initial state populated how you want it.
Notice how the PersonReducer takes an action of type PersonActions which is defined as a union type combining all your action class types. Essentially this means the action can be any one of those types.
const initialState: PersonState = { criteria: null, isSearching: false, results: [], hasResults: false, openList: [], activeTabId: '', nextNewId: 0 }; export function PersonReducer(state = initialState, action: PersonActions) : PersonState { switch (action.type) { case PersonActionTypes.SEARCH: return tassign(state, { criteria: action.payload, isSearching: true, results: null, hasResults: false }); case PersonActionTypes.SEARCH_COMPLETE: return tassign(state, { isSearching: false, results: <Person[]>action.payload, hasResults: action.payload && (<Person[]>action.payload).length > 0 }); default: return state; } }
It is very important that your reducer function does not change the state object passed into the function, but does instead return a new instance of state to maintain the undo capability of ngrx. Normally you would do this with “object.assign”; This code uses a helper function tassign instead which does an object.asssign under the covers. The benefit to tassign is type checking, now the compiler knows that the return value of tassign must be the same type as the first parameter to tassign, which ensures that you only attempt to set values on that state defined by your state interface.
You can find the other person actions and reducer logic of the person.store.ts within the source of the git repository.
Your top-level application state is composed of an interface to define the structure for each component, a constant to define the ‘reducers’ or access to those structures, and a top-level application reducer registered with ngrx to provide access to it all.
At this point you will have created one or more of your component states and the next step is to wrap them into a single store definition to hand over to ngrx store. Create a typescript file next to your app module called app.store.ts. Start the file by importing ngrx store and the sub-states you need from other files.
import { ActionReducer } from '@ngrx/store'; import { combineReducers } from '@ngrx/store'; import * as person from './person/person.store'; import * as location from './location/location.store'; import * as trip from './trip/trip.store';
Your application state (database) is defined as an interface with a property for each sub-state (table).
export interface AppState { person: person.PersonState, location: location.LocationState, trip: trip.TripState }
Reducers are the means to modify the state as actions are taken by the user. Here again we are just grouping the reducers defined in other files into one object to be given to ngrx.
const reducers = { person: person.PersonReducer, location: location.LocationReducer, trip: trip.TripReducer }
In the end, ngrx wants a single reducer for your application to accept all state modifications, and it provides the combineReducers function to take them all and create the single application state reducer.
export function AppReducer(state: any, action: any) { return combineReducers(reducers); }
Now to use your app reducer we need to expose it in the app.module.ts to be pulled in by other components that need to use or modify the state. In your module imports section, add the ngrx store module and give it the app reducer.
@NgModule({ imports: [ ... omitted StoreModule.provideStore(AppReducer), ], declarations: [ AppComponent ], bootstrap: [AppComponent] }) export class AppModule { }
In this article we learned how to define your application state for ngrx store and import it into your application. If you were migrating an existing app to use ngrx while reading this, the old app should still run at this point, bootstrapping ngrx but not yet using it.
The next article in this series will show how to use this new store in your components. Remember the full source is already complete and you can skip ahead to browsing the full working source code. The next article(s) will explain how the code in the repository works.
Full Source code for this article series
https://github.com/michael-lang/sample-ng2-mvc
Build a Better Angular 2 application with Redux and ngrx (Lukas Ruebbelke)
http://onehungrymind.com/build-better-angular-2-application-redux-ngrx/
Managing State in Angular2 Applications (Kyle Cordes)
https://www.youtube.com/watch?v=eBLTz8QRg4Q
Advanced typescript types:
https://www.typescriptlang.org/docs/handbook/advanced-types.html
Tagged Union Types (Marius Schulz)
https://blog.mariusschulz.com/2016/11/03/typescript-2-0-tagged-union-types
Angular Official Style Guide
https://angular.io/styleguide
How to build a real world Angular app with ngrx store, Part II: Components using Store
7 Comments