-
Notifications
You must be signed in to change notification settings - Fork 0
tasks
5min
-
Install @ngrx/store with
ng add @ngrx/store
. -
Install @ngrx/store-devtools with
ng add @ngrx/store-devtools
. -
Open app.module.ts and have a look on the modules automatically set up for you.
-
Make sure having Redux Devtools installed in your Browser (Chrome, Firefox).
- If you like to have more consistency in your NgRx code base you might want to check out @ngrx/eslint-plugin. It represents a list of the recommended code style used by the NgRx Team
- Install @ngrx/eslint-plugin with
ng add @ngrx/eslint-plugin
- You can read about each rule in the docs: https://ngrx.io/guide/eslint-plugin
10min
- Create a new directory book/store/.
- Create the file store/book-collection.actions.ts.
- Create the action
createBookStart
that requires the property { book: Book}. - Make sure that the action is exported.
- Open book-new.component.ts
- Inject the Store
- Dispatch
createBookStart
in the methodcreate
- Open Redux DevTools in your browser.
- Make sure that the dispatched action appears in the action log.
- Take some time to have a look what features the DevTools have.
- This task also has a bonus part.
// book-new.component.ts
import { Store } from '@ngrx/store';
this.store.dispatch(createBookStart());
Replace relative import paths with path-alias
- Create a barrel file (index.ts) inside store/book
- Make the barrel file export book-collection.actions.ts
- Open the root-tsconfig.json
- Add the path alias @store/book that points to ./src/app/book/store/index
- Update the import-path of createBookStart to use @store/book instead of the relative path.
"paths": {
"@store/book": ["./src/app/book/store/index"]
}
import { createBookStart } from '@store/book';
10min
Introduce Reducer
- Create a new file
book/store/book-collection.slice.ts
. - Create and export the interface
BookCollectionSlice
specifying the shape of your state slice. - Create a new file
book/store/book-collection.reducer.ts
. - Create a reducer function using createReducer.
- Specify an initial state.
- Handle the action
createBookStart
. - Add the book provided by the action to a list.
- Keep in mind that list operations have to be immutable.
- Register the reducer in
book.module.ts
by addingStoreModule.forFeature
. - Specify a name for the feature containing the book state.
- Export the name in its own file
book/store/book.feature.ts
because we need to reuse the feature name later on.
Manual Test
- Return to your application.
- Create a new book in the form
- Submit the form
- Check the Redux DevTools
- Expect to see the dispatched action
createBookStart
to have changed the state by adding a book to the state.
// book.feature.ts
export const bookFeatureName = 'book';
// book-collection.slice.ts
export interface BookCollectionSlice {
entities: ReadonlyArray<Book>;
}
// book-collection.reducer.ts
import { createReducer } from '@ngrx/store';
export const bookCollectionReducer = createReducer(/* ... */)
// book.module.ts
StoreModule.forFeature(bookFeatureName, { bookCollection: bookCollectionReducer })
- Open book-list.component.ts
- Inject the
Store
- Use the Store selecting the books instead of using the
BookApiService
Now, we are disconnected from the API. Later on we will load the books via HTTP again.
- Create a new book.
- After navigating back to the book list you should see the created book selected from the store.
5min
Current shape of the Store
{
book: { // feature name
bookCollection: { // slice
entities: [] // data
}
}
}
Select data
this.store.select(state => state.<feature name>.<slice>.<data>)
5min
-
Open the file book/store/book.feature.ts
-
Create a feature selector using the constant specifying the feature name.
-
Create the file 'book/store/book-collection.selectors.ts'.
-
Import the feature selector from book.feature.ts
-
Create a selector providing the books of the store slice.
-
Open the file book/book-list.component.ts
-
Use the selector you have created instead of the inline projection function
-
Create a new book.
-
After navigating back to the book list, you should see the created book selected from the store.
import { createFeatureSelector, createSelector } from '@ngrx/store';
createFeatureSelector<{ bookCollection: BookCollectionSlice }>(bookFeatureName);
createSelector();
10min
-
Open book/store/book-collection.selectors.ts.
-
Add the selector
bookByIsbn
that accepts an ISBN as parameter. -
Make the selector returning one book that has the given ISBN.
-
Open book-details.component.ts.
-
Update initialization of
book$
by selecting the book from the store instead of usingBookApiService
. -
Use the selector
bookByIsbn
This task has a bonus part
// book-collection.selectors.ts
export const bookByIsbn = (isbn: string) =>
createSelector(<reuse the selector you have written to select the books>, books => books.find(book => book.isbn === isbn));
// fragment from book-details.component.ts
switchMap(params => this.store.select(bookByIsbn(params.isbn)))
- Use the selector
bookByIsbn
in book-edit.component.ts, too.
5min
-
Open book/store/book.feature.ts.
-
Add the intrerface
BookState
representing the shape of the book-feature. -
Assign
ActionReducerMap<BookState>
to an exported const namedbookReducers
, collecting all reducers of the book-feature. -
Update the feature-selector to use the interface
BookState
instead of the inline typing. -
Open book.module.ts.
-
Update the
StoreModule.forFeature
-declaration to usebookReducer
, replacing the inline configuration for reducers.
10min
-
Install @ngrx/effects by executing
ng add @ngrx/effects
. -
Check if the installation was successful by having a look at app.module.ts.
- You should see EffectsModule.forRoot([])
-
Open book/store/book-collection.actions.ts
-
Create the action
loadBookStart
, being used to send a GET-Request to the API. -
Create the action
loadBookComplete
, being used to react to the response of the API. -
Create the file book/store/book-collection.effects.ts
-
Implement the effect
load
that uses BookApiService to load all books. -
Return the action
loadBookComplete
, when the books have been loaded successfully. -
Register BookCollectionEffects in book.module.ts by adding
EffectsModule.forFeature([BookCollectionEffects])
. -
Open book/store/book-collection.reducer.ts.
-
Update the reducer function handling the action
loadBookComplete
.- It should save the books coming from the API to the store.
-
Open book.component.ts.
-
Dispatch the action
loadBookStart
triggering the GET-Request loading all books. -
Visit your app again.
-
Check if the books are loaded instantly when you visit the book list.
// actions
export const loadBooksStart = createAction('[Book] Load Books Started');
export const loadBooksComplete = createAction('[Book] Load Books Completed', props<{ books: Book[] }>());
15min
- Implement the feature "Create Book" using the NgRx infrastructure.
export const createBookComplete = createAction('[Book] Create Book Completed', props<{ book: Book }>());
10min
- Implement the feature "Update Book" using the NgRx infrastructure.
- Implement the feature "Delete Book" using the NgRx infrastructure.
export const deleteBookStart = createAction('[Book] Delete Book Started');
export const deleteBookComplete = createAction('[Book] Delete Book Completed', props<{ bookIsbn: string }>());
export const updateBookStart = createAction('[Book] Update Book Started', props<{ patch: Book }>());
export const updateBookComplete = createAction('[Book] Update Book Completed', props<{ update: Book }>());
10min
- Install NgRx Entity by executing
ng add @ngrx/entity
. - Update the interface
BookCollectionSlice
to extendEntityState<Book>
. - Update the reducer
bookCollectionReducer
using the EntityAdapter-Methods getInitialState, addOne, updateOne, removeOne, setAll. - Use the selector
selectAll
of the entityAdapter for the selectorbookCollection
// book-collection.slice.ts
export type BookCollectionSlice = EntityState<Book>;
// book-collection.reducer.ts
const adapter = createEntityAdapter<Book>({ selectId: model => model.isbn });
10min
-
Install the RouterStore by executing
ng add @ngrx/router-store
-
Register the RouterStore's reducer in app.module.ts:
StoreModule.forRoot({ router: routerReducer }, { /* ... */ })
-
Create the file app/store/router/router.selectors.ts
-
Reexport all RouterStore's selctors (see: NgRx Docs | Selectors)
-
Open book/store/book-collection.selectors.ts
-
Update
bookByIsbn
to use the RouterStore's selectorselectRouteParam('isbn')
- Therefore you need to compose two selectors: bookCollection & selectRouteParam
- This means
bookByIsbn
no longer takes a parameter because we now get the route parameter from the Store.
-
Update all usages of
bookByIsbn
-
Remove the
ActivatedRoute
-Service from the respective components.
Notice that each component that is fully migrated has only one dependency, the Store.
You can updating the effects as well retrieving the params
from the store as well.
// ...
concatLatestFrom(() => this.store.select(selectRouteParam('isbn'))),
exhaustMap(([, bookIsbn]) => this.bookApi.delete({ bookIsbn }),
Replace relative import paths with path-alias
- Create a barrel file (index.ts) inside app/store/router.
- Make the barrel file export router-store-related files.
- Open the root-tsconfig.json.
- Add the path alias @store/router that points to ./src/app/store/router/index.
- Update relevant import-paths.
"paths": {
"@store/router": ["./src/app/store/router/index"]
}
10min
- Open book-list.component.spec.ts.
- Write a test ensuring that books are rendered in the template
- Therefore, mock the selector to return a collection of fake books.
Ressource: NgRx Testing
import { MockStore, provideMockStore } from '@ngrx/store/testing';
providers: [provideMockStore()],
store = TestBed.inject(MockStore);
store.overrideSelector(bookCollection as any, [new BookNa()]);
- Create book/store/book-collection.effects.spec.ts
- Write a test verifying that dispatching
createBookStart
ends up in caching the created book in the Store. - The flow of the test could be
- Set up a mock for the BookApiService
- Configure the TestBed to import EffectsModule.forRoot, StoreModule.forRoot, StoreModule.forFeature.
- Dispatch the action createBookStart with a mocked book
- Use the selector bookCollection to query the books of the store
- Assert that the dispatched book is part of the collection
15min
// Service Mock
bookApiMock = jasmine.createSpyObj<BookApiService>(['create']);
bookApiMock.create.and.returnValue(of(book));
// TestBed
TestBed.configureTestingModule({
imports: [
RouterTestingModule,
EffectsModule.forRoot([BookCollectionEffects]),
StoreModule.forRoot({}),
StoreModule.forFeature(bookFeatureName, bookReducers)
],
providers: [{ provide: BookApiService, useFactory: () => bookApiMock }]
});
// Act
store.dispatch(createBookStart({ book }));
// Assertion
store.select(bookCollection).subscribe(books => {
expect(books).toContain(book);
done();
});
10min
- Create the file book/store/book-collection.reducer.spec.ts
- Write a test ensuring that action
bookCreateComplete
adds a book to the store.
This task contains a bonus part.
const initialState: EntityState<Book> = { entities: {}, ids: [] };
const action = createBookComplete({ book });
const state = bookCollectionReducer(initialState, action);
- Write tests for every action handled by
bookCollectionReducer
.
- Migrate to NX
- If you are using path aliases, you need to update them manually
- Create an App-Library for books
npx @angular/cli add @nrwl/angular
"paths": {
- "@store/book": ["./src/app/book/store/index"],
- "@store/router": ["./src/app/store/router/index"]
+ "@store/book": ["./apps/workshop/src/app/book/store/index"],
+ "@store/router": ["./apps/workshop/src/app/store/router/index"]
},