Repository navigation
v2.14.0 #5568
markerikson
announced in
Announcements
v2.14.0
#5568
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
This feature release speeds up the dispatch path and RTK Query's
Apitype checking, shrinkscreateSlicein esbuild bundles, adds RTK-ownedDraftandPatchtypes so emitted declarations no longer reference Immer, addsremoveMany({ where })andselectByIdsto the entity adapter, adds RTK Query cache entry selectors and a DevTools action sanitizer, and fixes a very long list of RTK Query lifecycle, polling, upsert, timer, hook andfetchBaseQuerybugs.Note that we just announced our roadmap to Redux Toolkit 3.0, which includes both a large set of planned 2.x features and improvements, and planned 3.0 breaking changes. The fixes and updates in this release help pave the way for all of those efforts. We'd also appreciate feedback on the linked RTK Query usage patterns and 3.0 planned breaking changes discussion threads.
Changelog
Dispatch Performance
Every dispatched action runs through every reducer and middleware, so small costs on that path add up. We added a benchmark harness for the dispatch path and used it to find several places where RTK did work for actions it didn't care about.
createAction(...).matchnow comparesaction.typebefore callingisAction, so a non-matching action costs a property read.createReducerno longer allocates arrays on every dispatch to collect case and matcher reducers. SincecreateSlicebuilds its reducer withcreateReducer, every slice reducer gets this improvement.The listener middleware used to copy its listener list and call every predicate for every action. It now keeps a snapshot that is rebuilt only when listeners are added or removed, and that snapshot knows which action types have
typeoractionCreatorlisteners. If an action's type isn't in that set and there are nomatcherorpredicatelisteners, the middleware skips the loop entirely.The RTK Query root reducer now returns the existing state for actions that none of its slices handle, instead of running all five slice reducers. It also skips rewriting a query's provided tags when the new tags are the same as the stored ones, so re-fetching a query that returns the same tags no longer creates new
providedstate.Some numbers from the PR benchmarks (Node 26, ops/sec unless noted):
actionCreatorlisteners, matching actioncreateSlice, 20 slices, unrelated actionThe listener middleware numbers are the biggest change, because the old cost grew with the number of listeners. Apps with a handful of listeners will see much smaller gains.
The RTK Query middleware also does less work keeping the store's
subscriptionsstate in sync (#5499). It used to JSON-copy the whole subscriptions object and diff it with Immer every 500ms while subscriptions were changing. It now records which cache keys changed and writes one patch per key. Keys that didn't change keep their object references, so the Redux DevTools no longer receive a full copy ofsubscriptionson every sync. Polls,refetch()calls and tag invalidation refetches don't add subscribers, so they no longer schedule a sync at all. The visible difference: a cache key that only ever hadsubscribe: falserequests no longer shows up as an empty{}insubscriptions.Smaller
createSliceBundles With esbuildcreateSlicereferencesasyncThunkCreatorso thatcreate.asyncThunkworks in thereducerscallback. esbuild couldn't tell that theasyncThunkCreatorobject was free of side effects, so it always bundledcreateAsyncThunkalong withcreateSlice. webpack already dropped it. We've marked it as pure, and an esbuild bundle that imports onlycreateSliceis now about 1 kB smaller (brotli).Faster RTK Query
ApiType CheckingThe
Apitype combines every registered module (core, React hooks, and any third-party modules) by runningUnionToIntersectionover the module names. TanStack Table hit the same cost with the same design, and found that writing out the known intersection and adding variance annotations helped a lot.We've done the same thing here:
Apiuses the core module type directly, andApiModuleshasin outvariance annotations. On a test app with one ~50-endpointcreateApi, type instantiations dropped 22% and check time dropped about 25% on TS 6, and about 18% on the TS 7 native preview. Third-party modules likengrx-rtk-querysaw 7-8% fewer instantiations. There are no runtime changes.RTK-Owned
DraftandPatchTypesRTK used to re-export Immer's
Drafttype, and used Immer's types in signatures likeCaseReducerandPatchCollection. That meant the.d.tsfiles TypeScript generated for your slices referencedimmer. With a package manager layout whereimmerisn't hoisted, those declarations failed to build with TS2883 (#5516). Re-exporting doesn't fix that, so RTK now declares its ownDraft,WritableDraftandPatchtypes with the same shapes as Immer's. Emitted declarations now point at@reduxjs/toolkitinstead. Our type tests check that RTK'sDraftand Immer'sDraftstay interchangeable, so code that mixescastDraftorcurrentwith RTK's types keeps working. We also bumped theimmerdependency to^11.2.9.There are a few other type additions in this release:
ApiRootStatereplaces RTK Query'sRootStatetype, which is now deprecated.RootStatewill be removed in 3.0.TypedAddListenerActionandTypedRemoveListenerActiondescribe the actions thataddListenerandremoveListeneractually dispatch. The default payload type ofTypedAddListenerandTypedRemoveListeneris now the listener options object, soaddListener.match(action)no longer claimsaction.payloadhas fields likeidandunsubscribethat don't exist at runtime.dispatch(removeListener(...))is typed as returningbooleananddispatch(clearAllListeners())as returningvoid, matching the runtime.UseQueryStateDefaultResultandUseInfiniteQueryStateDefaultResultare exported from@reduxjs/toolkit/query/react, for wrapping hooks with a genericselectFromResult.RTK Query Cache Entry Selectors
We've had requests for a way to list everything in the cache for an endpoint (#2995).
api.util.selectEntriesForEndpoint(state, endpointName)returns all cache entries for one query or infinite query endpoint, andapi.util.selectAllQueryEntries(state)returns them for every endpoint. Each item has the entry's fields plus itsqueryCacheKey, and is typed for its endpoint. Both selectors are memoized. There's also a new exportedQueryCacheEntry<Definition>type.DevTools Action Sanitizer
All RTK Query requests use the same few action types, so the Redux DevTools show a long list of
api/executeQuery/fulfilledactions, and you have to open each one to see which endpoint it was for. The newcreateApiActionSanitizer(api)returns a function for the DevToolsactionSanitizeroption that renames those actions for display, likeapi/executeQuery/fulfilled (getPosts). The dispatched actions aren't modified.See the
createApiActionSanitizerdocs for combining sanitizers for multiple APIs.Entity Adapter Additions
adapter.removeManynow accepts a{ where }object, and removes every entity whose fields match all the given values (#5524):The match object is typed as
Partial<T>, and values are compared with===. Awhereobject with no keys removes nothing, so a filter built at runtime that ends up empty can't clear the collection by accident. UseremoveAllfor that. There's also a new exportedRemoveManyFilter<T>type.getSelectors()now includesselectByIds(state, ids), which returns the entities for the given ids in the order ofids, skipping ids that don't exist.removeManyis also faster. It used to filter the Immer draft of theidsarray, which reads every element through the draft proxy. It now filters a plain copy against aSetof removed ids.RTK Query Fixes
Lifecycles:
cacheDataLoadednow only resolves for a response from a request that started while that lifecycle was waiting. A late response from before aremoveQueryResultno longer resolves the next lifecycle on the same key with discarded data. Re-triggering a mutation with the samefixedCacheKeynow ends the previousonCacheEntryAddedlifecycle, so itscacheEntryRemovedcleanup runs instead of leaking. If that earlier trigger is still in flight, itscacheDataLoadednow rejects instead of never settling. Thanks to @hamarshehmhmd for the analysis in #5539.getCacheEntry()in a mutation'sonQueryStartednow finds the entry forfixedCacheKeymutations instead of always returninguninitialized.Synchronous throws from
onQueryStartedandonCacheEntryAddedare now caught and logged withconsole.error. Before, a sync throw inonQueryStartedescaped thependingdispatch and failed the request without ever callingqueryFn. We also added a dev warning whenupdateCachedDatais called before the entry has any data, since that call does nothing.Polling: Polling now stops after the last unsubscribe even if the subscription cleanup hasn't run yet (thanks to @LeulTew for the original fix in #5444). A due poll re-reads
skipPollingIfUnfocusedinstead of using the value from when the timer was set, and skips the poll if a request for that entry is already pending.pollingIntervalvalues above 2^31-1 ms are now clamped with a dev warning, since larger values overflowsetTimeoutand fire after 1ms. Removing a cache entry withremoveQueryResultnow stops its poll timer right away, and switching a subscriber to a slowerpollingIntervalnow reschedules the pending poll instead of letting the old, faster one fire once more. We also fixed a stale internal poll record that could blockupdatePollingInterval, andfetchBaseQuerynow clears itstimeouttimer when the request finishes.Upserts: A failed
upsertQueryDataover an existing entry (for example, from aresponseSchemavalidation error) no longer leaves that entry stuck inpending.upsertQueryEntriesnow skips bad entries one at a time instead of failing the whole batch: unknown or mutation endpoint names and throwingprovidesTagscallbacks are skipped, with dev warnings.Timers and multiple stores:
resetApiStatenow clears the subscription sync timer and the state for delayed tag invalidation, so work scheduled before the reset can't fire afterward. AkeepUnusedDataForremoval timer is now cleared when the key gets a new subscriber or is removed withremoveQueryResult, so an old timer can't remove a newer entry for the same key. Repeatedinitiate(arg, { subscribe: false })calls that hit the cache no longer push back thekeepUnusedDataForremoval, which could keep unused data around indefinitely (#4480, thanks @veksa!).setupListenersnow works with more than one store. Before, only the first store got focus and online events.Hooks: A query that failed before it ever got data now reports
isLoading: truewhen it refetches, instead of havingisLoading,isErroranddataall falsy while the request is pending (#4449, thanks @AnimeshPandey!). This undoes a change from 2.2.4 (#4364) that keptisLoadingfalse when a second hook subscribed to an errored entry. If a parent rendersnullwhileisLoadingis true and a child mounts the same query, the parent now unmounts the child on each refetch, and the two can loop. Render the child based ondataorisErrorinstead. Query args containingNaNno longer cause an endless refetch loop when the query errors. This was a regression from 2.9.0, where arg comparison treatedNaNas never equal to itself (#5338, thanks @aagostino31!).fetchNextPage()andfetchPreviousPage()no longer send a request when the infinite query hook's arg isskipToken, andinitiate(skipToken)no longer sends a request for any query or infinite query endpoint (#5028, thanks @haigou-web!).fetchBaseQuery: Anullbody no longer throwsCannot read properties of nullbefore the request is sent (thanks @dfedoryshchev!). An explicitcontent-typeheader is now kept forArrayBuffer, typed array andBlobbodies, which broke presigned S3 and GCS uploads. It's still removed forFormDataand for a JSON content type on a non-JSON body, and that JSON content type check is now case-insensitive (thanks @tad-bit-better!). A customresponseHandlerthat throws or rejects with a falsy value likeundefinednow produces aPARSING_ERROR, instead of being treated as a successful response (#5480, thanks @JunaidAhamed-7777!). Atimeoutthat fires while the response body is being read is now reported asTIMEOUT_ERRORinstead ofPARSING_ERROR(thanks @Gurmehar-git!).Other RTKQ fixes: An overlapping refetch now stays registered as the running query, so later calls join it and aborting it works (thanks @tedy69!). Apps with many
createApimiddlewares could hit a "Maximum call stack size exceeded" error when the middlewares registered themselves on the first query. Registration no longer restarts dispatch from the start of the middleware chain (thanks @PabloJoan!).api.enhanceEndpoints({ endpoints })now logs an error in development when it names an endpoint that doesn't exist, which usually means a typo.RTK Query thunk actions now include
startedTimeStampand a per-storeseqcounter in thefulfilledand rejected-with-value meta, matching the request'spendingaction, so you can tell which request a response belongs to and in what order requests started. Rejections for aborted requests and unhandled errors don't include these fields yet.Other Fixes
The listener middleware's
take(),condition()anddelay()no longer leave their timers running after the listener is cancelled or the action arrives first.The
autoBatchEnhancerraffallback no longer throws acancelAnimationFrameReferenceError in test environments that delete window globals during teardown (thanks @msutkowski!).RTK Query hooks now log a dev warning when they render with a different store than the one they first subscribed to. If you swap the store passed to
<Provider>, give the Provider akeythat changes with the store so the tree remounts.The serializability check middleware now reports a circular reference in state or actions as a non-serializable value, instead of overflowing the stack (thanks @OskarEichler!).
@types/reactis now declared as an optional peer dependency. RTK's React entry points import React types, and with pnpm's isolated linker or a global virtual store, TypeScript couldn't find them and those types becameany(thanks @unrevised6419!).What's Changed
cancelAnimationFrameReferenceError in the autoBatch raf fallback by @msutkowski in fix(toolkit):cancelAnimationFrameReferenceError in the autoBatch raf fallback #5462take/delaytimer leak and documentisAnyOfmatcher inference by @markerikson in Fix listenertake/delaytimer leak and documentisAnyOfmatcher inference #5538removeManyin the entity adapter by @markerikson in Speed upremoveManyin the entity adapter #5546pendingafter failedupsertQueryData, and skip badupsertQueryEntriesentries by @markerikson in Fix stuckpendingafter failedupsertQueryData, and skip badupsertQueryEntriesentries #5542cacheDataLoaded,fixedCacheKey,getCacheEntry, and sync throws in RTKQ lifecycles by @markerikson in Fix stalecacheDataLoaded,fixedCacheKey,getCacheEntry, and sync throws in RTKQ lifecycles #5543setupListenerswith multiple stores by @markerikson in Fix RTKQ middleware timer cleanup andsetupListenerswith multiple stores #5545Apitype checking by writing out the core module by @markerikson in Speed up RTKQApitype checking by writing out the core module #5547nullbody as jsonifiable infetchBaseQueryby @dfedoryshchev in fix(toolkit): don't treat anullbody as jsonifiable infetchBaseQuery#5450keepUnusedDataForon condition-skipped queries by @veksa in fix(query): don't resetkeepUnusedDataForon condition-skipped queries #5343fetchBaseQueryby @tad-bit-better in Keep explicit content-type for binary bodies infetchBaseQuery#5479TIMEOUT_ERRORby @Gurmehar-git in fix(toolkit): report response body timeouts asTIMEOUT_ERROR#5482NaNby @aagostino31 in fix(toolkit): prevent infinite refetch loop when query args containNaN#5339isLoadingstayingfalsewhen refetching after an error with no data by @AnimeshPandey in FixisLoadingstayingfalsewhen refetching after an error with no data #5470skipTokenin infinite query triggers by @haigou-web in fix(toolkit): respectskipTokenin infinite query triggers #5464Full Changelog: v2.13.0...v2.14.0
This discussion was created from the release v2.14.0.
All reactions