Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: add "merge" functionality #1059

Merged
merged 2 commits into from
Aug 14, 2022

Conversation

phryneas
Copy link
Member

This might be useful in "streaming data" scenarios where a .refresh call should not remove old data, but rather just merge current values into the existing dataset, e.g. as a "re-sync".

Not 100% sure about the usefulness though, hence the "RFC" in the title.

@codesandbox-ci
Copy link

codesandbox-ci bot commented May 16, 2021

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit fdb4ab6:

Sandbox Source
Vanilla Configuration
Vanilla Typescript Configuration
rsk-github-issues-example Configuration
@examples-query-react/basic Configuration
@examples-query-react/advanced Configuration
@examples-action-listener/counter Configuration

@netlify
Copy link

netlify bot commented May 16, 2021

Deploy Preview for redux-starter-kit-docs ready!

Name Link
🔨 Latest commit fdb4ab6
🔍 Latest deploy log https://app.netlify.com/sites/redux-starter-kit-docs/deploys/62f83cff9cac340008658ffa
😎 Deploy Preview https://deploy-preview-1059--redux-starter-kit-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site settings.

Base automatically changed from feature/v1.6-integration to master June 7, 2021 13:42
@aldreth
Copy link

aldreth commented Jul 7, 2021

@phryneas This is what I came to github to ask about! I currently use a thunk to merge new data into state & can't see a way of replicating that with RTK Query.

@phryneas
Copy link
Member Author

phryneas commented Jul 7, 2021

@aldreth Currently I'm at the point of "almost all usecases I can think of go against the idea of RTKQ itself and would probably be more dangerous that useful for almost everyone". But with enough good use cases I might change my mind. Can you elaborate a bit on your usecase?

@aldreth
Copy link

aldreth commented Jul 9, 2021

@phryneas I am basically showing a list of events. The API, which I don't control, doesn't return today's events. I save the redux state into local storage, so if a user has previously visited the site I can merge new events into state and still show events for today.
I can see that it's an edge case - the right thing to do would be to update the API, but it's an local government thing, so I have no control over it.

@phryneas
Copy link
Member Author

phryneas commented Jul 9, 2021

In that case I'd recommed to use an extraReducer in a slice you have control over to save that data to the side. You should not persist an RTKQ cache slice.

@ryota-murakami
Copy link
Contributor

Is this will become default strategy of refetch()?
Actually common use case doe't affect this change i think.

@phryneas
Copy link
Member Author

@ryota-murakami no, this will not be the default - the default will still be replacing the last result.
This will be an edge case feature that should be used with extreme care - for example in an endless scrolling situation it could easily lead to memory leaks if the user does not clean up old entries. The cache entry would just grow and grow.

Even for endless scrolling, it would still be advisable for almost all situations to use multiple useQuery hook calls, as that way RTK-Q can do the cleanup.

@ryota-murakami
Copy link
Contributor

@phryneas Thanks, that's make sense!

@markerikson
Copy link
Collaborator

markerikson commented Jun 23, 2022

Here's an example of where this might be useful, from Replay's codebase, along with the possibility of customizing cache key generation.

Replay stores info on "source actors". I'm still kinda hazy on the difference between a "source" and a "source actor", but we have a reducer that stores an entry per "source actor" along with its async request status, and that entry has a few different fields, including details on "breakable lines" and "hit counts per line":

https://github.com/replayio/devtools/blob/61525ea3f097926435d6d03f94a7c9895cca71a8/src/devtools/client/debugger/src/reducers/source-actors.ts#L30-L44

We fetch the basic data up front, and then fill in more of that detail later based on user interactions (hovering over a line number fetches hit counts, etc).

The current fetching logic is really ugly and uses an internal abstraction called a "memoizableAction":

https://github.com/replayio/devtools/blob/61525ea3f097926435d6d03f94a7c9895cca71a8/src/devtools/client/debugger/src/actions/source-actors.ts#L85-L119

and the corresponding reducer logic then has to figure out:

  • Do we have an entry for this ID?
  • what's its status?
  • okay, now use an "update resource" util to find that item and update these couple fields, including merging the hit counts data into an existing array:

https://github.com/replayio/devtools/blob/61525ea3f097926435d6d03f94a7c9895cca71a8/src/devtools/client/debugger/src/reducers/source-actors.ts#L195-L228

function updateBreakpointHitCounts(
  state: SourceActorsState,
  action: SetSourceActorBreakpointHitCountsAction
) {
  // @ts-ignore start/done mismatch
  const value = asyncActionAsValue<SetSourceActorBreakpointHitCountsAction["value"]>(action);
  if (value.state === "pending") {
    return state;
  }

  const { id: sourceId } = action;

  if (!hasResource(state, sourceId)) {
    return state;
  }

  const {
    breakpointHitCountLinesKnown: currentLinesKnown,
    breakpointHitCounts: currentBreakpointHitCounts,
  } = {
    ...state.values[sourceId],
  };

  return updateResources(state, [
    {
      breakpointHitCountLinesKnown: mergeRanges([
        ...(currentLinesKnown || []),
        { max: action.value.max, min: action.value.min },
      ]),
      breakpointHitCounts: [...(currentBreakpointHitCounts || []), ...action.value.hits],
      id: sourceId,
    },
  ]);
}

This all feels like code that could potentially be replaced by RTK Query endpoint handling... except that we don't have a way to merge in more data over time.

And, imagine that I do something like getSourceLineHits({sourceId, line: 123}), and then again with line: 456. I'd like them both to resolve to the same sourceId entry.

