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

[DRAFT] Implement a util function to batch-upsert cache entries #4561

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

markerikson
Copy link
Collaborator

This PR:

  • Extracts the current reducer logic for writing pending and fulfilled cache entries into reusable helper functions
  • Implements a new util.upsertEntries action creator + reducer that accepts an array of {endpointName, arg, value} entries, and upserts those into the cache, overwriting any prior entries

Use Cases

One use case might be just preloading some data into the cache programmatically.

The other main use case I'm thinking of is a pseudo-normalization approach, ala #4106 and #4073.

Say you have a getPosts endpoint and it returns the usual array. RTKQ won't deduplicate individual objects across multiple endpoints, so if you then try to access getPost("2"), we have to make that request separately.

It seems very reasonable to want to take the response contents from the getPosts query, and prepopulate all of the corresponding individual getPost cache entries.

We have upsertQueryData, which relies on the entire async thunk lifecycle, and like the rest of our utils it only does a single entry at a time. That's always felt too limiting, both in terms of possibly wanting to update many cache entries at once, and performance due to the number of actions that would have to get dispatched.

Status

This is the first seemingly-working implementation. Thanks to @EskiMojo14 the external types seem to work (ie, {endpointName: 'getPost'} correctly expects to go along with {arg: string, value: Post}). I threw in one POC unit test

Perf

Per #4106, doing this by calling the apiSlice.reducer() with a separate faked action for each item was still relatively slow - 1500ms for 1000 items (as compared to the abominably slow 6000ms for 1000 items using individual upsertQueryData calls).

In a quick local test, a single dispatch(api.util.upsertEntries()) call with 1000 separate getPost entries was only 31ms. That's because we only called the reducer once, so we did all that work inside a single Immer call. That should reduce the perf overhead significantly.

Plans

The starting point would be to just ship this util action creator as-is, and users would manually dispatch that in onQueryStarted after the response resolves.

I can also imagine a new upsertCacheEntries option to make this more of a first-class citizen, which would look roughly like (data: T) => NormalizedQueryUpsertEntry[].

I'm sure there's edge cases here. For example, merge expects baseQueryMeta, and since we aren't even using a real request here, we don't have that. Right now I'm faking it with an empty object. I could imagine a merge function actually expecting a real value instead.

Copy link

codesandbox bot commented Aug 14, 2024

Review or Edit in CodeSandbox

Open the branch in Web EditorVS CodeInsiders

Open Preview

Copy link

codesandbox-ci bot commented Aug 14, 2024

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 3e7e4a8:

Sandbox Source
@examples-query-react/basic Configuration
@examples-query-react/advanced Configuration
@examples-action-listener/counter Configuration
rtk-esm-cra Configuration

Copy link

netlify bot commented Aug 14, 2024

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

Name Link
🔨 Latest commit 3e7e4a8
🔍 Latest deploy log https://app.netlify.com/sites/redux-starter-kit-docs/deploys/66d642a507650600082dfa8f
😎 Deploy Preview https://deploy-preview-4561--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 configuration.

Copy link

github-actions bot commented Aug 14, 2024

size-limit report 📦

Path Size
1. entry point: @reduxjs/toolkit/query/react (modern.mjs) 13.55 KB (+2.2% 🔺)
1. entry point: @reduxjs/toolkit/query (cjs, production.min.cjs) 22.35 KB (+1.31% 🔺)
1. entry point: @reduxjs/toolkit/query/react (cjs, production.min.cjs) 24.36 KB (+1.16% 🔺)
2. entry point: @reduxjs/toolkit/query (without dependencies) (cjs, production.min.cjs) 9.13 KB (+3.39% 🔺)
3. createApi (.modern.mjs) 13.94 KB (+2.12% 🔺)
3. createApi (react) (.modern.mjs) 15.62 KB (+1.84% 🔺)

>
},
],
) => PayloadAction<NormalizedQueryUpsertEntryPayload>
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
) => PayloadAction<NormalizedQueryUpsertEntryPayload>
) => PayloadAction<NormalizedQueryUpsertEntryPayload[]>

@phryneas
Copy link
Member

phryneas commented Aug 18, 2024

My main critique point here is conceptual - if you do normalization, oftentimes you have a list with 50 items, and a request might only update a few of those (and also not return the unchanged ones).

So to update three items in a list of 50, userland code would have to retrieve the current cache entry with 50 items from the Redux store, manually update those three items and write them back. Essentially, moving business logic out of Redux.

That's why we have the callback notation in updateQueryData (which moves the business logic kinda into Redux), and in my eyes upsertQueryData has only very limited use case.

Could I suggest we modify this a bit so something like this would become possible?

upsertEntries([
  {
    endpointName: "item",
    arg: 1,
    value: item1,
    // this is the default
    merge: (_, value) => value,
  },
  {
    endpointName: "item",
    arg: 20,
    value: item20,
    // if someone wanted to opt out of overriding existing values, they could do something like this
    merge: (existing) => existing,
  },
  {
    endpointName: "allItems",
    arg: undefined,
    value: { 1: item1, 20: item20 },
    // and here someone could have custom merging logic
    merge(existing, upserted) {
      return { ...existing, ...upserted }
    },
  },
]);

The example above shows one weakness regarding usage with entityAdapter, though. The following would not be possible:

{
    endpointName: "allItems",
    arg: undefined,
    value: [ item1, item20 ],
    merge: itemsAdapter.setMany,
  },

The problem here being the case of "nothing exists yet, so we don't call merge". In that case, [ item1, item20 ] would be upserted without calling merge, which would result in the wrong cache entry shape for future cache updates.
But if a user worked around that by doing value: itemsAdapter.setMany(itemsAdapter.getInitialState(), upserted), the merge function itself wouldn't work out, since setMany doesn't accept a full entity-adapter-state as second argument.

I'm open for ideas here :)


PS: This could work?

{
    endpointName: "allItems",
    arg: undefined,
    value: [ item1, item20 ],
+   initialState: itemsAdapter.getInitialState()
+   // or
+   getInitialState: itemsAdapter.getInitialState
    merge: itemsAdapter.setMany,
  },

@markerikson markerikson force-pushed the feature/4106-rtkq-normalization branch from f87820f to 68235d6 Compare September 2, 2024 22:39
@markerikson markerikson force-pushed the feature/4106-rtkq-normalization branch from 08983a0 to 3e7e4a8 Compare September 2, 2024 22:56
@markerikson
Copy link
Collaborator Author

Lenz and I discussed this internally, and we noted that we already have a merge endpoint option. With the way this new util is constructed, all the normal merge logic applies. That said, we don't get transformResponse, because A) that runs inside query thunks, and B) it can be async. So, you'd need to pass in the values as transformResponse would return them.

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.

3 participants