How to use Redux in Angular Applications
Redux, now in version 3, has been around less than a year but it has proved very successful. Inspired by Flux and Elm, is used to handle Application state and bind it to the User Interface in a very effective way. Redux also enables cool features like hot reloading or time travel with little effort. Redux is usually seen with React but it can be used separately.
Redux builds on top of Flux concepts although previous experience is not mandatory.
In this article, we are going to explore a Todo List example ported from React, from the recent Redux video course by Dan Abramov. Use the links below to hack the final solution:
Find the latest Angular content following my feed at @gerardsans.
Introduction to Redux
Redux follows three basic principles:
- Single Immutable State Tree
- Uni-directional data-flow
- Changes are made using pure functions (Reducers)
By following these principles We can achieve a predictable and reproducible Application behaviour.
Let’s review the responsibilities for each component on the diagram below.
Action Creators are components containing helper methods that create specific Actions to be dispatched and run by Reducers.
Reducers specify how the state changes in response to Actions. All Reducers must be pure functions meaning:
- they produce the same output given the same input
- they don’t produce side-effects (Eg: mutate state, make calls to backend)
Reducers always create a new state to avoid side-effects; a more advanced option is to use a library like immutable.js.
Reducers can also be composed with other reducers as required with combineReducers. See a basic rootReducer below.
It’s a common practice to define the initialState as a default parameter (line 1) and handle each action with a switch statement.
The Application Store is central to Redux and offers an API to:
- dispatch actions by appStore.dispatch(action)
- register listeners for change notification: appStore.subscribe(callback)
- read the Application State: appStore.getState()
Todo List example
We are going to explore a Todo List Application to learn how we can integrate Redux with Angular. This is a basic implementation where we can add new todos, mark them as completed and filter them.
In Angular we start designing our applications using a component tree and starting from the root component. Find below a schematic pseudo-HTML including all the UI components: add-todo, todo-list (child components: todo), filters (child components: filter-link).
See below the code to setup Redux:
Angular Applications are bootstrapped passing in the Application Module. The Application Module declares all required components, directives and pipes. This includes the root component App, and all the rest grouped in APP_DECLARATIONS. Global dependencies are defined in providers so they are available to our Redux Components. See appStore and TodoActions (lines 15–16). TodoActions (class) will act as an ActionCreator with a public method for each action. We imported all dependencies (lines 2–5), then instantiated appStore (line 7) using createStore and passing the rootReducer (function). Finally we used bootstrap with our root module AppModule (line 21).
You can read more about how ngModules work at Angular Modules (angular.io)
Note that when using a string token we have to prepend @Inject(‘AppStore’) on our components.
The Application Store (appStore) will hold the Application State. This is: the todos Array and the current filter. We will define the initial state as follows:
In the next section, we will define the structure for a todo item. This core structure will remain unchanged during the life of the Application.
Adding a New Todo
Let’s see a simplified version of the AddTodo component that will allow us to add a new todo and take care of user input.
In the template (lines 4–8), we are using a local template variable #todo (input HTML element, line 6) and passing its reference on the button click event (line 7). On the constructor, we injected appStore and todoActions into the component (lines 11–17) as private properties. When the user enters a description and clicks on ‘Add Todo’ this will dispatch an action (line 20) like the one below and clear the input content.
To avoid manually creating action objects in our components we created the TodoActions class as an ActionCreator.
We expose the ADD_TODO token as an action identifier (line 2). Note how we extended the action object to include the information we require to identify todos and flag them as completed or not (lines 12–15).
After dispatching the action the rootReducer will be called by the store passing the currentState (initialState if undefined) and the user action.
In order to create the new state we are using concat (creating a new Array) and maintaining the current filter, initially it shows all todos.
Toggling a Todo
For each todo the user can toggle it as completed clicking over its description. Below you can see a simplified mark-up for an active todo:
Similar to what we did with add todo, each click event will pass down the todo id (input attribute, line 6) and dispatch the corresponding action (line 17).
TypeScript tip: using private or public modifiers in the constructor arguments is a shortcut for declaring private or public properties (lines 12–13). See private/public modifiers.
Toggling the initial example todo would produce the following action:
As before, dispatching the action will execute the reducer and create a new state.
The helper function toggleTodo creates a new array toggling the todo matching the action.id being dispatched and maintaining the rest.
The Filters component allows the user to filter: all, only active or only completed todos. We use FilterLink components to encapsulate each filter passing an identifier through the attribute filter.
Within FilterLink each click event passes down the filter (input attribute, line 6) and dispatch the corresponding filter action.
Filtering by Completed will generate the following action
As before, dispatching the action will execute the reducer and create a new state. In this case, we keep the same todos and change the current filter with the one dispatched (lines 5).
Displaying the Todo List
We will use a child component todo to encapsulate a single todo passing some properties as attributes (id, completed) and the description (text) as content. This pattern is known as Container Component.
Following see an extract of the TodoList component.
Above, we registered a listener using appStore.subscribe (line 7). Once within our listener, we can easily read the current state using appStore.getState (line 8). Subscribe returns a function that we can use to unsubscribe. In Angular we use the OnDestroy event handler for clean up (lines 2, 13–16).
Note how we kept all component properties and helper methods as private. We don’t want other components accessing them.
Redux life-cycle review
Let’s review how a Redux Application behaves at different stages.
- On Application bootstrap: we initialise the appStore passing the rootReducer. This will trigger appStore internal initialisation. Usually this sets the initialState.
- On Component creation: We inject appStore and TodoActions on the constructor as required. Components that display data subscribe to the appStore and read it by calling appStore.getState(). Components that mutate the state prepare dispatch code for the corresponding action passing any required data.
- On Component destruction: Components that display data unsubscribe to the appStore to clean up resources.
- On User interactions: each user interaction will trigger an underlaying dispatch action. This will execute the rootReducer producing a new state. The appStore will then notify all subscribed listeners that will update accordingly.
- On Server-side initiated actions: some Applications can dispatch actions in response to server-side initiated events. Eg: WebSockets. These actions once properly setup follow the same flow as User interactions.
We covered how to build a basic Angular Application using Redux. Hope you are now curious about Redux and maybe use it in your next project. Thanks for reading!