Skip to content

Releases: reduxjs/redux-toolkit

v1.3.0-alpha.9

21 Feb 22:48
Compare
Choose a tag to compare
v1.3.0-alpha.9 Pre-release
Pre-release

This release reduces bundle sizes by forking and inlining the redux-immutable-state-invariant and redux-devtools-extension dependencies, and modifies the types for createEntityAdapter.

Changes

Dependency Updates and Bundle Size

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.

As a future note, Immer is currently being reworked to enable better shakeability, and once that is released, we plan on updating RTK to use the new Immer version as well. This is unlikely to make it into RTK 1.3.0, but we'll put out another update once we've been able to confirm that the changes work for us.

createEntityAdapter Types

We made a few more tweaks to the type definitions for createEntityAdapter. Shouldn't be anything breaking, but clarifying the possible overloads for the generated methods.

Changelog

v1.3.0-alpha.8...v1.3.0-alpha.9

v1.3.0-alpha.8

19 Feb 05:19
Compare
Choose a tag to compare
v1.3.0-alpha.8 Pre-release
Pre-release

This release fixes a couple edge case bugs with entity update operations, and documents expected update behavior. Also, since the new APIs look to be somewhat stabilized, we've merged the original PR into a v1.3.0 tracking branch where we can integrate additional planned changes.

Roadmap

We've put up a v1.3.0 roadmap issue to track other planned work before 1.3.0 is released.

Changes

createEntityAdapter Update Logic

We identified a couple potential bugs and edge cases inside the updateMany method implementation. We've fixed a potential issue that might have appeared when multiple updates were passed in attempting to rename the same entity to different IDs in a row, fixed a wrong type definition for comparer callbacks, and documented expected behavior when ID renames do occur.

Documentation

The alpha docs are available at https://deploy-preview-374--redux-starter-kit-docs.netlify.com/. In particular, see the API references for:

Changelog

v1.2.5...v1.3.0-alpha.8

v1.3.0-alpha.7

18 Feb 02:48
Compare
Choose a tag to compare
v1.3.0-alpha.7 Pre-release
Pre-release

This release reworks the cancellation functionality in createAsyncThunk.

Changes

createAsyncThunk cancellation

We previously added AbortController support to createAsyncThunk, to allow other code to trigger whatever cancellation might be applicable to the async logic inside.

The thunk now exits early when the abort() method is called and doesn't keep waiting for the payloadCreator to finish. This should be an improvement especially in cases where the payloadCreator didn't care about the abort signal, but something outside was awaiting the promise from dispatch.

Also, before this, a non-signal-aware payloadCreator could still finish which would have caused a dispatch of a "fulfilled" action after an "rejected" (abort) action, which was confusing.

We've also removed the meta.abortReason property as it's no longer possible to override the error in the "rejected" action.

Changelog

v1.3.0-alpha.6...v1.3.0-alpha.7

v1.3.0-alpha.6

17 Feb 06:12
Compare
Choose a tag to compare
v1.3.0-alpha.6 Pre-release
Pre-release

This release alters the internal behavior of the createEntityAdapter CRUD methods to allow them to work as "mutating helper functions" when not directly used as case reducers, and adds initial API reference documentation for createEntityAdapter.

Changes

createEntityAdapter and Immer

The original version of createEntityAdapter that we ported from the @ngrx/entity library used hand-written immutable update logic internally. We replaced that with use of Immer, as it made it consistent with how createReducer and createSlice work, and simplified some of the logic.

Previously, the CRUD methods such as addOne() always called Immer's createNextState() internally to wrap the updating logic. This worked okay when the CRUD method was used as a case reducer directly.

However, when called manually as a helper function inside an existing case reducer, the behavior was confusing. The case reducer had already received state as an Immer Draft value, but that draft was being passed into createNextState again. In theory, updates to the nested draft are supposed to propagate back to the parent draft, but we didn't see updates propagating upwards as expected.

