This release updates Immer to v8.x, adds a set of "matcher" utilities to simplify checking if dispatched actions match against a range of known action types, adds additional customization of async thunk error handling, and adds miscellaneous improvements around TS types and some reducer edge cases.
Changes
Immer v8
In RTK v1.4, we upgraded Immer to v7.x, which added a new current
API for debugging.
Immer recently released v8.0.0, which changes its default behavior around auto-freezing state values. Previously, Immer only auto-froze data in development mode, partly under the assumption that freezing would be slower overall. Due to internal changes in Immer 7, Immer now checks if data is frozen and can bail out of some of its processing early. As a result, Immer 8 switches to always freezing return values, even in production, Per discussion in the Immer issues linked from the v8 release announcement, this apparently is actually faster on average.
This is a noticeable change in behavior, but we consider it not breaking for RTK on the grounds that you shouldn't be mutating state outside of a reducer anyway, so there shouldn't be any visible effect on correctly-written Redux logic.
We've updated our Immer dependency to v8.x.
Per the Immer docs on auto-freezing, it may be more efficient to shallowly pre-freeze very large data that won't change by using Immer's freeze
utility. RTK now re-exports freeze
as well.
Action Matching Utilities
In RTK v1.4, we added the ability to write "matching reducers" that can respond to more than one potential action based on a predicate function, such as builder.addMatcher( (action) => action.type.endsWith('/pending'), reducer)
.
Many users have asked for the ability to directly list a series of RTK-generated action creators as possible actions to match against. For RTK 1.5, we're adding several utilities that will help handle that process.
First, we've added isAnyOf(matchers)
and isAllOf(matchers)
. These effectively perform boolean ||
and &&
checks, and accept an array containing RTK action creators or action-matching predicates. They return a new matching callback that can be passed to builder.addMatcher()
.
const isActionSpecial = (action: any): action is SpecialAction => {
return action.payload === 'SPECIAL'
}
const thunkA = createAsyncThunk<string>('a', () => 'result');
// later
createSlice({
name,
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addMatcher(isAllOf(isActionSpecial, thunkA.fulfilled), reducer);
}
})
When used with TypeScript, isAllOf
and isAnyOf
will correctly narrow down the possible type of action
based on the actions they match against.
We've also added a set of matching utilities specifically meant to help check if a given action corresponds to the lifecycle actions dispatched by some specific async thunks. isPending
, isFulfilled
, isRejected
, isRejectedWithValue
, and isAsyncThunkAction
all accept an array of thunks generated by createAsyncThunk
, and will match the corresponding action types from those thunks:
const thunkA = createAsyncThunk<string>('a', () => 'result')
const thunkB = createAsyncThunk<string>('b', () => 'result')
// later
createSlice({
name,
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addMatcher(isFulfilled(thunkA, thunkC), reducer);
}
})
They can also be used as standard TS type guards:
if (isFulfilled(action)) {
// TS will narrow the type of `action` to match the standard "fulfilled" fields
}
createAsyncThunk
Improvements
We've fixed a bug in the unwrapResult
utility where it didn't correctly handle use of rejectWithValue()
in the thunk. It will now correctly re-throw the value passed into rejectWithValue()
.
The auto-generated requestId
is now attached to the promise returned when the thunk is dispatched, as is the arg
that was passed into the thunk when it was dispatched.
We've added a new serializeError
callback option to createAsyncThunk
. By default, createAsyncThunk
will convert thrown errors into a serializable equivalent format, but you can override the serialization and provide your own callback if desired.
Draft-Safe Selectors
Memoized selectors created with Reselect's createSelector
don't work well with Immer's Proxy-wrapped draft states, because the selectors typically think the Proxy object is the same reference and don't correctly recalculate results as needed.
We've added a createDraftSafeSelector
API that lightly wraps createSelector
by checking if the initial argument (usually state
) is actually an Immer draft value, and if so, calling current(state)
to produce a new JS object. This forces the selector to recalculate the results.
We've also updated createEntityAdapter
's getSelectors
API to use these draft-safe selectors.
In general, using selectors inside of reducers is an unnecessary abstraction - it's fine to access data like state.entities[id].value = 123
. However, a number of users have expressed an interest in doing so, so we've made these changes to help accommodate that.
Other Changes
We now export our internal isPlainObject
implementation.
If an Immer-powered reducer has null
as its value, returning undefined
is now accepted.
TS types for case reducers have been tweaked to allow returning Draft<T>
to handle an edge case with generics.
The TS types for the devTools.serialize
option in configureStore
have been updated to correctly match the actual values.
The RTK docs now use a custom Remark plugin created by @phryneas, which allows us to write real TS code for code blocks, compile it to verify it type-checks correctly, and generate a plain JS version of that exact same example, with the TS and JS variations viewable in tabs for each code block.
You can see that in action in pages like the createSlice
API docs:
https://redux-toolkit.js.org/api/createSlice
Changelog
- allow to return undefined for null-states in caseReducers (#631 - @phryneas)
- fix
serialize
types (#752 - @blaiz) - Allow CaseReducers to also return
Draft<T>
(#756 - @phryneas) - Add
requestId
property to the promise returned bycreateAsyncThunk
(#784 - @phryneas) - Export isPlainObject (#795 - @msutkowski)
- fix unwrapResult behavior (#704 - @phryneas)
- isAnyOf/isAllOf HOFs for simple matchers (#788 - @douglas-treadwell)
- AsyncThunk matchers (#807 - @douglas-treadwell)
- Update Immer to 8.x (#821 - @markerikson)
- [entityAdapter] handle draft states in selectors (#815 - @phryneas)
- cAT: serializeError option (#812 - @phryneas)