So that's where I imagine that a customizeCacheKey option would let me drop the line field from the cache key so that we're really just keying off sourceId, but still run the request, and then use merge to update the existing entry.

@markerikson markerikson force-pushed the feature/queryDefinition-merge branch from 7225646 to b4fa0fe Compare July 2, 2022 21:04
@markerikson
Copy link
Collaborator

I've rebased this PR and would like to finalize it. But, I don't think the behavior in this initial implementation is what we'd want.

Right now it's:

              const { merge = (x: any) => x } = definitions[
                meta.arg.endpointName
              ] as QueryDefinition<any, any, any, any>
              substate.status = QueryStatus.fulfilled
              let newData = merge(payload, substate.data)

              substate.data =
                definitions[meta.arg.endpointName].structuralSharing ?? true
                  ? copyWithStructuralSharing(substate.data, newData)
                  : newData

If you're supplying a merge option, that means you're now trying to mutate substate.data. which means that at that point newData and structuralSharing are a moot point, right?

I feel like if you've supplied a merge callback, it's now your responsibility to do the update work, and we should make that mutually exclusive with the replacement or structural sharing behavior.

phryneas and others added 2 commits August 13, 2022 18:17
- Make `merge` an optional callback
- Flip args order to put `currentCacheData` first
- Use Immer to handle the "mutate or return" behavior
- Only do structural sharing if there's no `merge`
@markerikson markerikson changed the base branch from master to v1.9-integration August 14, 2022 00:09
@markerikson markerikson marked this pull request as ready for review August 14, 2022 00:09
@markerikson
Copy link
Collaborator

I successfully updated the logic to look like this:

if (merge) {
  if (substate.data !== undefined) {
    // There's existing cache data. Let the user merge it in themselves.
    // We're already inside an Immer-powered reducer, and the user could just mutate `substate.data`
    // themselves inside of `merge()`. But, they might also want to return a new value.
    // Try to let Immer figure that part out, save the result, and assign it to `substate.data`.
    let newData = createNextState(
      substate.data,
      (draftSubstateData) => {
        // As usual with Immer, you can mutate _or_ return inside here, but not both
        return merge(draftSubstateData, payload)
      }
    )
    substate.data = newData
  } else {
    // Presumably a fresh request. Just cache the response data.
    substate.data = payload
  }
} else {
  // Assign or safely update the cache data.
  substate.data =
    definitions[meta.arg.endpointName].structuralSharing ?? true
      ? copyWithStructuralSharing(substate.data, payload)
      : payload
}

and added a couple tests.

This looks pretty good to me.

