export interface Action { type: string }
This is what defines an Action.
Ngrx implements a message passing architecture, where Actions are dispatched.
this.store.dispatch(action)
The mechanism behind this is an ActionSubject, which is a BehaviorSubject with some extensions. When you dispatch an action, it is as simple as this.
this.actionsObserver.next(action);
The listeners subscribe to this action stream, either in the reducers which modify the state, or an Effect. This simple structure allows you to build a message passing system which define the data flows in your application.
Here is a list of some things you need to know:
- Actions are defined by { type: string }. The function name of your createAction function does not define the action.
- The string must be unique.
- Every reducer and every Effect sees every Action that is dispatched. This includes forFeature loaded reducers and Effects. Effects and reducers filter out the Action it is listening for.
- The suggested pattern for the string is "[where it came from] what it does".
- Reducers and effects can listen to multiple Actions to do the same thing. So if you dispatch an action that loads your Contacts list from the contacts module and the invoice module, two different Actions can be defined, with 'where it came from' in the string. The reducers and effects accept a list of actions to respond to.
- Actions can have other properties. The command can be accompanied by data.
A well designed message passing system will clearly define the paths of execution and transfer of data. I find the easiest way of seeing this in my applications is to log out the action from a reducer.
export function reducer(state: State | undefined, action: Action) {
console.log(action.type);
return emailReducer(state, action);
}
You will see the flow of commands and data as your application goes through it's function.
Broadly, there are two types of Actions. Those that the reducers listen for to modify the state, and those that don't, and are listened for in Effects. Reducers are pure functions, and when something asynchronous or a side effect needs to occur, the Effect will listen. The typical example goes like this
- Component dispatches an action to load data.
- an Effect listens for the load action, and does an api call to fetch the data.
- the Effect emits an action with the data attached.
- the reducer listens for that action and updates the state.
Message passing systems can easily get out of hand. I think the key is to use a declarative approach. An example of a declarative system that easily translates into actions is a file upload component that I wrote. It has the input to select a file or files, a table to contain the list, and buttons to upload them individually or all of them.
- selected file or files are inserted in the state
- a file or all files can be removed from the state
- a file or list of files are uploaded
- the upload progress is displayed
- success clears the state
- an error sets the status
This is how it is translated into Actions
export const InsertFile = createAction(
'[UploadTableComponent] Insert file',
props<{ selectedfile: SelectedFileList }>()
);
export const RemoveFile = createAction(
'[UploadTableComponent] Remove file',
props<{ selectedfile: SelectedFileList }>()
);
export const SetFileStatus = createAction(
'[SelectedFileList Effect] Set File Status',
props<{ selectedfile: SelectedFileList, status: string }>()
);
export const SetFileProgressStatus = createAction(
'[SelectedFileList Effect] Set File progress Status',
props<{ selectedfile: SelectedFileList, progress: number }>()
);
export const SetFileProgressComplete = createAction(
'[SelectedFileList Effect] Set File progress Complete',
props<{ selectedfile: SelectedFileList }>()
);
export const SelectAllFiles = createAction(
'[UploadTableComponent] Select All',
props<{ id: string, module: string, selectall: boolean }>()
);
export const SelectFiles = createAction(
'[UploadTableComponent] Select File',
props<{ selectedfile: SelectedFileList }>()
);
export const UploadFiles = createAction(
'[UploadTableComponent] Upload File',
props<{ selectedfile: SelectedFileList }>()
);
export const UploadSelected = createAction(
'[UploadTableComponent] Upload Selected',
props<{ selectedfile: SelectedFileList, process: string }>()
);
In this component the file select input dispatches the InsertFile action. Buttons on the file list table can either remove or upload an individual file, or remove or upload the whole list. The httpclient emits actions to update the progress and completion of the upload, or notify of an error.
If you lay out a declarative description of your component, it makes the flow clear and understandable, and it is easy to translate into actions. All that is then required is to define the data that gets passed around. Then when the component gets executed, logging the actions will match the declarative description, or expose flaws in the implementation that can be fixed.
The goal of the NGRX pattern is to make complicated data flows and interactions easy to understand. Self documenting Actions, along with a well thought out declarative spec for the flows and interactions will lead to a successful implementation.