Releases: reduxjs/redux-toolkit
v1.5.0
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)
v1.4.0
This release updates Immer to v7.x, adds new options for defining "matcher" and "default case" reducers, and adds a new option for adding middleware to the store.
Changes
Immer v7
Immer recently released v7.0.0. The main feature is a new current
API, which takes a Proxy
-wrapped draft value and returns a finalized snapshot of the draft at that point in time.
Logging draft states in RTK reducers has always been difficult, as browsers display proxies in a hard-to-read format. This utility allows a return to logging partially-updated data to see the results, like console.log(current(state))
.
We've updated our Immer dependency to v7.x, and now export current
as part of our public API.
"Matcher" and "Default Case" Reducers
createReducer
has always been designed to handle exact action types. Both the object form and "builder callback" form let you specify a specific action type string to match against.
This is by far the most common use case, but we didn't previously have a way to handle matching a range of possible action types based on some criteria. We also had some requests to add some kind of a "default case" handler, similar to the default
keyword in a switch
statement.
The builder callback form of createReducer
now supports two new methods in addition to the existing builder.addCase
method:
builder.addMatcher
accepts a predicate function that looks like(action: Action) => boolean
, and a case reducer. If the matcher returnstrue
, the case reducer will run. Multiple matchers may be added tocreateReducer
, and all matchers that returntrue
will run in the order they were defined after any exact case match has run.builder.addDefaultCase
will run a reducer if no other cases or matchers have run.
Example:
const increment = createAction('increment')
const decrement = createAction('decrement')
createReducer(0, builder =>
builder
.addCase(increment, (state, action) => {
// action is inferred correctly here
})
// You can chain calls, or have separate `builder.addCase()` lines each time
.addCase(decrement, (state, action) => {})
// You can match a range of action types
.addMatcher(
action => action.endsWith('rejected'),
(state, action) => {}
)
// and provide a default case if no other handlers matched
.addDefaultCase((state, action) => {})
)
The new builder methods work the same way in the extraReducers
field of createSlice
as well.
Middleware Creation
We already export getDefaultMiddleware
to allow you to customize the middleware setup when creating the store. However, running [...getDefaultMiddleware, otherMiddleware]
often loses type information when used with TypeScript, and middleware types may need to reference the root state type as well.
The middleware
option for configureStore
can now be a callback function that receives a strongly-typed version of getDefaultMiddleware
as an argument, and should return the final middleware array. It also includes strongly-typed concat
and prepend
methods that preserve type information better than spreading arrays:
const store = configureStore({
reducer: rootReducer,
middleware: getDefaultMiddleware => getDefaultMiddleware().concat(logger)
})
Bug Fixes
createAsyncThunk
could sometimes skip handling a promise under certain conditions, causing an "Uncaught Promise" warning.- The
updateMany
CRUD method ofcreateEntityAdapter
wasn't properly merging together multiple changes for the same item ID
Typing Updates
The condition
argument for createAsyncThunk
is now defined as returning boolean | undefined
. The actual code explicitly checks if condition() !== false
, so returning undefined
is legal, but the typing was too strict.
We now export the return type for createAsyncThunk
for reuse.
Docs Updates
We've extensively updated the createReducer
and createSlice
pages to cover the new builder methods, and configureStore
and getDefaultMiddleware
to cover the new middleware syntax.
We've extracted the info on the immutability and serializability dev check middleware into their own separate API reference pages.
We've added a Usage Guide section on handling "non-serializable value" warnings, including examples of configuration for use with Redux-Persist and React-Redux-Firebase. Related to that, the serializability warning message now includes a link to this section.
The API reference section now has subcategories for "Store Setup", "Reducers and Actions", and "Other".
We've enabled Dark Mode for Docusaurus, including tweaks to colors for better contrast.
Changes
- Update docs on serializability usage and dev check middleware (@markerikson - #630)
- add
addMatcher
to builder notations &actionMatchers
argumen… (@phryneas - #610) - Add styling for blockquotes on darkmode (@msutkowski - #615)
- Add section to usage guide on working with non-serializable data (@cloeper - #623)
- Fixed multiple updates with same id in updateMany (@jakeboone02 - #621)
- Bump immer to v7, export current util, add usage docs (@mutkowksi - #604)
- docs: implement dark mode (@sreetamdas - #575)
- Export return type of createAsyncThunk (@smrq - #574)
- Prevent unhandled promises in createAsyncThunk (@msutkowski - #570)
- Adding correctly typed
prepend` and
concat` to the array… (@phryneas - #559) - add middlewareBuilder notation with type-curried arguments to co… (@phryneas - #549)
- Allow undefined return type for createAsyncThunk options (@melanieseltzer - #595)
v1.3.6
This release fixes a couple edge cases with Immer usage and reducers, and exposes the typePrefix
field from thunks generated by createAsyncThunk
.
Changes
Immer Reducer Fixes
The createEntityAdapter
CRUD methods can be used as either standalone reducers (in which case they call createNextState()
internally) or "mutating" helper functions if given an existing Immer Draft
value. However, createReducer
always assumed you were using the reducer standalone.
If you were trying to wrap createReducer
and pass in a Draft
value, changes inside wouldn't be reflected in the external Draft
. We've updated createReducer
to check if the incoming state value is actually a `Draft.
Also, the removeAll
CRUD method from createEntityAdapter
wasn't working correctly when used as a mutating helper, for similar reasons. We've tweaked the logic there to work right.
Thunk Type Prefix
createAsyncThunk
accepts a typePrefix
string as its first argument, and uses that to generate the pending/fulfilled/rejected
action types it dispatches. We had some requests to expose that type string for later usage, so the thunk function now has a thunk.typePrefix
field containing that string.
Changelog
v1.3.5
This release adds the ability to cancel async thunks before execution, improves TS thunk types, and fixes a broken middleware option name change.
Changes
Async Thunk Cancellation
The createAsyncThunk
API already had support for signaling cancellation of in-progress requests via an AbortController
. However, there was no way to skip executing the payload creator callback itself, or skip dispatching the pending
action.
We've added a condition
option to createAsyncThunk
that allows you to run checks before the payload creator is executed, and bail out of the thunk entirely if desired by returning false
:
const fetchUserById = createAsyncThunk(
'users/fetchByIdStatus',
async (userId, thunkAPI) => {
const response = await userAPI.fetchById(userId)
return response.data
},
{
condition: (userId, { getState, extra }) => {
const { users } = getState()
const fetchStatus = users.requests[userId]
if (fetchStatus === 'fulfilled' || fetchStatus === 'loading') {
// Already fetched or in progress, don't need to re-fetch
return false
}
}
}
)
Thunk Typing Improvements
We've updated the createAsyncThunk
TS types to fix an issue when there is no thunk argument, and to make it easier to wrap createAsyncThunk
in your own abstractions. See #486 / #489 for the issues and #502 / #512 for the updates.
Immutable Middleware Options
When we inlined the immutable check middleware, we ended up changing the ignore
option name to ignoredPaths
. We've added handling for ignore
for backwards compatibility just in case anyone was relying on that. We've also better documented the options for the serializable check middleware. See #491, #492, and #510 .
Changelog
- allow to skip AsyncThunks using a condition callback (@phryneas - #513)
- payloadCreator arg argument => asyncThunk arg type (@phryneas - #502)
- export AsyncThunkPayloadCreator type (@phryneas - #512)
- Update docs, add test, alias ignore->ignoredPaths (@msutkowski - #492)
- Add missing parameters to docs for serializable middleware (@msutkowski - #510)
v1.3.4
This release updates our internal nanoid
implementation, and exports it for general usage.
Changes
Export nanoid
The new createAsyncThunk
API we added in v1.3.0 auto-generates a unique request ID every time it's called, so that your reducers can distinguish between separate calls if necessary. To do this, we inlined a copy of the nanoid/non-secure
API into RTK.
The nanoid
library just released a new version, so we've updated our inlined copy to match the implementation of nanoid/non-secure
as of 3.0.2.
Since the API is already in the codebase, we've exported it publicly in case it's useful. Usage:
import { nanoid } from '@reduxjs/toolkit'
console.log(nanoid())
// 'dgPXxUz_6fWIQBD8XmiSy'
Changelog
- Update nanoid and export it (@markerikson - #481)
v1.3.3
This release improves serializability checking in actions, and exports additional types.
Changes
Action Serializability Checks
The serializability check middleware checks the contents of all dispatched actions. When we added createAsyncThunk
in 1.3, we tried to exclude the meta.args
path from those checks, because users may want to pass non-serializable values to their thunks, and the args are automatically added to the actions without the user explicitly putting them there.
However, the field name was changed from meta.args
to meta.arg
late in development, and the middleware wasn't updated to match, leading to some false positive warnings. We've fixed that, and added additional middleware options for ignoring paths in actions.
Type Exports
Per request, we've exported ThunkDispatch
from Redux Thunk, and the rest of the internal typedefs related to entities.
Changelog
v1.3.2
When we inlined the immutability check middleware in 1.3.0, we documented the createImmutableInvariantMiddleware
API, but forgot to export it. That's been fixed.
Changelog
v1.3.1
This release adds additional argument types for some createEntityAdapter
CRUD methods.
Changes
createEntityAdapter
Insertion APIs
createEntityAdapter
generates three methods that can insert entity objects: setAll
, addMany
, and upsertMany
. All three of them accept an array of entities.
We expect that a common use case will be to pre-normalize an API response using normalizr
, put the parsed entities into an action, and then handle action.payload.articles
in a reducer. However, in that case, action.payload.articles
is a pre-normalized object, not an array. While you could do articlesAdapter.addMany(state, Object.values(action.payload.articles))
, we decided to make those three methods accept a normalized object in addition to an array, allowing articlesAdapter.addMany(state, action.payload.articles)
to work correctly.
createEntityAdapter
Usage Guide Docs
We've also added usage guide examples for createEntityAdapter
as well.
Changelog
- Add basic createEntityAdapter usage docs (@msutkowski - #442)
- Add dictionary support to createEntityAdapter many methods (@msutkowski - #444)
v1.3.0
This release adds two new APIs: createEntityAdapter
to help manage normalized state, and createAsyncThunk
to abstract common data fetching behavior.
It also improves bundle size by inlining some of our prior dependencies and fixing cases where dev APIs were accidentally being included in production, as well as using a new version of Immer that tree-shakes better.
Finally, we've improved the developer experience by tweaking our TS typings for better inference and updating the dev check middleware to warn if checks are taking too much time.
New APIs
One of the primary goals for Redux Toolkit has always been to simplify common use cases and reduce "boilerplate" by providing APIs that can replace code you were previously writing out by hand.
To that end, v1.3.0 adds two new APIs for the common use cases of async data fetching and managing normalized data in the store.
createAsyncThunk
The Redux docs have taught that async logic should typically dispatch "three-phase async actions" while doing data fetching: a "start" action before the request is made so that loading UI can be displayed, and then a "success" or "failure" action to handle loading the data or showing an error message. Writing these extra action types is tedious, as is writing thunks that dispatch these actions and differ only by what the async request is.
Given that this is a very common pattern, we've added a createAsyncThunk
API that abstracts this out. It accepts a base action type string and a callback function that returns a Promise, which is primarily intended to be a function that does a data fetch and returns a Promise containing the results. It then auto-generates the request lifecycle action types / creators, and generates a thunk that dispatches those lifecycle actions and runs the fetching callback.
From there, you can listen for those generated action types in your reducers, and handle loading state as desired.
createEntityAdapter
The Redux docs have also advised storing data in a "normalized" state shape, which typically means keeping each type of item in a structure that looks like {ids: [], entities: {} }
. However, the Redux core provides no APIs to help manage storing and updating your data using this approach. Many community libraries exist, with varying tradeoffs, but so far we haven't officially recommended any of them.
Caching data is a hard problem, and not one that we are interested in trying to solve ourselves. However, given that we do recommend this specific pattern, and that Redux Toolkit is intended to help simplify common use cases, we want to provide a minimal set of functionality to help users manage normalized state.
To help solve this, we've specifically ported the @ngrx/entity
library to work with Redux Toolkit, with some modifications.
The core API function is createEntityAdapter
. It generates a set of reducer functions and selectors that know how to work with data that has been stored in that normalized {ids: [], entities: {} }
format, and can be customized by passing in a function that returns the ID field for a given item. If you want to keep the item IDs in a sorted order, a comparison function can also be passed in.
The returned EntityAdapter
object contains generated CRUD functions for manipulating items within that state, and generated selector functions that know how to read from that state. You can then use the generated CRUD functions and selectors within your own code.
There is one very important difference between RTK's implementation and the original @ngrx/entity
implementation. With @ngrx/entity
, methods like addOne(item, state)
accept the data argument first and the state second. With RTK, the argument order has been flipped, so that the methods look like addOne(state, item)
, and the methods can also accept a standard Redux Toolkit PayloadAction
containing the data as the second argument. This allows them to be used as Redux case reducers directly, such as passing them in the reducers
argument for createSlice
. They can also be used as "mutating" helper functions inside createReducer
and createSlice
as well, thanks to use of Immer internally.
Documentation
We've added new API reference and usage guide sections to the Redux Toolkit docs to cover these new APIs:
- Usage:
- API reference:
Bundle Size Improvements and Dependency Updates
Immer 6.0
Immer has always been the largest chunk of code added to your bundle from using RTK. Until now, RTK specifically depended on Immer 4.x, since 5.x added support for handling Map
s and Set
s (which aren't useful in a Redux app) and that support added to its bundle size.
Immer's code was written in a way that kept it from tree-shaking properly. Fortunately, Immer author Michel Weststrate put in some amazing work refactoring the code to better support tree-shaking, and his efforts are now available as Immer 6.0.
Per the Immer documentation on customizing Immer's capabilities, Immer now uses a plugin architecture internally, and additional functionality has to be explicitly enabled as an opt-in. There are currently three Immer plugins that can be enabled: ES5 support (for environments without ES6 Proxies), Map/Set
support, and JSON Patch support.
Redux Toolkit force-enables ES5 support. This is because we expect RTK to be used in multiple environments that do not support Proxies, such as Internet Explorer and React Native. It's also how Immer previously behaved, so we want to keep that behavior consistent and not break code given that this is a minor release of RTK. (In a hypothetical future major release, we may stop force-enabling the ES5 plugin and ask you to do it if necessary.)
Overall, this should drop a couple KB off your app's minified bundle size.
You may choose to enable the other plugins in your app code if that functionality is desired.
Store Configuration Dependencies
Since its creation, RTK has depended on leoasis/redux-immutable-state-invariant
to throw errors if accidental mutations are detected, and the zalmoxisus/redux-devtools-extension
NPM package to handle setup and configuration of the Redux DevTools Extension as the store is created.
Unfortunately, neither of these dependencies is currently published as ES Modules, and we recently found out that the immutable middleware was actually being included in production bundles despite our attempts to ensure it is excluded.
Given that the repo for the immutable middleware has had no activity in the last 3 years, we've opted to fork the package and include the code directly inside Redux Toolkit. We've also inlined the tiny-invariant
and json-stringify-safe
packages that the immutable middleware depended on.
The DevTools setup package, while tiny, suffers from the same issue, and so we've forked it as well.
Based on tests locally, these changes should reduce your production bundle sizes by roughly 2.5K minified.
During the development process, we found that the serializable invariant middleware was partly being included in production. We've decided that both the immutable and serializable middleware should always be no-ops in prod if they're ever included, both to ensure minimum bundle size, and to eliminate any unwanted slowdowns.
Other Changes
Type Inference Improvements
Users reported that it was possible to pass an entity adapter update method as a case reducer even if the slice state type didn't match what the update method expected (#434 ). We've updated the TS types to prevent that from being possible.
We've also had a number of cases where users had issues with the typings for action payloads depending on whether strictNullChecks: false
was set. We've altered our action creator types to improve that behavior.
Dev Check Middleware Timings
The immutability and serializability dev check middleware both do deep checks of state on every dispatch in dev mode. With a large state tree, this can sometimes noticeably slow down the app, and it's not immediately clear that the dev check middleware are responsible for this.
We've updated both middleware to record how much time is spent actually performing the state checks, and they will now log warning messages if the checks take too long to give you a heads-up that you might want to alter the middleware settings or disable them entirely. The delay is configurable, and defaults to 32ms (two UI frames).
In addition, the serializable middleware now ignores meta.args
in every action by default. This is because createAsyncThunk
automatically takes any arguments to its payload creator function and inserts them into dispatched actions. Since a user may be reasonably passing non-serializable values as arguments, and they're not intentionally inserting those...
v1.3.0-beta.1
This release improves type inference for action creators and for entity adapters that are used as reducers, adds warnings if dev check middleware are taking excessive amounts of time, and adds a new selectById
entity selector.
Changes
Type Inference Improvements
Users reported that it was possible to pass an entity adapter update method as a case reducer even if the slice state type didn't match what the update method expected (#434 ). We've updated the TS types to prevent that from being possible.
We've also had a number of cases where users had issues with the typings for action payloads depending on whether strictNullChecks: false
was set. We've altered our action creator types to improve that behavior.
Meanwhile, we've also re-exported the TS types from Reselect for convenience.
Dev Check Middleware Timings
The immutability and serializability dev check middleware both do deep checks of state on every dispatch in dev mode. With a large state tree, this can sometimes noticeably slow down the app, and it's not immediately clear that the dev check middleware are responsible for this.
We've updated both middleware to record how much time is spent actually performing the state checks, and they will now log warning messages if the checks take too long to give you a heads-up that you might want to alter the middleware settings or disable them entirely. The delay is configurable, and defaults to 32ms (two UI frames).
Entity Adapter API Changes
We've added a selectById
selector to createEntityAdapter
for convenience.
We've also removed the map()
update method, which accepted a callback that returned changes to be applied. It couldn't be used as a reducer (the action would be non-serializable), it's easily re-implemented in userland using the updateMany()
update method, and the name map
implied a potentially completely different return type of a non-mutating copy, which is not what it actually did.