NgRx is a framework for managing application state. Without it, our apps would typically:
- manage app state via (stateful) services;
- services would be accessed from various components;
- there would be no control over state mutations.
NgRx is an Angular version of Redux - React’s state management framework. It makes use of RxJS making the state observable. It makes our components less dependent on services, and it makes services simpler and stateless.
(Some) Building Blocks
NgRx exposes three main types of entities:
There are also effects, but they come as part of separate package.
There is one store, but there are many actions and reducers:
- each entity type will have its own reducer (e.g. Products or Users will have their own reducers)
- each entity type will have 1 or more actions (e.g. Product will have AddProduct, UpdateProduct, while Users will have AddUser, RemoveUser, etc.)
- each feature section of our app will have its own
storedirectory where the reducer and actions files will be kept
Actions are the atomic operations that we want to apply on our state. They
represent the interactions of our users with the data kept in the store. Things
such as adding, modifying, deleting data from our state is exposed via actions.
An action can (optionally) carry some data. Additionally, each action has its
type. That type is used to inform a reducer about the kind of action that
we want to execute. Action can be viewed as a kind of event.
Since actions are typically small, it’s OK to keep them all in one file (per feature). Here’s an example:
Alternatively, we can use this syntax:
The Legacy Syntax works as well.
Actions are dispatched, as we will see a bit later. Actions are a bit
similar to DTO objects. Some actor wants to do something with the store, so it
creates a new
Action instance and dispatches it to the store (to be handled by
Reducers handle actions in the context of the store. A single reducer will handle all the actions of a specific entity kind (like a Product). The store in our app is built with the help of reducers!
Reducer is a pure function that is invoked anytime some action is dispatched (and at the bootstrap of our app!). It receives two parameters:
- the current state (or an initial state if we set it up this way)
- the action (with its optional payload)
And, it returns the new state.
It’s important to mention that reducers are very limited in what they actually do. They should first of all take care of the store. They should not interact with the ourside world. Things such as network requests, accessing storage, etc.
- these should be left to effects.
We probably are going to have a few kinds of data and a few reducers. It makes sense to define a global type that defines all kinds of data being stored:
With such a type, it’s easier to inject the store into classes. Since the
AppState type is global within our app, it makes sense to put it in its own
app.reducer.ts, in the
In the same file, we can also put the listing of all the reducers within our app:
It will come useful very quickly.
Using the Store
First of all, our store needs to be imported in some module:
This is the place where we inform the framework of any reducers that we have defined. In the end, the reducers build up the store in the app.
Our components or services may inject the
Store in order to access
Store<> is a generic type. Thanks to Store Type being
defined as an interface, we can quickly specify our store.
We can dispatch actions like this:
That dispatch will go through NgRx’s internals, which will invoke our reducer.
Based on the action (
AddProduct) it will update the store with the new
Store’s data is exposed via
dispatch that “modifies” the store, would bring data via the subscription.
(@ngrx/effects) is an additional package for NgRx that adds Side Effects. Until now we had some actions defined, and reducers that acted on them. Reducers focused on state “modifications”. They should not contain any logic other than that.
Our state actions are often associated with some additional logic that we need to execute - sending some network request, storing some data in local storage, etc. Reducers shouldn’t execute these, because they are pure functions. However, effects can do all those things.
Each feature in our app can have its own
store/*.effects.ts file. Here’s an
Effects for a given domain is a class with properties representing different effects.
Effects typically return observable of NgRx actions. Example of that could be an effect that:
- Listens for the [Auth] Login Start actions.
- Wehever such action comes in, it sends a login request to some authentication server.
- When the response comes back, it either issues [Auth] Logic Success or [Auth] Login Fail action.
- The reducer sets up the state properly for all 3 kinds of actions mentioned above (it handles them before their effects are executed!).
- Some UI component subscribes to state changes and reacts properly to each state change.
In some cases though, effects do not emit any actions. Instead, an effect could
use a Router to redirect the user somewhere else. To do that,
createEffect function needs to be provided with a second argument:
With effects, our components can be decoupled from the logic of how to do some
external activity (like, how to get some data). All component does is it invokes
some action like
FetchBooks. It also subscribes to the
books slice of the
store to receive the books as soon as they arrive.
Accessing state within an action is possible with the
RxJs operator. It joins the original observable with data
from some another observable (like some state selector).
Redux Dev Tools
Redux Dev Tools is a browser extension (or standalone app) that is similar to browser DevTools. It requires the app to be extended with DevTools module, which is not ideal, but allows us to see in detail what happens with the store:
- actions being emited
- shape of the store after each action
- and more…
An offical package from NgRx @ngrx/router-store is an addition that emits actions based on Router’s activity. Whenever some navigation happens, etc., a specific action type is emitted with some payload, allowing us to react to it in our actions or reducers.
The Old API
Many projects still use the older API exposed by NgRx. Some of the below code would generate warnings, because the classes/methods are obsolete.