NgRX Slice
ngrx-slice is a plugin that intends to provide the same functionalities that Redux Toolkit createSlice provides. It is meant to be opinionated .
ngrx-slice also assumes the consumers to be familiar with the concepts of NgRX . If not, please visit NgRX Official Documentations before continue on.
- Express a slice of the global state in a single central place
- Generate
ActionCreatorsfrom reducers' cases - Generate Async
ActionCreatorsfrom reducers' cases - Generate
MemoizedSelectorsfrominitialState - Utilize
immerandngrx-immerunder the hood for State updates - Customizable (partial) Actions' types
Peer Dependencies
| NgRx Slice | Angular | NgRX |
|---|---|---|
| v6 | v13 | v13 |
| v5 | v11, v12 | v11, v12 |
| No support for Angular <11 because of TypeScript version | ||
Installation
npm install ngrx-sliceyarn add ngrx-slice
ngrx-slice has ngrx-immer and immer as its peerDependencies so go ahead and install those:
npm install ngrx-immer immeryarn add ngrx-immer immer
Here's one command for all three:
npm install ngrx-slice ngrx-immer immeryarn add ngrx-slice ngrx-immer immer
Counter Example
You have pressed increment: 0 times
You have pressed decrement: 0 times
This is the most minimal-simple example of a Counter . Let's take a look again at slice.ts
slice.ts
import { createSlice } from 'ngrx-slice'; export interface CounterState { value: number; incrementCount: number; decrementCount: number; } export const initialState: CounterState = { decrementCount: 0, incrementCount: 0, value: 0, }; export const { actions: CounterActions, selectors: CounterSelectors, ...CounterFeature } = createSlice({ name: 'counter', initialState, reducers: { increment: (state) => { state.value++; state.incrementCount++; }, decrement: (state) => { state.value--; state.decrementCount++; }, }, });
A slice represents a piece of the global state . createSlice works based on two main things:
reducers: used to generate theActionCreatorsinitialState: used to generate theMemoizedSelectors
Reducers - ActionCreators
With createSlice , we express the State Updaters via reducers . Each case defined in reducers will act as the name of the generated ActionCreator respectively
For example, reducers.increment will allow createSlice to generate actions.increment()
Initial State - MemoizedSelectors
On the other hand, the initialState will be used to generate the Selectors . If your initialState is an empty object {} , then no Selectors will be generated except for the Feature Selector
For example, initialState in the example above will generate selectors.selectCounter ( FeatureSelector via createFeatureSelector ), selectors.selectValue ( initialState.value via createSelector ), selectors.selectIncrementCount , and selectors.selectDecrementCount
Mutability
createSlice makes use of Immer under the hood to provide simpler State Updates operations. Normally, mutating state is not allowed but as you notice, we mutate our CounterState with state.value++ . This is all thanks to Immer . For more complex update patterns, please check out Immer documentations
Counter with Effects Example
You have pressed increment: 0 times
You have pressed decrement: 0 times
In this Effects example, we introduce a couple of additional features that ngrx-slice has
- Async Case Reducer
- Typed Action
- Noop Reducers
Async Case Reducer
In previous examples, we have seen increment: state => {/*...*/} . This is a Case Reducer which is a reducer function that updates state . In addition, ngrx-slice provides Async Case Reducer to define group of cases that are highly related to each other.
An Async Case Reducer is an object with the shape of { success: CaseReducer, trigger?: CaseReducer, failure?: CaseReducer, cancel?: CaseReducer, clear?: CaseReducer } where the success case is required .
Typed Action
Since Action Creators are generated from Case Reducers , there is a need to provide a type for Actions with Payload
ngrx-slice provides PayloadAction type to do so.
increment: state => {/*...*/}givesactions.increment()increment: (state, action: PayloadAction<{ value: number }>) => {/*...*/}givesactions.increment({ value: 5 })
Noop Reducers
Not all Actions need to be handled by Reducers . Sometimes, we have some Actions as triggers for Side Effects . For these situations, ngrx-slice provides the noopReducer<ActionPayload, SliceState>() which does nothing rather than making itself available as a generated action.
Namespaced Slice
ngrx-slice exposes a more opinionated createSlice which is createNamespacedSlice
createNamespacedSlice utilizes the name of the slice to return a more-opinionated object, aka "namespaced". For example:
import { createNamespacedSlice } from 'ngrx-slice'; export interface CounterState { value: number; incrementCount: number; decrementCount: number; } export const initialState: CounterState = { decrementCount: 0, incrementCount: 0, value: 0, }; export const { CounterActions, CounterSelectors, CounterFeature } = createNamespacedSlice({ name: 'counter', initialState, reducers: { increment: (state) => { state.value++; state.incrementCount++; }, decrement: (state) => { state.value--; state.decrementCount++; }, }, });
Action Type
By default, all generated actions will have the following type: [featureName_in_capitalize]: actionName and all generated async actions will have the same type with success , failure , and triggersuffixed to respective actions. For example:
CounterActions.increment(); // {type: '[Counter] increment'} CounterActions.decrement(); // {type: '[Counter] decrement'} CounterActions.double(); // {type: '[Counter] double'} CounterActions.multiplyBy.trigger(); // {type: '[Counter] multiplyBy trigger'} CounterActions.multiplyBy.success(); // {type: '[Counter] multiplyBy success'}
This behavior is customizable with sliceActionNameGetter that createSlice accepts. sliceActionNameGetter has the following signature: (featureName: string, actionName: string) => string;
External Actions
With createSlice API, internal reducers are used to generate ActionCreators , what about external actions that can change the CounterState ? We can utilize extraReducers for that
import { on } from '@ngrx/store'; import { triple } from 'path/to/external'; const { CounterActions, CounterSelectors, CounterFeature } = createSlice({ name: 'counter', initialState, reducers: { /* ... */ }, extraReducers: [ on<CounterState, [typeof triple]>(triple, (state) => ({ ...state, value: state.value * 3, })), ], });
Because triple is an action that is external to this slice, createSlice will not generate an action for triple when it's used in extraReducers . extraReducers will be merged with reducers and return a complete reducer for StoreModule.forFeature()
Entity Example w/ Todo
Similar to @ngrx/entity , ngrx-slice also exposes a way to deal with Entity which is a function called createSliceEntityAdapter() . This function is exported from ngrx-slice/entity
The signature of createSliceEntityAdapter is exactly the same as createEntityAdapter from @ngrx/entity
selectId: an ID selector function. Default to:(entity) => entity.id. If your entity uses a different field as the ID rather thanid, you need to provideselectIdsortComparer: a comparer function which will be used to determine the order ofentityState.ids. Default to:false
import { createSelector } from '@ngrx/store'; import { createSlice } from 'ngrx-slice'; import { createSliceEntityAdapter } from 'ngrx-slice/entity'; export interface Todo { id: number; text: string; isCompleted: boolean; } export const todoAdapter = createSliceEntityAdapter<Todo>({ sortComparer: (a, b) => a.text.localeCompare(b.text), }); export const { actions: TodoActions, selectors, ...TodoFeature } = createSlice({ name: 'todo', initialState: todoAdapter.getInitialState(), reducers: { todoAdded: todoAdapter.addOne, toggleComplete: todoAdapter.updateOne, }, }); export const TodoSelectors = { ...selectors, selectAll: createSelector( selectors.selectTodoState, todoAdapter.getSelectors().selectAll ), };
Same idea as createSlice , you do not have to return a new state in your reducers. Adapter methods are invoked with Immer's Draft .
You can either create your CaseReducer like usual and invoke the Adapter methods, or you can just use the Adapter methods as your CaseReducer . Like: todoAdded: todoAdapter.addOne
Special Mentions
- Marko Stanimirović ( @MarkoStDev ) for
ngrx-child-selectorsand hiscreateFeaturePR - Tim Deschryver ( @tim_deschryver ) for
ngrx-immer - Mark Erikson ( @acemarke ) for Redux Toolkit