@markerikson markerikson merged commit aa7dafc into v1.9-integration Aug 14, 2022
// We're already inside an Immer-powered reducer, and the user could just mutate `substate.data`
// themselves inside of `merge()`. But, they might also want to return a new value.
// Try to let Immer figure that part out, save the result, and assign it to `substate.data`.
let newData = createNextState(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The nested createNextState is not causing any problems? I do remember something vaguely that this had caused problems in the past.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, Immer had some issues with nested drafts in earlier versions. That behavior has been made more consistent now. I did some testing here with both mutate and return to make sure they work, and also confirmed it throws if you mutate + return.

let newData = createNextState(
substate.data,
(draftSubstateData) => {
// As usual with Immer, you can mutate _or_ return inside here, but not both
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unnecessary return if you omit the method body. I don't know if minifiers would eliminate that, but why take the chance they don't? :)

}
)
substate.data = newData
} else {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this case could be skipped if you test for substate.data up there together with the if (merge) { - the fallthrough to copyWithStructuralSharing would be totally fine


/**
* Can be provided to merge the current cache value into the new cache value.
* If supplied, no automatic structural sharing will be applied - it's up to
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯 for mentioning this. I hope people read it.

@@ -29,6 +29,10 @@ const authSlice = createSlice({

const storeRef = setupApiStore(api, { auth: authSlice.reducer })

function delay(ms: number) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see waitForFakeTimer from helpers.ts

silasbw referenced this pull request in valora-inc/wallet Jan 10, 2023
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [@reduxjs/toolkit](https://redux-toolkit.js.org)
([source](https://github.com/reduxjs/redux-toolkit)) | [`^1.8.5` ->
`^1.9.1`](https://renovatebot.com/diffs/npm/@reduxjs%2ftoolkit/1.8.5/1.9.1)
|
[![age](https://badges.renovateapi.com/packages/npm/@reduxjs%2ftoolkit/1.9.1/age-slim)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://badges.renovateapi.com/packages/npm/@reduxjs%2ftoolkit/1.9.1/adoption-slim)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://badges.renovateapi.com/packages/npm/@reduxjs%2ftoolkit/1.9.1/compatibility-slim/1.8.5)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://badges.renovateapi.com/packages/npm/@reduxjs%2ftoolkit/1.9.1/confidence-slim/1.8.5)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>reduxjs/redux-toolkit</summary>

###
[`v1.9.1`](https://github.com/reduxjs/redux-toolkit/releases/tag/v1.9.1)

[Compare
Source](https://github.com/reduxjs/redux-toolkit/compare/v1.9.0...v1.9.1)

This bugfix release fixes assorted issues that were reported with RTK
1.9.0, and adds a few additional requested tweaks and improvements.

#### Changelog

##### Fixes

The `createAsyncThunk.withTypes` function was fully broken (it
type-checked correctly, but pointed to the wrong function due to a name
shadowing issue). That now works correctly.

The `maxRetries` option for RTKQ was inadvertently filtering out `0`
values, and those are now accepted.

`fulfillWithValue` had incorrect types that made it appear as if the
data was nested an additional level deeper. The types are now correct.

The `ActionCreatorWithoutPayload` type was tweaked to force an error
when an action creator is accidentally called with an argument, which
happens in cases like `onClick={todoAdded}`. This avoids accidentally
passing values like React event objects as the payload.

Timer handling for `batchActions` and `autoBatchEnhancer` now works in
more JS runtime environments.

##### Other Changes

The `TagDescription` type is now exported from RTKQ.

API endpoints now have a `.name` field containing the endpoint name,
such as `"getPokemon"`.

Calling `promise.abort()` on a `createAsyncThunk` promise before an
async `condition` resolves will now be treated as if the `condition`
itself returned `false`, bailing out and not dispatching anything.

The `merge` option now receives a third argument containing `{arg,
baseQueryMeta, fulfilledTimeStamp, requestId}`, in case that info is
useful in deciding how to merge.

The `@reduxjs/rtk-codemods` package has been updated to fix cases where
the `createSliceBuilder` codemod didn't preserve fields with function
variable arguments, like `[todoAdded]: adapter.addOne`. That package has
been updated to v0.0.3.

#### What's Changed

- fix createAsyncThunk.withTypes by
[@&#8203;phryneas](https://github.com/phryneas) in
[https://github.com/reduxjs/redux-toolkit/pull/2885](https://github.com/reduxjs/redux-toolkit/pull/2885)
- Update timer polyfills to work in more environments by
[@&#8203;markerikson](https://github.com/markerikson) in
[https://github.com/reduxjs/redux-toolkit/pull/2887](https://github.com/reduxjs/redux-toolkit/pull/2887)
- Retry now checks whether potential retry counts are undefined, rather
than boolean, in order to avoid filtering out 0's by
[@&#8203;OliverRadini](https://github.com/OliverRadini) in
[https://github.com/reduxjs/redux-toolkit/pull/2958](https://github.com/reduxjs/redux-toolkit/pull/2958)
- Fix multiple small issues with 1.9 by
[@&#8203;markerikson](https://github.com/markerikson) in
[https://github.com/reduxjs/redux-toolkit/pull/2964](https://github.com/reduxjs/redux-toolkit/pull/2964)
- fulfillWithValue should infer return value by
[@&#8203;phryneas](https://github.com/phryneas) in
[https://github.com/reduxjs/redux-toolkit/pull/2888](https://github.com/reduxjs/redux-toolkit/pull/2888)
- Fix Identifier/MemberExpression values in createSliceBuilder codemod
by [@&#8203;kyletsang](https://github.com/kyletsang) in
[https://github.com/reduxjs/redux-toolkit/pull/2881](https://github.com/reduxjs/redux-toolkit/pull/2881)
- Additional 1.9.1 fixes by
[@&#8203;markerikson](https://github.com/markerikson) in
[https://github.com/reduxjs/redux-toolkit/pull/2965](https://github.com/reduxjs/redux-toolkit/pull/2965)

**Full Changelog**:
reduxjs/redux-toolkit@v1.9.0...v1.9.1

###
[`v1.9.0`](https://github.com/reduxjs/redux-toolkit/releases/tag/v1.9.0)

[Compare
Source](https://github.com/reduxjs/redux-toolkit/compare/v1.8.6...v1.9.0)

This **feature release** adds several new options for RTK Query's
`createApi` and `fetchBaseQuery` APIs, adds a new `upsertQueryData`
util, rewrites RTKQ's internals for improved performance, adds a new
`autoBatchEnhancer`, deprecates the "object" syntax for `createReducer`
and `createSlice.extraReducers`, deprecates and *removes* broken utils
for getting running query promises, improves TS inference, exports
additional types, and fixes a number of reported issues.

```bash
npm i @&#8203;reduxjs/toolkit@latest

yarn add @&#8203;reduxjs/toolkit@latest
```

We plan to start work on [RTK
2.0](https://github.com/reduxjs/redux-toolkit/issues/958) in the next
few weeks. RTK 2.0 will focus on dropping legacy build compatibility and
deprecated APIs, with some potential new features. See the linked
discussion thread and give us feedback on ideas!

##### Deprecations and Removals

##### Object Argument for `createReducer` and
`createSlice.extraReducers`

RTK's `createReducer` API was originally designed to accept a lookup
table of action type strings to case reducers, like `{ "ADD_TODO" :
(state, action) => {} }`. We later added [the "builder callback" form]()
to allow more flexibility in adding "matchers" and a default handler,
and did the same for `createSlice.extraReducers`.

**We intend to remove the "object" form for both `createReducer` and
`createSlice.extraReducers` in RTK 2.0**. The builder callback form is
effectively the same number of lines of code, and works *much* better
with TypeScript.

Starting with this release, **RTK will print a one-time runtime warning
for both `createReducer` and `createSlice.extraReducers` if you pass in
an object argument**.

As an example, this:

```js
const todoAdded = createAction('todos/todoAdded');

createReducer(initialState, {
  [todoAdded]: (state, action) => {}
})

createSlice({
  name,
  initialState,
  reducers: {/* case reducers here */},
  extraReducers: {
    [todoAdded]: (state, action) => {}
  }
})
```

should be migrated to:

```js
createReducer(initialState, builder => {
  builder.addCase(todoAdded, (state, action) => {})
})

