
Security News
Deno 2.2 Improves Dependency Management and Expands Node.js Compatibility
Deno 2.2 enhances Node.js compatibility, improves dependency management, adds OpenTelemetry support, and expands linting and task automation for developers.
@ngxp/store-service
Advanced tools
Adds an abstraction layer / facade between Angular components and the ngrx store with powerful testing helpers
Adds an abstraction layer between Angular components and the @ngrx store and effects. This decouples the components from the store, selectors, actions and effects and makes it easier to test components.
Get the latest version from NPM
npm install @ngxp/store-service
Component
import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { Book } from 'src/app/shared/books/book.model';
// Tight coupling to ngrx, state model, selectors and actions
import { Store } from '@ngrx/store';
import { Actions, ofType } from '@ngrx/effects';
import { AppState } from 'src/app/store/appstate.model';
import { getAllBooks, getBook } from 'src/app/store/books/books.selectors';
import { addBookAction, booksLoadedAction } from 'src/app/store/books/books.actions';
@Component({
selector: 'nss-book-list',
templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.scss']
})
export class BookListComponent {
books$: Observable<Book[]>;
book$: Observable<Book>;
booksLoaded: boolean = false;
constructor(
private store: Store<AppState>
private actions: Actions
) {
this.books$ = this.store.select(getAllBooks);
this.book$ = this.store.select(getBook, { id: 0 });
this.actions
.pipe(
ofType(booksLoadedAction),
map(() => this.loaded = true)
)
.suscribe();
}
addBook(book: Book) {
this.store.dispatch(addBookAction({ book }));
}
}
Component
import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { Book } from 'src/app/shared/books/book.model';
import { BookStoreService } from 'src/app/shared/books/book-store.service';
// Reduced to just one dependency. Loose coupling
@Component({
selector: 'nss-book-list',
templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.scss']
})
export class BookListComponent {
books$: Observable<Book[]>;
book$: Observable<Book>;
booksLoaded: boolean = false;
constructor(
private bookStore: BookStoreService // <- StoreService
) {
this.books$ = this.bookStore.getAllBooks(); // <- Selector
this.book$ = this.bookStore.getBook({ id: 0 }); // <- Selector
this.bookStore.booksLoaded$ // <-- Observer / Action stream of type
.pipe(
map(() => this.loaded = true)
)
.subscribe();
}
addBook(book: Book) {
this.bookStore.addBook({ book }); // <- Action
}
}
BookStoreService
import { Injectable } from '@angular/core';
import { select, StoreService, dispatch, observe } from '@ngxp/store-service';
import { Book } from 'src/app/shared/books/book.model';
import { getBooks } from 'src/app/store/books/books.selectors';
import { State } from 'src/app/store/store.model';
import { addBookAction, booksLoadedAction } from 'src/app/store/books/books.actions';
@Injectable()
export class BookStoreService extends StoreService<State> {
getAllBooks = select(getBooks); // <- Selector
getBook = select(getBook); // <- Selector
addBook = dispatch(addBookAction); // <- Action
booksLoaded$ = observe([booksLoadedAction]); // <- Observer / Action stream
}
The BookStoreService
Injectable class has to extend the StoreService<State>
class where State
is your ngrx state model.
import { StoreService } from '@ngxp/store-service';
import { AppState } from 'app/store/state.model';
@Injectable()
export class BookStoreService extends StoreService<AppState> {
...
}
To use selectors you wrap the ngrx selector inside the select(...)
function:
// Define the selector function
export const selectAllBooks = createSelector(
(state: State) => state.books;
};
//Or with props
export const selectBook = createSelector(
(state: State, props: { id: number }) => state.books[id];
};
...
// Use the selector function inside the select(...) function
allBooks = select(selectAllBooks); // () => Observable<Book[]>
book = select(selectBook); // (props: { id: number }) => Observable<Book>
The select(...)
function automatically infers the correct typing according to the props and return type of the selector.
To dispatch actions add a property with the dispatch(...)
function.
// Defined the Action as a class
export const loadBooksAction = createAction('[Books] Load books');
export const addBookAction = createAction('[Books] Add book' props<{ book: Book}>())
...
loadBooks = dispatch(loadBooksAction); // () => void
addBook = dispatch(addBookAction); // (props: { book: Book }) => void
The dispatch(...)
function automatically infers the parameters according to the props of the action.
Observers are a way to listen for specific action types on the Actions
stream from @ngrx/effects.
booksLoaded$ = observe([booksLoadedAction]);
You can provide multiple types, just like in the ofType(...)
pipe.
booksLoaded$ = observe([booksLoadedAction, booksLoadFailedAction]);
The observe(...)
function has an additional parameter to provide a custom customMapper
mapping function.
Initially this will be:
action => action
To use a custom mapper, provide it as second argument in the observe(...)
function.
export const toData = action => action.data;
...
dataLoaded$ = observe([dataLoadedAction], toData);
Before Version 12 the Store Service used annotations instead of functions. This old way still works but is deprecated and will be removed in the future.
import { Select, StoreService, Dispatch, Selector, Dispatcher, Observe } from '@ngxp/store-service';
@Select(getBooks)
getAllBooks: Selector<typeof getBooks>;
@Dispatch(addBookAction)
addBook: Dispatcher<typeof addBookAction>;
@Observe([dataLoadedAction], toData)
dataLoaded$: Observable<Book[]>;
Testing your components and the StoreService is easy. The @ngxp/store-service/testing
package provides useful test-helpers to reduce testing friction.
To test selectors you provide the StoreService
using the provideStoreServiceMock
method in the testing module of your component. Then get the StoreServiceMock<T>
instance using the getStoreServiceMock
helper function.
import { provideStoreServiceMock, StoreServiceMock, getStoreServiceMock } from '@ngxp/store-service/testing';
...
let bookStoreService: StoreServiceMock<BookStoreService>;
...
TestBed.configureTestingModule({
declarations: [
BookListComponent
],
providers: [
provideStoreServiceMock(BookStoreService)
]
})
...
bookStoreService = getStoreServiceMock(BookStoreService);
The StoreServiceMock
class replaces all selector functions on the store service class with a BehaviorSubject
. So now you can do the following to emit new values to the observables:
bookStoreService.getAllBooks().next(newBooks);
The BehaviorSubject
is initialized with the value being undefined
. If you want a custom initial value, the provideStoreServiceMock
method offers an optional parameter. This is an object of key value pairs where the key is the name of the selector function, e.g. getAllBooks
.
import { provideStoreServiceMock, StoreServiceMock, getStoreServiceMock } from '@ngxp/store-service/testing';
...
let bookStoreService: StoreServiceMock<BookStoreService>;
...
TestBed.configureTestingModule({
declarations: [
BookListComponent
],
providers: [
provideStoreServiceMock(BookStoreService, {
getAllBooks: []
})
]
})
...
bookStoreService = getStoreServiceMock(BookStoreService);
The BehaviorSubject
for getAllBooks
is now initialized with an empty array instead of undefined
.
To test if a component calls the dispatch methods you provide the StoreService
using the provideStoreServiceMock
method in the testing module of your component. Then get the StoreServiceMock<T>
instance using the getStoreServiceMock
helper function.
You can then spy on the method as usual.
import { provideStoreServiceMock, StoreServiceMock } from '@ngxp/store-service/testing';
...
let bookStoreService: StoreServiceMock<BookStoreService>;
...
TestBed.configureTestingModule({
declarations: [
NewBookComponent
]
imports: [
provideStoreServiceMock(BookStoreService)
]
})
...
it('adds a new book', () => {
const book: Book = getBook();
const addBookSpy = jest.spyOn(bookStoreService, 'addBook');
component.book = book;
component.addBook();
expect(addBookSpy).toHaveBeenCalledWith({ book });
});
To test observers inside components you provide the StoreService
using the provideStoreServiceMock
method in the testing module of your component. Then get the StoreServiceMock<T>
instance using the getStoreServiceMock
helper function.
import { provideStoreServiceMock, StoreServiceMock, getStoreServiceMock } from '@ngxp/store-service/testing';
...
let bookStoreService: StoreServiceMock<BookStoreService>;
...
TestBed.configureTestingModule({
declarations: [
BookListComponent
],
providers: [
provideStoreServiceMock(BookStoreService)
]
})
...
bookStoreService = getStoreServiceMock(BookStoreService);
The StoreServiceMock
class replaces all observer properties on the store service class with a BehaviorSubject
. So now you can do the following to emit new values to the subscribers:
bookStoreService.booksLoaded$().next(true);
The BehaviorSubject
is initialized with the value being undefined
. If you want a custom initial value, the provideStoreServiceMock
method offers an optional parameter. This is an object of key value pairs where the key is the name of the observer property, e.g. booksLoaded$
.
import { provideStoreServiceMock, StoreServiceMock, getStoreServiceMock } from '@ngxp/store-service/testing';
...
let bookStoreService: StoreServiceMock<BookStoreService>;
...
TestBed.configureTestingModule({
declarations: [
BookListComponent
],
providers: [
provideStoreServiceMock(BookStoreService, {
booksLoaded$: false
})
]
})
...
bookStoreService = getStoreServiceMock(BookStoreService);
The BehaviorSubject
for booksLoaded$
is now initialized with false
instead of undefined
.
To test the StoreService
itself you use the provided test helpers from @ngrx/store/testing
and @ngrx/effects/testing
.
You can provide mocks for selectors with the provideMockStore
from @ngrx/store/testing
a. See (@ngrx/store Testing)[https://ngrx.io/guide/store/testing] for their documentation.
Mock the selectors using the provideMockStore
function and check if the StoreService
returns an Observable with the mocked value.
import { MockStore, provideMockStore, getStoreServiceMock } from '@ngrx/store/testing';
import { BookStoreService } from 'src/app/shared/books/book-store.service';
import { selectBook, selectBooks } from '../../store/books/books.selectors';
describe('BookStoreService', () => {
let bookStoreService: BookStoreService;
let mockStore: MockStore<{ books: BookState }>;
const books: Book[] = [
{
author: 'Joost',
title: 'Testing the StoreService',
year: 2019
}
];
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
providers: [
BookStoreService,
provideMockStore({
selectors: [
{
selector: selectBooks,
value: books
},
{
selector: selectBook,
value: books[0]
}
]
})
]
});
}));
beforeEach(() => {
bookStoreService = getStoreServiceMock(BookStoreService);
mockStore = TestBed.inject(Mockstore);
});
it('executes the getBooks Selector', () => {
const expected = cold('a', { a: books });
expect(bookStoreService.getAllBooks()).toBeObservable(expected);
});
it('executes the getBook Selector', () => {
const expected = cold('a', { a: books[0] });
expect(bookStoreService.getBook({ id: 0 })).toBeObservable(expected);
});
});
You can provide mocks for selectors with the provideMockStore
from @ngrx/store/testing
a. See (@ngrx/store Testing)[https://ngrx.io/guide/store/testing] for their documentation.
Mock the selectors using the provideMockStore
function and check if the StoreService
returns an Observable with the mocked value.
To test if the StoreService
dispatches the correct actions the MockStore
from @ngrx has a property called scannedActions$
. This is an Observable of all dispatched actions to check if an action was dispatched correctly.
import { MockStore, provideMockStore, getStoreServiceMock } from '@ngrx/store/testing';
import { cold } from 'jest-marbles';
import { BookStoreService } from 'src/app/shared/books/book-store.service';
import { addBookAction, loadBooksAction } from '../../store/books/books.actions';
describe('BookStoreService', () => {
let bookStoreService: BookStoreService;
let mockStore: MockStore<{ books: BookState }>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
providers: [
BookStoreService,
provideMockStore()
]
});
}));
beforeEach(() => {
bookStoreService = getStoreServiceMock(BookStoreService);
mockStore = TestBed.inject(MockStore);
});
it('dispatches a new addBookAction', () => {
const book: Book = getBook();
bookStoreService.addBook({ book });
const expected = cold('a', { a: addBookAction({ book }) });
expect(mockStore.scannedActions$).toBeObservable(expected);
});
it('dispatches a new loadBooksAction', () => {
bookStoreService.loadBooks();
const expected = cold('a', { a: loadBooksAction() });
expect(mockStore.scannedActions$).toBeObservable(expected);
});
});
To test the observers / actions stream, you import the provideMockActions
from @ngrx/effects/testing
inside the testing module.
Then check if the Observer filters the correct actions.
import { getStoreServiceMock } from '@ngxp/store-service/testing';
import { provideMockActions } from '@ngrx/effects/testing';
import { BehaviorSubject } from 'rxjs';
import { BookStoreService } from 'src/app/shared/books/book-store.service';
import { booksLoadedAction } from '../../store/books/books.actions';
describe('BookStoreService', () => {
let bookStoreService: BookStoreService;
const mockActions = new BehaviorSubject(undefined);
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
providers: [
BookStoreService,
provideMockActions(mockActions)
]
});
}));
beforeEach(() => {
bookStoreService = getStoreServiceMock(BookStoreService);
});
it('filters the BooksLoadedActions in booksLoaded$', () => {
const expectedValue: Book[] = [{
author: 'Author',
title: 'Title',
year: 2018
}];
const action = booksLoadedAction({ books: expectedValue });
mockActions.next(action);
const expected = cold('a', { a: action });
expect(bookStoreService.booksLoaded$()).toBeObservable(expected);
});
});
For detailed examples of all this have a look at the Angular Project in the apps/store-service-sample folder.
Have a look at the BookStoreService
For examples on Component Tests please have look at the test for the BookListComponent and the NewBookComponent
Testing the StoreService
is also very easy. For an example have a look at the BookStoreService
FAQs
Adds an abstraction layer / facade between Angular components and the ngrx store with powerful testing helpers
The npm package @ngxp/store-service receives a total of 14 weekly downloads. As such, @ngxp/store-service popularity was classified as not popular.
We found that @ngxp/store-service demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 3 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Deno 2.2 enhances Node.js compatibility, improves dependency management, adds OpenTelemetry support, and expands linting and task automation for developers.
Security News
React's CRA deprecation announcement sparked community criticism over framework recommendations, leading to quick updates acknowledging build tools like Vite as valid alternatives.
Security News
Ransomware payment rates hit an all-time low in 2024 as law enforcement crackdowns, stronger defenses, and shifting policies make attacks riskier and less profitable.