We've updated the CRUD methods to first check whether the incoming state value is an Immer Draft, or just a plain JS value. If it's already a Draft, we pass that draft to the mutating internal update logic, so the changes are applied correctly. If it's a plain object, we still call createNextState() and return a plain value.

So, when using the CRUD methods as helper functions, treat them as mutating the existing state value:

const booksSlice = createSlice({
    name: "books",
    initialState: booksAdapter.getInitialState({totalBooks: 0),
    reducers: {
        bookAdded(state, action) {
            // Ignore return value here, and "mutate" state
            booksAdapter.addOne(state, action.payload.book)
            // can continue to mutate state here
            state.totalBooks++
        }
    }
})

Documentation

A first draft of API documentation for createEntityAdapter is now available:

createEntityAdapter draft API reference

Changelog

v1.3.0-alpha.5...v1.3.0-alpha.6

v1.3.0-alpha.5

16 Feb 18:42
Compare
Choose a tag to compare
v1.3.0-alpha.5 Pre-release
Pre-release

This release reworks the createAsyncThunk types to enable more flexibility in declaring optional generic arguments.

Changes

createAsyncThunk Generic Types

Per notes in alpha.4, our original types made it actually impossible to declare the correct State type for use by getState in your promise payload creator callback.

We came up with a stopgap solution, but since TS doesn't allow you to specify individual generic arguments by name, it meant that specifying types for some of the thunk options might require specifying all generic types whether you wanted to or not.

Fortunately, @Ethan-Arrowood has found a novel technique for optionally overriding specific generics by name, in a syntax similar to object destructuring, and we've been able to apply that here.

Per the previous example, the most common use case for createAsyncThunk still does not require specifying any types by hand, as the types for the Returned value and the thunkArg argument will be inferred from your payload creator callback:

// basic usage:
const thunk1 = createAsyncThunk("a", async (arg: string) => {
    return 42
})

To specify the type of State, you'll need to specifically declare the types of Returned and thunkArg. Then, pass in an object as the third generic argument, and declare the type of a field named state inside that object:

// specify state type for getState usage
const thunk2 = createAsyncThunk<Promise<number>, string, {state: RootState}>(
    "a",
    async (arg: string, {getState}) => {
        const state = getState();
        return 42;
    }
)

If you only want to declare the type of the extra thunk argument, do the same thing, but override the extra field instead of state:

interface UserAPI {
  fetchUserById: (id: number) => Promise<User>
}
// specify state type for getState usage
const thunk2 = createAsyncThunk<Promise<User>, string, {extra: UserAPI}>(
    "a",
    async (arg: string, {extra}) => {
        const user = await extra.fetchUserById(123)
        return user;
    }
)

Previously, that would have required also declaring the type of the state generic, since state was listed before extra in the generics definition.

Changelog

v1.3.0-alpha.4...v1.3.0-alpha.5

v1.3.0-alpha.4

16 Feb 06:43
Compare
Choose a tag to compare
v1.3.0-alpha.4 Pre-release
Pre-release

This alpha release rearranges the TS generic types of createAsyncThunk to fix broken usage.

Changes

createAsyncThunk Types

createAsyncThunk gives you access to getState in the thunkAPI object argument. However, we hadn't tried to exercise that yet in our tests, and it turns out there was no valid way to specify the correct type of the state returned by getState.

We've rearranged the generic types and tweaked the defaults. You should now be able to use it without specifying any generics for the most common case (returning a value, with a potential argument for the payload callback), and specify three types if you need to declare what the state type is:

// basic usage:
const thunk1 = createAsyncThunk("a", async (arg: string) => {
    return 42
})
// infers: return = Promise<number>, arg = string

// specify state type for getState usage
const thunk2 = createAsyncThunk<Promise<number>, string, RootState>(
    "a",
    async (arg: string, {getState}) => {
        const state = getState();
        return 42;
    }
)
// declared: return = Promise<number>, arg = string, state: RootState

We have some ideas for additional potential improvements to these types that may make usage simpler, so please keep an eye out for further alpha releases.

Documentation

A first draft of API documentation for createAsyncThunk is now available:

createAsyncThunk draft API reference

Changes

v1.3.0-alpha.3...v1.3.0-alpha.4

See PR #352: Port ngrx/entity and add createAsyncThunk for the complete alpha changes.

v1.3.0-alpha.3

15 Feb 23:23
Compare
Choose a tag to compare
v1.3.0-alpha.3 Pre-release
Pre-release

This alpha release alters the error-handling behavior of createAsyncThunk (again! 😁 )

Changes

createAsyncThunk Error Handling

In alpha.2, we tried having the thunk always re-throw caught errors. That didn't work out, because they always show up in the browser's console as unhandled exceptions.

Instead, the thunk now always returns a resolved promise containing the last action dispatched, which will be either the fulfilled action or the rejected action. We also export an unwrapAction utility that will either return action.payload if fulfilled or throw action.error if rejected, allowing the dispatching code to chain off the promise if desired:

import { unwrapResult } from '@reduxjs/toolkit'

// in the component
const onClick = () => {
  dispatch(fetchUserById(userId))
    .then(unwrapResult)
    .then(originalPromiseResult => {})
    .catch(serializedError => {})
}

Aborting Requests

Since createAsyncThunk accepts a promise callback, we have no way of knowing if or how the async logic can be canceled. However, we now provide a way for your logic to signal that cancellation needs to occur. An AbortController instance will be created for each request, and abort method will be attached to the returned promise that calls abortController.abort() and rejects the promise. The corresponding abortController.signal object will be passed in to your payload creator callback in the thunkAPI object as thunkAPI.signal, allowing your async logic to check if a cancellation has been requested.

Meta Field Names

The action.meta field in each action object previously looked like: {args, requestId}

The args field was renamed to arg to indicate it's only one value.

For the pending and fulfilled actions, action.meta is now: {arg, requestId}

The rejected action now includes details on whether the request was aborted, and
action.meta is now: {arg, requestId, aborted, abortReason}

Documentation

A first draft of API documentation for createAsyncThunk is now available:

createAsyncThunk draft API reference

Changes

v1.3.0-alpha.2...v1.3.0-alpha.3

See PR #352: Port ngrx/entity and add createAsyncThunk for the complete alpha changes.

v1.3.0-alpha.2

15 Feb 04:39
Compare
Choose a tag to compare
v1.3.0-alpha.2 Pre-release
Pre-release

This alpha release alters the error-handling behavior of createAsyncThunk.

Changes

createAsyncThunk Error Handling

createAsyncThunk catches rejected promises from the promise payload callback, and dispatches a "rejected" action in response with the error value. That means that the thunk itself always returns a resolved promise.

We had a request to re-throw errors from the thunk, in case the calling code wants to chain off the promise or handle it further, so we've implemented that.

We've also removed the "finished" action that was previously being dispatched in a finally {} clause at the end, as it shouldn't truly be necessary - app logic should just need to respond to either the "fulfilled" or "rejected" actions.

When an error is caught in the thunk, we try to put it into the "rejected" action. But, since JS Error objects aren't actually serializable, we now check for Error objects and serialize them into plain JS objects with any of the relevant fields ( {name?, message?, stack?, code?}), or just the value itself if it's not an Error. Unfortunately, since the err value in a catch(err) {} clause doesn't have a type, the action.error field will come through as a type of any either way.

Finally, we've reordered the logic inside to avoid cases where an error while dispatching "success" gets swallowed and triggers the "AJAX failed" handling.

v1.3.0-alpha.1...v1.3.0-alpha.2

v1.3.0-alpha.1

14 Feb 05:17
Compare
Choose a tag to compare
v1.3.0-alpha.1 Pre-release
Pre-release

This release makes several noticeable changes to the alpha createEntityAdapter and createAsyncThunk APIs that were introduced in v1.3.0-alpha.0.

Changes

createEntityAdapter

We made several changes to the type definitions for EntityAdapter and its related types:

  • Replaced separate overloads for handling string and number IDs with a single type EntityId = string | number type, and used that everywhere
  • Added EntityAdapter method type overloads for correct inference of PayloadAction<T> when passed directly as a case reducer inside of createSlice's reducers field
  • Removed the removeMany(Predicate) overload, as we discourage passing functions inside of actions

createAsyncThunk

Type Changes

The alpha.0 release was broken when used with TypeScript. If you tried to declare a promise payload callback that took no parameters, our initial types forced TS to assume that the actual thunk action creator took a single arg of type never, making it impossible to dispatch correctly.

The types that we started from also were overly complex in how they tried to infer arguments and the return value of the promise callback.

We've reworked the createAsyncThunk types to correctly allow declaring a promise callback with no arguments, and simplified the type inference when there are arguments.

Action Payload Changes

Previously, the result of the promise callback and the arguments to the thunk action creator were passed together in the payload, as payload: {result, args}

This makes it harder to combine the thunk lifecycle actions and the EntityAdapter reducers together, as the reducers expect the real contents as simply action.payload. So, we've altered the action definitions so that the fulfilled action has the promise result as its action.payload, the rejected action has the error as action.error, and the thunk arguments are now in action.meta.args for all action types.

In addition, we wanted to add some kind of unique request ID to each set of lifecycle actions, to help tie them together if necessary. We now generate a unique ID per call to the thunk, using nanoid, and include that value as action.meta.requestId in each action dispatched out of that thunk call. It will also be passed in the options object that is the second argument to the promise callback.

Since we don't have any formal documentation yet, here is the signature of the call to the promise payload creator callback:

const result = (await payloadCreator(args, {
  dispatch,
  getState,
  extra,
  requestId
} as TA)) as Returned

and here are the internal action definitions:

const fulfilled = createAction(
    type + '/fulfilled',
    (result: Returned, requestId: string, args: ActionParams) => {
      return {
        payload: result,
        meta: { args, requestId }
      }
    }
  )

  const pending = createAction(
    type + '/pending',
    (requestId: string, args: ActionParams) => {
      return {
        payload: undefined,
        meta: { args, requestId }
      }
    }
  )

  const finished = createAction(
    type + '/finished',
    (requestId: string, args: ActionParams) => {
      return {
        payload: undefined,
        meta: { args, requestId }
      }
    }
  )

  const rejected = createAction(
    type + '/rejected',
    (error: Error, requestId: string, args: ActionParams) => {
      return {
        payload: undefined,
        error,
        meta: { args, requestId }
      }
    }
  )

Removal of TS 3.3 and 3.4 from CI

We've been trying to keep our TS types compatible with multiple TS versions, from 3.3 onwards. We've had to do a number of type workarounds to keep 3.3 compatibility, and it's become painful to keep that going.

Other libraries such as Immer have already jumped up to only supporting TS 3.7+.

For now, we're removing TS 3.3 and 3.4 from our CI runs, and will likely stop supporting them in our library types when 1.3.0 is released.

Example

I've put together a small CodeSandbox that demonstrates use of createSlice, createEntityAdapter, and createAsyncThunk working together in a test:

Redux Toolkit v1.3.0-alpha.1 APIs example

Changes

See PR #352: Port ngrx/entity and add createAsyncThunk

v1.3.0-alpha.0...v1.3.0-alpha.1

v1.2.5

13 Feb 04:06
Compare
Choose a tag to compare

This release tweaks the type definitions to fix an error where meta and error could not be typed when using the prepare notation of createSlice.

Changelog

  • correctly enforce meta and error types as returned from prepare (@phryneas - #350)

v1.2.4...v1.2.5