yarn @reduxjs/toolkit 1.5.0
v1.5.0

latest releases: 2.2.3, 2.2.2, 2.2.1...
3 years ago

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

Don't miss a new toolkit release

NewReleases is sending notifications on new releases.