createSlice({
  name,
  initialState,
  reducers: {/* case reducers here */},
  extraReducers: builder => {
    builder.addCase(todoAdded, (state, action) => {})
  }
})
```

##### Codemods for Deprecated Object Reducer Syntax

To simplify upgrading codebases, we've published a set of codemods that
will automatically transform the deprecated "object" syntax into the
equivalent "builder" syntax.

The codemods package is available on NPM as
[**`@reduxjs/rtk-codemods`**](https://www.npmjs.com/package/@&#8203;reduxjs/rtk-codemods).
It currently contains two codemods: `createReducerBuilder` and
`createSliceBuilder`.

To run the codemods against your codebase, run `npx
@&#8203;reduxjs/rtk-codemods <TRANSFORM NAME> path/of/files/
or/some**/*glob.js`.

Examples:

```bash
npx @&#8203;reduxjs/rtk-codemods createReducerBuilder ./src

npx @&#8203;reduxjs/rtk-codemods createSliceBuilder ./packages/my-app/**/*.ts
```

We also recommend re-running Prettier on the codebase before committing
the changes.

**These codemods *should* work, but we would greatly appreciate testing
and feedback on more real-world codebases!**

<details>
  <summary>
    Object reducer codemod before/after examples
  </summary>
Before:

```js
createReducer(initialState, {
  [todoAdded1a]: (state, action) => {
    // stuff
  },
  [todoAdded1b]: (state, action) => action.payload,
});

const slice1 = createSlice({
  name: "a",
  initialState: {},
  extraReducers: {
    [todoAdded1a]: (state, action) => {
      // stuff
    },
    [todoAdded1b]: (state, action) => action.payload,
  }
})
```

After:

```js
createReducer(initialState, (builder) => {
  builder.addCase(todoAdded1a, (state, action) => {
    // stuff
  });

  builder.addCase(todoAdded1b, (state, action) => action.payload);
})

const slice1 = createSlice({
  name: "a",
  initialState: {},

  extraReducers: (builder) => {
    builder.addCase(todoAdded1a, (state, action) => {
      // stuff
    });

    builder.addCase(todoAdded1b, (state, action) => action.payload);
  }
})
```

</details>

##### `getRunningOperationPromises` Deprecation and Replacement

In
[`v1.7.0`](https://github.com/reduxjs/redux-toolkit/releases/tag/v1.7.0),
we added an `api.util.getRunningOperationPromises()` method for use with
SSR scenarios, as well as a singular `getRunningOperationPromise()`
method intended for possible use with React Suspense.

Unfortunately, in
[#&#8203;2477](https://github.com/reduxjs/redux-toolkit/issues/2477)
we realized that **both those methods have a fatal flaw - they do not
work with multiple stores in SSR**.

As of this release, **we are immediately marking
`getRunningOperationPromises()` as deprecated and discouraging its use
before we remove it completely in RTK 2.0!** It will **now throw both
runtime and compile errors in development** to enforce moving away from
using it. However, we are leaving its existing behavior in production
builds to avoid actual breakage.

The `getRunningOperationPromise()` util was experimental, and as far as
we can tell not actually being used by anyone, so **we are removing
`getRunningOperationPromise` completely in this release**.

As replacements, RTKQ now includes four new thunks attached to
`api.util`:

-   `getRunningQueryThunk(endpointName, queryArgs)`
-   `getRunningMutationThunk(endpointName, fixedCacheKeyOrRequestId)`
-   `getRunningQueriesThunk()`
-   `getRunningMutationsThunk()`

Usages would typically change like this:

```diff
-await Promise.all(api.util.getRunningOperationPromises())
+await Promise.all(dispatch(api.util.getRunningQueriesThunk()))
```

##### Changelog

##### New RTK Query `createApi` Options

`createApi` endpoints now have several additional options that can be
passed in, some of which are intended to work together.

##### `merge` Option

RTKQ was built around the assumption that the server is the source of
truth, and every refetch replaces the cached data on the client. There
*are* use cases when it would be useful to merge an incoming response
into the existing cached data instead, such as pagination or APIs that
return varying results over time.

Query endpoints can now accept a `merge(cachedData, responseData)`
callback that lets you do Immer-powered "mutations" to update the
existing cached data instead of replacing it entirely.

Since RTKQ assumes that each response per key should replace the
existing cache entry by default, the `merge` option is expected to be
used with the `serializeQueryArgs` and `forceRefetch` options, as
described below.

##### `serializeQueryArgs` Option

RTK Query always serializes the cache key value, and uses the string as
the actual key for storing the cache entry. The default serialization is
the name of the endpoint, plus either the primitive value or a
stable-serialized object. An example might be
`state.api.queries['getPokemon("pikachu")']`.

RTKQ already supported customization of this serialization behavior at
the `createApi` level. Now, each endpoint can specify its own
`serializeQueryArgs` method.

The per-endpoint `serializeQueryArgs` may return either a string, an
object, a number, or a boolean. If it's a string, that value will be
used as-is. Otherwise, the return value will be run through the default
serialization logic. This simplifies the common case of stripping out a
couple unwanted object fields from the cache key.

This option serves two main purposes: leaving out values that are passed
in to an endpoint but not really part of the "key" conceptually (like a
socket or client instance), and altering cache key behavior to use a
single entry for the endpoint (such as in an infinite loading /
pagination scenario).

Also, the `defaultSerializeQueryArgs` util is now exported.

```ts
    getPost: build.query<Post, { id: string; client: MyApiClient }>({
      queryFn: async ({ id, client }) => {
        const post = await client.fetchPost(id)
        return { data: post }
      },
      serializeQueryArgs: ({ queryArgs, endpointDefinition, endpointName }) => {
        const { id } = queryArgs
        // This can return a string, an object, a number, or a boolean.
        // If it returns an object, number or boolean, that value
        // will be serialized automatically via `defaultSerializeQueryArgs`
        return { id } // omit `client` from the cache key

        // Alternately, you can use `defaultSerializeQueryArgs`:
        // return defaultSerializeQueryArgs({
        //   endpointName,
        //   queryArgs: { id },
        //   endpointDefinition
        // })
        // Or  create and return a string yourself:
        // return `getPost(${id})`
      },
    }),
