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
ActionCreators
from reducers' cases - Generate Async
ActionCreators
from reducers' cases - Generate
MemoizedSelectors
frominitialState
- Utilize
immer
andngrx-immer
under 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-slice
yarn add ngrx-slice
ngrx-slice
has ngrx-immer
and immer
as its peerDependencies
so go ahead and install those:
npm install ngrx-immer immer
yarn add ngrx-immer immer
Here's one command for all three:
npm install ngrx-slice ngrx-immer immer
yarn 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 theActionCreators
initialState
: 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 trigger
suffixed 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 provideselectId
sortComparer
: 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-selectors
and hiscreateFeature
PR - Tim Deschryver ( @tim_deschryver ) for
ngrx-immer
- Mark Erikson ( @acemarke ) for Redux Toolkit