This is a multipart training workshop, aimed at teaching you the basic principles of Redux.
We'll be using the following technologies in this workshop:
- Angular
- Angular CLI
- ngrx/store
- ngrx/effects
The presentation is provided along with the workshop. Simply open the /presentation
folder and run the run.cmd
file. The presentation should automatically open at http://localhost:8000.
Note that you must have Python installed to run the run.cmd
command. Alternatively, you can probably just open index.html
directly to view the presentation.
- Clone this project:
git clone https://github.com/landmarkhw/training-redux-arch.git
- Checkout the "part1" branch:
git checkout part1
Run ng serve
for a dev server. Navigate to http://localhost:4200/
. The app will automatically reload if you change any of the source files.
At this point, the application doesn't have ngrx (an Angular implementation of Redux) installed yet. It's just a simple Angular application.
Let's get acquainted with the application.
Notice a few things:
- This file is pretty simple (although it's got a few Angular tricks).
- We're displaying both the movie details and the movie list in the same component.
- The
app-movie-details
component depends on data from the movie-list component. This means the movie-list component is responsible to track which movie has been selected.
This file is a pretty standard Angular component:
- Calls the
MovieService
to retrieve search results.- NOTE: we don't call themoviedb.org API directly here. We properly separate the concerns by creating a
MovieService
class that's responsible for interacting with the API.
- NOTE: we don't call themoviedb.org API directly here. We properly separate the concerns by creating a
- Holds the search results in the
searchResults$
observable variable (more on RxJS Observables in a later training). - Holds the selected movie in the
selectedMovie
variable. - Sets/clears the
selectedMovie
when a movie is clicked, or when the back button is clicked (respectively).
- Calls
themoviedb.org
's API - Uses an
api_key
specifically meant for this workshop.
Notice that there's business logic here: {{movie.vote_average / 2 }}
We want to display the movie rating as a 5-star rating, whereas the data is based on a 1-10 rating. This business logic is currently happening directly in the view (hint: not good).
In Part 2, ngrx/store
has been added. Run git checkout part2
to see the changes to the code.
This part is all about Separation of Concerns - we begin using the Redux architecture to separate different responsibilties into different areas of the application.
ngrx/store
has been installed and added toapp.module.ts
ngrx/store-devtools
has been installed. This allows you to use Redux DevTools to watch what's happening in Redux (actions & state).movie.actions.ts
has been added, in a new folder -actions
movie.reducers.ts
has been added, in a new folder -reducers
movie.service.ts
now dispatches (e.g. publishes) theGotSearchResults
action, which is reduced (saved) to state in themovieReducer
.movie-list.component.ts
now gets its search results byselect
ing (retrieving) it from the Redux store.
- This file is in a new folder -
actions
. This helps us organize the different parts of the application that have different responsibilities. - This file defines the actions that are available for the
movie
area of the application (which is currently the whole app). - The
MovieActionTypes
object is simply a collection of string constants that are used to uniqueqly identify each action. - The
GotSearchResultsAction
is a TypeScript definition of the one action we have -GotSearchResults
- The
MovieActions
object is a collection of action creators that are used to create actions when they occur.- Note that actions in Redux are just plain old JavaScript objects. They have a
type
, which is a string constant that identifies what kind of action it is, and they have apayload
, which is the data associated with the action.
- Note that actions in Redux are just plain old JavaScript objects. They have a
- This file is in a new folder - 'reducers'. This helps us group all our reducers in one area.
- This file does 2 things:
- Defines the shape of state for the
movie
area of the application (again, currently the whole app). - Defines a
reducer
, which is a function that, given the currentstate
of the application and anaction
that's happening in the store, returns a newstate
.
- Defines the shape of state for the
- Reducers are commonly built with a
switch
statement - they only respond to the actions that affect this part of the application's state.- In this case, the reducer is reacting to the
MovieActionTypes.GotSearchResults
action, by saving the data from the API call into state.
- In this case, the reducer is reacting to the
- You'll also notice that the reducer defines the
defaultState
of that area of the application. This is what the state looks like before any changes are made. - An important note: Reducers are pure functions.
- Given a set of input data, the same output is returned.
- Cannot produce side-effects
Being pure
functions, reducers provide us many guarantees, along with being unit-testable.
1 change: instead of returning this.http.get(...)
directly, we're subscribing to the results and dispatch
ing an action to the store with the search results.
1 change: instead of getting searchResults$
directly from the movieService
, we are instead retrieving the data from the store
. Now the store
is the "source of truth" for that data. One problem - we're manually navigating the state in the store
to find the information we need. This can create some tightly-coupled situations where the view is tightly-coupled with the shape of state. In the next part, we'll look at how to address this problem.
In Part 3, selectors have been added to the mix. Run git checkout part3
to see the changes to the code.
This part further separates our concerns - we address a couple of problems we noticed earlier:
- Business logic is found in the view
- Tight coupling of view to the shape of state
movie.selectors.ts
has been added, in a new folder -selectors
movie-list.component.ts
uses the new selectorgetSearchResultList
- This decouples the view from the shape of the state. Now, if the shape of state changes, we can simply modify the
getSearchResultList
selector accordingly, and the view is unaffected.
- This decouples the view from the shape of the state. Now, if the shape of state changes, we can simply modify the
movie-details.component.ts
andmovie-details.component.html
use the new selectorget5StarRating
- This moves business logic out of the view and into selectors.
New file - added 2 selectors
* getSearchResultList
- gets a list of movie search results from state
* get5StarRating
- gets the 5-star rating for a movie search result
Note that, like reducers, selectors are pure functions.
As with other pure
functions, selectors can easily be unit tested when necessary.
1 change - uses the getSearchResultList
selector. Note that it's passed directly to the select()
function.
2 changes - added get5StarRating()
method and use it in the html template.
In Part 4, effects have been added. Run git checkout part4
to see the changes to the code.
This part adds an important piece to Separation of Concerns with ngrx: Side-effect handling.
As mentioned before, reducers
and selectors
are both pure
functions: They cannot produce side-effects.
A few examples of side-effects are:
- Calling an API
- Mutating data
- Logging to console or file
- DOM manipulation
- Calling other functions that are not
pure
(e.g.Math.random()
)new Date()
- as it uses the current time, it's notpure
OK, so reducers
and selectors
can't have side-effects. What about our view
-- it seems like that's the most logical place for these side-effects to go.
It turns out, the answer to this is both "yes", and "no":
- If the
view
has side-effects that are scoped directly to that view, then it's OK for it to contain its own side-effects.- Note that this rarely includes making API calls - those are best done elsewhere.
- Otherwise, side-effects should be contained elsewhere.
In this case, we move the movieService.getNowPlaying()
call from the movie-list.component.ts
file into an effect.
As you've probably guessed, this helps us further separate our concerns.
The @ngrx/effects library uses RxJS to address the question of "where should my side-effects go?"
Effects
in @ngrx are RxJS Observables that watch actions
pass through the Redux store
.
I've added the defs.ts
file, which has some helper interfaces and functions for creating and working with actions/action creators.
- Removed the
GotSearchResults
action - Added new
Search
action, an async action- We did this since we have multiple actions that we need to represent now:
- 1st action - call the API
- 2nd action - receive results from the API
- 3rd action - handle any failures from the API
- Async actions do this for us. They are simply a collection of normal actions:
- BEGIN - the action is beginning, usually triggers some action or API call
- SUCCESS - the action has succeeded
- FAILURE - the action has failed
- UPDATE - triggered when a long-running action publishes a status update
- We did this since we have multiple actions that we need to represent now:
- Updated
MovieActions
- see line 18.NowPlayingSearchOptions
- the data passed to the BEGIN action.SearchResults
- the data passed to the SUCCESS action.MovieActionTypes.Search
- the type of action being created.
- Instead of calling the
MovieService
directly, we now dispatch an action
- The
MovieService
API is called here, when we see theMovieActionTypes.Search.BEGIN
action occur. - If the
MovieService
succeeds, it dispatches theMovieActionTypes.Search.SUCCESS
action. - If the
MovieService
fails, it dispatches theMovieActionTypes.Search.FAILURE
action.
There are still some concerns remaining that should be separated, that haven't been mentioned yet:
- The
selectedMovie
is tracked from themovie-list
component, but this information is useful for other parts of the application. - The
movie-list
component currently contains themovie-details
component, and is responsible to provide all data to it. This dependency should be eliminated.
This final part of the workshop will involve (finally), you doing some coding. Since this is a brand new concept, and for many (most) of you, Angular is a new technology, I'll walk you through it step-by-step.
- In
movie.actions.ts
,- Add
Select: Action("movie/select"),
to theMovieActionTypes
object. - Add
select: createAction<SearchResult>(MovieActionTypes.Select),
to theMovieActions
object.
- Add
- In
movie.reducers.ts
,- Add
selectedMovie: SearchResult;
to theMovieState
interface. - Add
selectedMovie: null,
to thedefaultState
object.
- Add
- In
movie.reducers.ts
,- Create a
case
statement for theMovieActionTypes.Select
action type. - Return a new state using Object.assign() that sets the
selectedMovie
.
- Create a
- In
movie.selectors.ts
,- Create a new selector
getSelectedMovie
that returns theselectedMovie
from themovie
state.
- Create a new selector
- In
movie-list.component.ts
,- Remove the
selectedMovie
field. - Add a new
selectedMovie$
field. Note that the$
suffix is called "Finnish Notation", and indicates that the variable is Observable. - Set the
selectedMovie$
field like we setsearchResults$
-this.selectedMovie$ = this.store.select(getSelectedMovie);
- Change
handleClick
to instead dispatch theMovieActions.select
action:this.store.dispatch(MovieActions.select(movie));
- Change
handleClose
to instead dispatch theMovieActions.select
action:this.store.dispatch(MovieActions.select(null));
- Remove the
- In
movie-list.component.html
,- Anywhere
selectedMovie
is found, replace it with(selectedMovie$ | async)
.
- Anywhere
This should take care of moving that data into Redux state. I know, it's a lot of steps, but now this data is agnostic, and can be accessed and worked with from anywhere in the application. It can also be used in selectors to gather multiple pieces of state together and work with them.
More on that in another training...
- In
movie-list.component.html
,- Move the node into
app.component.html
.
- Move the node into
- In
movie-list.component.ts
,- Move the
selectedMovie$
field toapp.component.ts
. - Move the
handleClose()
function toapp.component.ts
.
- Move the
- In
movie-list.component.css
,- Move the
app-movie-details
CSS rules toapp.component.css
. - Change the rule
.movie-container.active {
to:host.active .movie-container {
- This is because we're moving the responsibility of telling us which component is
active
to theapp
component.
- This is because we're moving the responsibility of telling us which component is
- Move the
- In
movie-list.component.html
,- Change the line starting with
[class]
line to readclass="movie-container flex-row"
- Change the line starting with
That's it! You've so completely decoupled the selectedMovie
from the movie-list
state, that you can
select (or deselect) a movie from either the movie-list
component or the movie-details
component.
I hope you found this training enlightening and valuable, but I'd love to hear your feedback. If this isn't the kind of information you need, the kind of training you'd like, please let me know -- I can't get better if I don't get feedback!
Regards, Doug