```

##### `forceRefresh` option

Sometimes you may want to force a refetch, even though RTKQ thinks that
the serialized query args haven't changed and there's already a
fulfilled cache entry.

This can be used to force RTKQ to actually refetch. One expected use
case is an "infinite pagination" scenario where there is *one* cache
entry for the endpoint, different page numbers are given as query args,
and the incoming responses are merged into the existing cache entry:

```ts
    listItems: build.query<string[], number>({
      query: (pageNumber) => `/listItems?page=${pageNumber}`,
      // Only have one cache entry because the arg always maps to one string
      serializeQueryArgs: ({ endpointName }) => {
        return endpointName
      },
      // Always merge incoming data to the cache entry
      merge: (currentCache, newItems) => {
        currentCache.push(...newItems)
      },
      // Refetch when the page arg changes
      forceRefetch({ currentArg, previousArg }) {
        return currentArg !== previousArg
      },
    }),
```

##### `transformErrorResponse` Option

Similar to `transformResponse`, endpoints can now specify a
`transformErrorResponse` option as well.

##### `upsertQueryData` Util

RTKQ already has an `updateQueryData` util to synchronously modify the
contents of an existing cache entry, but there was no way to create a
new cache entry and its metadata programmatically.

This release adds a new `api.util.upsertQueryData` API that allows
creating a cache entry + its data programmatically. As with the other
util methods, this is a thunk that should be dispatched, and you should
pass in the exact cache key arg and complete data value you want to
insert:

```ts
dispatch(
  api.util.upsertQueryData('post', '3', {
    id: '3',
    title: 'All about cheese.',
    contents: 'I love cheese!',
   })
)
```

The dispatch acts like all other RTKQ requests, so the process is async,
and the thunk returns a promise that resolves when the upsert is
complete.

##### RTK Query Performance Improvements

We've significantly rewritten RTK Query's internal implementation to
improve performance, especially in cases where many components with
query hooks mount at the same time. The middleware has been "flattened"
and runs fewer internal checks against each action, subscription updates
are grouped together, and some unnecessary memoized selectors have been
removed. One consequence is that **forgetting to add the RTKQ middleware
now throws an *error* instead of logging a warning**.

Overall, RTK Query processing time should be noticeably faster than it
was in 1.8.

RTK Query also can take advantage of the new "auto-batch enhancer"
(described below) for some additional perf optimization, and **we
recommend adding that to your Redux store configuration**.

##### `fetchBaseQuery` Options

`fetchBaseQuery` has several new options for processing requests:

You can now specify a `timeout` option for both individual endpoints and
`fetchBaseQuery` . If provided, requests that take longer than this
value will automatically abort.

`fetchBaseQuery` now supports passing the `responseHandler` and
`validateStatus` options directly to `fetchBaseQuery` itself, in
addition to accepting it as part of specific endpoints. If provided,
these options will be applied as defaults to *all* requests for that
API, which simplifies using them on many endpoints. Providing them for
endpoints overrides the global option.

You can now specify a `jsonContentType` string that will be used to set
the `content-type` header for a request with a jsonifiable body that
does not have an explicit `content-type` header. Defaults to
`"application/json"`.

You can now specify a `isJsonContentType` callback that checks to see if
the request body or response body should be stringified. The default
looks for values like `"application/json"` and
`"application/vnd.api+json"`. You can also now specify `responseHandler:
'content-type'` to have RTKQ automatically check to see whether a
response should be treated as text or JSON.

The `prepareHeaders` method can now return void and does *not* have to
`return headers`.

##### Other RTK Query Changes

The `refetch()` methods now return a promise that can be awaited.

Query endpoints can now accept a `retryCondition` callback as an
alternative to `maxRetries`. If you provide `retryCondition`, it will be
called to determine if RTKQ should retry a failed request again.

##### New Auto-Batching Store Enhancer

There are [several different ways to "batch actions" with Redux
stores](https://blog.isquaredsoftware.com/2020/01/blogged-answers-redux-batching-techniques/),
ranging from reducers to debounced subscriber notifications.

RTK now includes a new `autoBatchEnhancer()` store enhancer that uses a
variation on the "debounced notification" approach, inspired by React's
technique of batching renders and determining if an update is
low-priority or high-priority.

The enhancer looks for any actions tagged with an
`action.meta[SHOULD_AUTOBATCH] = true` flag, and delays notifying
subscribers until a queued callback runs. This means that if multiple
"auto-batched" actions are dispatched in a row, there will be only one
subscriber notification. However, if any "normal-priority" action
without that flag are dispatched before the queued callback runs, the
enhancer will notify subscribers immediately instead and ignore the
callback.

This allows Redux users to selectively tag certain actions for effective
batching behavior, making this purely opt-in on a per-action basis,
while retaining normal notification behavior for all other actions.

The enhancer defaults to using `requestAnimationFrame`, but can also be
configured to use `queueMicrotask` to run at the end of an event loop
tick, `setTimeout`, or a user-provided callback.

RTK Query's internals have been updated to mark several key actions as
batchable. While the enhancer is purely opt-in, benchmarks indicate that
it can help speed up UI performance with RTK Query, especially when
rendering many components with query hooks. **We recommend adding this
enhancer to your store setup if you're using RTK Query**:

```ts
  const store = configureStore({
  reducer,
  enhancers: (existingEnhancers) => {
    // Add the autobatch enhancer to the store setup
    return existingEnhancers.concat(autoBatchEnhancer())
  },
})
```

Additionally, there's a `prepareAutoBatched` util that can be used to
help add the `SHOULD_AUTOBATCH` flag to actions, designed for use with
`createSlice`:

```ts
const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 } as CounterState,
  reducers: {
    incrementBatched: {
      // Batched, low-priority
      reducer(state) {
        state.value += 1
      },
      // Use the `prepareAutoBatched` utility to automatically
      // add the `action.meta[SHOULD_AUTOBATCH]` field the enhancer needs
      prepare: prepareAutoBatched<void>(),
    },
    // Not batched, normal priority
    decrementUnbatched(state) {
      state.value -= 1
    },
  },
})
```

##### TypeScript Improvements

RTK 1.9 **now requires TS 4.2 or greater**, and supports through TS 4.9.

The action type strings generated by `createAction` are now full TS
template literals when possible.

There's now a `createAsyncThunk.withTypes()` method that creates a
"pre-typed" version of `createAsyncThunk` with types like `{state,
dispatch, extra}` baked in. This can be used to simplify customizing
`createAsyncThunk` with the right types once during app setup.

RTK Query now exports TS types for "pre-typed hook results", for cases
when you want to wrap the query/mutation hooks in your own code.
Additionally, RTKQ also exports `BaseQueryApi` and exposes TS-only types
for endpoint definitions.

`configureStore` now correctly infers changes to the store shape from
any store enhancers, such as adding actual extra fields to `store`.

RTK now exports additional types from `redux-thunk`.

##### Bug Fixes

Manually initiated RTKQ promises should resolve correctly.

Previous API tags are removed before adding new ones, instead of
accidentally merging them together.

`invalidateTags` works correctly when dealing with persisted query
state.

##### What's Changed

- Add `isJsonContentType` predicate to `fetchBaseQuery` by
[@&#8203;msutkowski](https://github.com/msutkowski) in
[https://github.com/reduxjs/redux-toolkit/pull/2331](https://github.com/reduxjs/redux-toolkit/pull/2331)
- Add `jsonContentType` to `fetchBaseQuery` options by
[@&#8203;msutkowski](https://github.com/msutkowski) in
[https://github.com/reduxjs/redux-toolkit/pull/2403](https://github.com/reduxjs/redux-toolkit/pull/2403)
- Add ThunkMiddleware to the re-exported types from redux-thunk by
[@&#8203;orta](https://github.com/orta) in
[https://github.com/reduxjs/redux-toolkit/pull/2451](https://github.com/reduxjs/redux-toolkit/pull/2451)
- add timeout option to fetchBaseQuery by
[@&#8203;phryneas](https://github.com/phryneas) in
[https://github.com/reduxjs/redux-toolkit/pull/2143](https://github.com/reduxjs/redux-toolkit/pull/2143)
- createSlice: use template literal types for action type by
[@&#8203;phryneas](https://github.com/phryneas) in
[https://github.com/reduxjs/redux-toolkit/pull/2250](https://github.com/reduxjs/redux-toolkit/pull/2250)
- add types for manually typing hook results in userland code by
[@&#8203;phryneas](https://github.com/phryneas) in
[https://github.com/reduxjs/redux-toolkit/pull/2276](https://github.com/reduxjs/redux-toolkit/pull/2276)
- Add 'content-type' ResponseHandler by
[@&#8203;taylorkline](https://github.com/taylorkline) in
[https://github.com/reduxjs/redux-toolkit/pull/2363](https://github.com/reduxjs/redux-toolkit/pull/2363)
- feature : endpoint-specific args serializers by
[@&#8203;michal-kurz](https://github.com/michal-kurz) in
[https://github.com/reduxjs/redux-toolkit/pull/2493](https://github.com/reduxjs/redux-toolkit/pull/2493)
- RFC: add "merge" functionality by
[@&#8203;phryneas](https://github.com/phryneas) in
[https://github.com/reduxjs/redux-toolkit/pull/1059](https://github.com/reduxjs/redux-toolkit/pull/1059)
- Add runtime deprecation warning for reducer object notation by
[@&#8203;markerikson](https://github.com/markerikson) in
[https://github.com/reduxjs/redux-toolkit/pull/2591](https://github.com/reduxjs/redux-toolkit/pull/2591)
- feat: Add the ability to type `StoreEnhancers` by
[@&#8203;fostyfost](https://github.com/fostyfost) in
[https://github.com/reduxjs/redux-toolkit/pull/2550](https://github.com/reduxjs/redux-toolkit/pull/2550)
- Batch RTKQ "subscribe on reject" actions, and improve cache collection
timer handling by
[@&#8203;markerikson](https://github.com/markerikson) in
[https://github.com/reduxjs/redux-toolkit/pull/2599](https://github.com/reduxjs/redux-toolkit/pull/2599)
- add transformErrorReponse to rtkq endpoints by
[@&#8203;dreyks](https://github.com/dreyks) in
[https://github.com/reduxjs/redux-toolkit/pull/1841](https://github.com/reduxjs/redux-toolkit/pull/1841)
- Fix manually initiate()d rtk-query promises by
[@&#8203;wesen](https://github.com/wesen) in
[https://github.com/reduxjs/redux-toolkit/pull/2187](https://github.com/reduxjs/redux-toolkit/pull/2187)
- Implement codemods for createReducer and createSlice builder by
[@&#8203;markerikson](https://github.com/markerikson) in
[https://github.com/reduxjs/redux-toolkit/pull/2602](https://github.com/reduxjs/redux-toolkit/pull/2602)
- RFC: Expose endpoint types by
[@&#8203;phryneas](https://github.com/phryneas) in
[https://github.com/reduxjs/redux-toolkit/pull/1646](https://github.com/reduxjs/redux-toolkit/pull/1646)
- add createAsyncThunk.withTypes by
[@&#8203;phryneas](https://github.com/phryneas) in
[https://github.com/reduxjs/redux-toolkit/pull/2604](https://github.com/reduxjs/redux-toolkit/pull/2604)
- return promise from query result & hook `refetch` by
[@&#8203;phryneas](https://github.com/phryneas) in
[https://github.com/reduxjs/redux-toolkit/pull/2212](https://github.com/reduxjs/redux-toolkit/pull/2212)
- retry condition by http error response by
[@&#8203;kahirokunn](https://github.com/kahirokunn) in
[https://github.com/reduxjs/redux-toolkit/pull/2239](https://github.com/reduxjs/redux-toolkit/pull/2239)
- implemented upsertQueryData functionality per
[#&#8203;1720](https://github.com/reduxjs/redux-toolkit/issues/1720)
[#&#8203;2007](https://github.com/reduxjs/redux-toolkit/issues/2007)
by [@&#8203;barnabasJ](https://github.com/barnabasJ) in
[https://github.com/reduxjs/redux-toolkit/pull/2266](https://github.com/reduxjs/redux-toolkit/pull/2266)
- Consolidate RTKQ middleware to simplify stack size by
[@&#8203;markerikson](https://github.com/markerikson) in
[https://github.com/reduxjs/redux-toolkit/pull/2641](https://github.com/reduxjs/redux-toolkit/pull/2641)
- Try fixing createAsyncThunk issues with TS 4.8 by
[@&#8203;markerikson](https://github.com/markerikson) in
[https://github.com/reduxjs/redux-toolkit/pull/2643](https://github.com/reduxjs/redux-toolkit/pull/2643)
- Fix assorted issues with the RTKQ middleware refactor by
[@&#8203;markerikson](https://github.com/markerikson) in
[https://github.com/reduxjs/redux-toolkit/pull/2644](https://github.com/reduxjs/redux-toolkit/pull/2644)
- fix `upsertQueryData` race situations by
[@&#8203;phryneas](https://github.com/phryneas) in
[https://github.com/reduxjs/redux-toolkit/pull/2646](https://github.com/reduxjs/redux-toolkit/pull/2646)
- Fix upsert by [@&#8203;phryneas](https://github.com/phryneas) in
[https://github.com/reduxjs/redux-toolkit/pull/2669](https://github.com/reduxjs/redux-toolkit/pull/2669)
- Export the BaseQueryApi interface from rtk-query so it can be used as
a type in TypeScript without importing from dist/. by
[@&#8203;nsthorat](https://github.com/nsthorat) in
[https://github.com/reduxjs/redux-toolkit/pull/2740](https://github.com/reduxjs/redux-toolkit/pull/2740)
- Fix retryCondition `error`. by
[@&#8203;phryneas](https://github.com/phryneas) in
[https://github.com/reduxjs/redux-toolkit/pull/2743](https://github.com/reduxjs/redux-toolkit/pull/2743)
- fix: export ToolkitStore interface from configureStore by
[@&#8203;adamhari](https://github.com/adamhari) in
[https://github.com/reduxjs/redux-toolkit/pull/2750](https://github.com/reduxjs/redux-toolkit/pull/2750)
- reset `dispatchQueued` variable after flushing by
[@&#8203;phryneas](https://github.com/phryneas) in
[https://github.com/reduxjs/redux-toolkit/pull/2757](https://github.com/reduxjs/redux-toolkit/pull/2757)
- Add `forceRefetch` to `QueryExtraOptions` by
[@&#8203;schadenn](https://github.com/schadenn) in
[https://github.com/reduxjs/redux-toolkit/pull/2663](https://github.com/reduxjs/redux-toolkit/pull/2663)
- Speed up subscription behavior by tracking state in middleware by
[@&#8203;markerikson](https://github.com/markerikson) in
[https://github.com/reduxjs/redux-toolkit/pull/2759](https://github.com/reduxjs/redux-toolkit/pull/2759)
- `prepareHeaders` does not need to return `headers` any more by
[@&#8203;phryneas](https://github.com/phryneas) in
[https://github.com/reduxjs/redux-toolkit/pull/2775](https://github.com/reduxjs/redux-toolkit/pull/2775)
- Add `codemods-cli` and rewrite "object reducer" codemods by
[@&#8203;markerikson](https://github.com/markerikson) in
[https://github.com/reduxjs/redux-toolkit/pull/2768](https://github.com/reduxjs/redux-toolkit/pull/2768)
- remove alpha compatibility fallbacks by
[@&#8203;phryneas](https://github.com/phryneas) in
[https://github.com/reduxjs/redux-toolkit/pull/2777](https://github.com/reduxjs/redux-toolkit/pull/2777)
- fix `skipToken` behaviour in `useQueryState` by
[@&#8203;phryneas](https://github.com/phryneas) in
[https://github.com/reduxjs/redux-toolkit/pull/2779](https://github.com/reduxjs/redux-toolkit/pull/2779)
- `fetchBaseQuery`: allow `headers` option by
[@&#8203;phryneas](https://github.com/phryneas) in
[https://github.com/reduxjs/redux-toolkit/pull/2778](https://github.com/reduxjs/redux-toolkit/pull/2778)
- \[breaking fix] replace `getRunningOperationPromise(s)` with
`getRunning(Query|Queries|Mutation|Mutations)Thunk` by
[@&#8203;phryneas](https://github.com/phryneas) in
[https://github.com/reduxjs/redux-toolkit/pull/2481](https://github.com/reduxjs/redux-toolkit/pull/2481)
- General pre-1.9-beta cleanup by
[@&#8203;markerikson](https://github.com/markerikson) in
[https://github.com/reduxjs/redux-toolkit/pull/2789](https://github.com/reduxjs/redux-toolkit/pull/2789)
- Convert "middleware not registered" warning into a full error by
[@&#8203;markerikson](https://github.com/markerikson) in
[https://github.com/reduxjs/redux-toolkit/pull/2792](https://github.com/reduxjs/redux-toolkit/pull/2792)
- Rework endpoint serializeQueryArgs to allow object/number returns by
[@&#8203;markerikson](https://github.com/markerikson) in
[https://github.com/reduxjs/redux-toolkit/pull/2835](https://github.com/reduxjs/redux-toolkit/pull/2835)
- allow for global `responseHandler` and `validateStatus` configuration
by [@&#8203;phryneas](https://github.com/phryneas) in
[https://github.com/reduxjs/redux-toolkit/pull/2823](https://github.com/reduxjs/redux-toolkit/pull/2823)
- Fix refetches when sQA returns same value and queryArgs are object by
[@&#8203;markerikson](https://github.com/markerikson) in
[https://github.com/reduxjs/redux-toolkit/pull/2844](https://github.com/reduxjs/redux-toolkit/pull/2844)
- Add an auto-batching enhancer that delays low-pri notifications and
use with RTKQ by [@&#8203;markerikson](https://github.com/markerikson)
in
[https://github.com/reduxjs/redux-toolkit/pull/2846](https://github.com/reduxjs/redux-toolkit/pull/2846)
- Check middleware registration directly to avoid persistence issues by
[@&#8203;markerikson](https://github.com/markerikson) in
[https://github.com/reduxjs/redux-toolkit/pull/2850](https://github.com/reduxjs/redux-toolkit/pull/2850)
- Fix "running thunks" types and remove unnecessary RTKQ selectors by
[@&#8203;markerikson](https://github.com/markerikson) in
[https://github.com/reduxjs/redux-toolkit/pull/2856](https://github.com/reduxjs/redux-toolkit/pull/2856)
- Make autobatching notification queueing configurable by
[@&#8203;markerikson](https://github.com/markerikson) in
[https://github.com/reduxjs/redux-toolkit/pull/2857](https://github.com/reduxjs/redux-toolkit/pull/2857)
- Hopefully final v1.9 tweaks by
[@&#8203;markerikson](https://github.com/markerikson) in
[https://github.com/reduxjs/redux-toolkit/pull/2859](https://github.com/reduxjs/redux-toolkit/pull/2859)

**Full Changelog**:
reduxjs/redux-toolkit@v1.8.6...v1.9.0

###
[`v1.8.6`](https://github.com/reduxjs/redux-toolkit/releases/tag/v1.8.6)

[Compare
Source](https://github.com/reduxjs/redux-toolkit/compare/v1.8.5...v1.8.6)

This bugfix release fixes a couple of issues with RTKQ endpoint tags not
invalidating correctly, and tweaks the `dispatch` type inference to
handle more variations of arrays.

#### What's Changed

- Fix the `dispatch` type inference to correctly handle read-only
middleware arrays by [@&#8203;dokmic](https://github.com/dokmic) in
[https://github.com/reduxjs/redux-toolkit/pull/2629](https://github.com/reduxjs/redux-toolkit/pull/2629)
- fix(toolkit): export "ThunkMiddleware" from redux-thunk by
[@&#8203;VinceOPS](https://github.com/VinceOPS) in
[https://github.com/reduxjs/redux-toolkit/pull/2745](https://github.com/reduxjs/redux-toolkit/pull/2745)
- Remove previous api tags before adding new provided tags by
[@&#8203;Bezmehrabi](https://github.com/Bezmehrabi) in
[https://github.com/reduxjs/redux-toolkit/pull/2702](https://github.com/reduxjs/redux-toolkit/pull/2702)
- Fix invalidateTags by
[@&#8203;manceau-jb](https://github.com/manceau-jb) in
[https://github.com/reduxjs/redux-toolkit/pull/2721](https://github.com/reduxjs/redux-toolkit/pull/2721)

**Full Changelog**:
reduxjs/redux-toolkit@v1.8.5...v1.8.6

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://app.renovatebot.com/dashboard#github/valora-inc/wallet).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNC45Ny4xIiwidXBkYXRlZEluVmVyIjoiMzQuOTcuMSJ9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants