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
- Angular Schematics
- @ngrx/store
- @ngrx/effects
- @ngrx/store-devtools
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.
- Clone this project, starting at the
part1
branch:git clone --branch part1 https://github.com/landmarkhw/training-redux-arch.git
Navigate to the training-redux-arch
folder via command line.
Run npm install
to install all the dependencies.
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 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).
- MovieList needs the selected item to display an outline
- MovieDetails needs the selected item to display its details
- Without MovieList, there's no place to trigger a search, or store movie search results. What if we wanted to use MovieDetails alone?
- Service calls happen in components
Classes should know as little as possible about each other.
- MovieList knows about MovieDetails, and in fact is responsible for delivering all its content to it
- MovieList knows that it will be animated, and is responsible for animating itself and the MovieDetails.
From a command line, navigate to the project folder and run these commands from the official docs:
npm install @ngrx/store @ngrx/effects @ngrx/entity @ngrx/store-devtools --save
npm install @ngrx/schematics --save-dev
ng add @ngrx/store
ng add @ngrx/effects
ng add @ngrx/store-devtools
Notice that:
@ngrx/store
has been installed and added toapp.module.ts
for us- A default
State
interface and globalreducer
was created (src\app\reducers\index.ts
) - The Store and Effects have been registered with the app module.
@ngrx/store-devtools
has been installed. This allows you to use Redux DevTools to watch what's happening in Redux (actions & state).
Now, run this command to create a new Movie
feature in NgRx
:
ng generate @ngrx/schematics:feature Movie --module=app.module.ts --api
Notice that:
- An actions file was created
movie.actions.ts
- A reducer was created
movie.reducer.ts
andmovie.reducer.spec.ts
- An effect was created
movie.effects.ts
andmovie.effects.spec.ts
There should be 3 separate actions. We will dispatch:
SearchMovies
- when the user initiates a search for movies. It doesn't need a payload (it always "finds all movies")SearchMoviesFailure
- when an error happens while communicating with the API. It can be of typeany
.SearchMoviesSuccess
- when the API returns a successful result. Its payload should be of typeSearchResults
.
Add a switch case for SearchMoviesSuccess
that reduces the action into state:
case MovieActionTypes.SearchMoviesSuccess:
return {
...state,
searchResults: action.payload,
};
- Inject the
MovieService
into the constructor - e.g.private movieService: MovieService
- Create an effect that will call the
MovieService
and dispatch a success/failure action based on the results:
@Effect()
searchMovies = this.actions$.pipe(
ofType(MovieActionTypes.SearchMovies),
switchMap(() => this.movieService
.getNowPlaying({ page: "1" })
.pipe(
map((searchResults: SearchResults) => new SearchMoviesSuccess(searchResults)),
catchError(error => of(new SearchMoviesFailure({ error })))
)
)
);
- Create a file named
movie.selectors.ts
next to the other NgRx files. - Create a selector named
getSearchResultItems
that retrieves a list of movies from the search results, or an empty array otherwise:
export const getSearchResultItems = createSelector(
state => state.movie,
movie => movie.searchResults && movie.searchResults.results || [],
);
In the constructor, first inject the store:
private store$: Store<any>
Then, in the constructor initialize searchResults$
like this:
this.searchResults$ = this.store$.pipe(select(getSearchResultItems));
In ngOnInit()
, dispatch a SearchMovies
action like this:
this.store$.dispatch(new SearchMovies());