How to use NgRx and Standalone components
Since the release of Angular 14, we were introduced to a new way of structuring our Angular applications with Standalone components.
It's something that I embraced quickly because I was already using the SCAM pattern and Standalone components removed all the boilerplate that came with it.
In this article, I won't go into explaining what NgRx is or what Standalone components are. For the latter, I recommend checking out my article about Standalone components. You will find all that you need to get up and running in no time.
How do I provide the store?
This is applicable only for standalone applications. When you start an application with a standalone component, you will need to specify in the bootstrapApplication
the component together with its dependencies.
Here, you can use some of the OOTB NgRx functions to provide the imports and providers. Let's have an example:
bootstrapApplication(AppComponent, {
providers: [
provideStore({
shop: shopItemsReducer,
basket: basketItemsReducer,
}),
provideEffects([ShopListEffects, BasketEffects]),
provideStoreDevtools({ maxAge: 25, logOnly: false }),
]
})
You will need to provide the reducers, the effects and optionally the Devtools. If you want to stick to the old way of importing, you can do it like this but I do not recommend it.
bootstrapApplication(AppComponent, {
providers: [
importProvidersFrom(
StoreModule.forRoot({}),
StoreModule.forFeature('shop', shopItemsReducer),
StoreModule.forFeature('basket', basketItemsReducer),
EffectsModule.forRoot([]),
EffectsModule.forFeature(ShopListEffects),
EffectsModule.forFeature(BasketEffects),
StoreDevtoolsModule.instrument({
maxAge: 25, // Retains last 25 states
logOnly: false, // Restrict extension to log-only mode
}),
)
]
})
It's a bit more verbose and you can easily miss StoreModule.forRoot({})
and EffectsModule.forRoot([])
and it will take a while to figure out that you need to use importProvidersFrom()
function to get the providers from the modules.
If you will use the functions provided by NgRx, you will not have to worry about it.
How do I test a standalone component that is using NgRx?
It's quite easy, similar to what you used to do in the past. Only the setup is a bit different.
I will give you a step-by-step example of how to setup the test.
- Setup an initial state and declare the mockStore
...
let mockStore: MockStore;
beforeEach(() => {
const initialState: ShopState = {
shop: {
[ShopStateKeys.SHOP_ITEMS]: []
},
basket: {
[BasketStateKeys.BASKET_ITEMS]: [],
[BasketStateKeys.BASKET_TOTAL_PRICE]: 0,
[BasketStateKeys.BASKET_COUNT]: 0,
[BasketStateKeys.BASKET_CHECKOUT_OPEN]: false
}
}
...
- Provide the Store with its initial state in the TestBed
TestBed.configureTestingModule({
imports: [
NgrxShopComponent,
],
providers: [
provideMockStore({ initialState }),
]
});
- Inject the store to the storeMock using the TestBed
mockStore = TestBed.inject(MockStore);
The setup is complete. Now, you have two ways of changing the store during the test execution. Depending on your use case you will either override the state of the store (not recommended) or you override the selectors.
Overriding the state:
it('should fetch items from store', (done) => {
const mockItems: ShopItem[] = [
{ id: 1, name: 'Item 1', price: 100, description: 'Test 1' },
{ id: 2, name: 'Item 2', price: 100, description: 'Test 2' },
]
mockStore.setState(
{
shop: {
[ShopStateKeys.SHOP_ITEMS]: mockItems
},
basket: {
[BasketStateKeys.BASKET_ITEMS]: [],
[BasketStateKeys.BASKET_TOTAL_PRICE]: 0,
[BasketStateKeys.BASKET_COUNT]: 0,
[BasketStateKeys.BASKET_CHECKOUT_OPEN]: false
}
}
);
fixture.detectChanges();
component.items$.subscribe(items => {
expect(items).toEqual(mockItems);
done();
})
})
Overriding the selectors:
it('should fetch items from store', (done) => {
const mockItems: ShopItem[] = [
{ id: 1, name: 'Item 1', price: 100, description: 'Test 1' },
{ id: 2, name: 'Item 2', price: 100, description: 'Test 2' },
]
mockStore.overrideSelector(selectShopItems, mockItems);
mockStore.overrideSelector(selectBasketItems, [])
fixture.detectChanges();
component.items$.subscribe(items => {
expect(items).toEqual(mockItems);
done();
})
})
I do not recommend using setState()
because it doesn't guarrantee you that the selectors will give you the response you need. Overriding the selectors is more similar to mocking the API of a white box. You want to test how you interact with it, not how it works underneeth the hood.
Also, overriding selectors allows you to not provide the entire state of the store which can be, especially on big projects, a bit daunting.
The usage of NgRx is similar to what you've seen in previous experience. There were some small changes to it if you're using standalone components but nothing too heavy to change.
Subscribe to my newsletter
Read articles from Mihai Oltean directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Mihai Oltean
Mihai Oltean
Software engineer / Angular / C#