reduxjs / redux-toolkit

The official, opinionated, batteries-included toolset for efficient Redux development
https://redux-toolkit.js.org
MIT License
10.64k stars 1.15k forks source link

RTK Query: Question: How can I manipulate the store manually, when RTK Query is controlling it? #1355

Closed ghasemikasra39 closed 3 years ago

ghasemikasra39 commented 3 years ago

I am using RTK Query to fetch a list of items:

export const CommentsApi = createApi({
  reducerPath: 'comments',
  baseQuery: fetchBaseQuery({ baseUrl: 'https://something' }),
  endpoints: (builder) => ({
    getComments: builder.query({
      query: () => `comments`,
    }),
  }),
})

With this I can see that these actions are automatically dispatched: comments/fetchComments/pending comments/fetchComments/fulfilled

So everything working perfectly.

In this scenario, the comments piece of state is completely handled by RTK Query.

But how can I manually dispatch some actions and change the cached data? i.e. How can I do CRUD operation on this piece of state (comments) without doing any API call through RTK Query? e.g. by dispatching:

{
   type: 'comments/update',
   payload: { some data here }
}

Correct me if I am wrong, but I think when we use RTK Query, that piece of state that is handled by RTK Query is completely locked and we cannot interact with it from outside RTK Query mechanism. Is it true?

I know that this may be against the basic mindset behind RTK Query, but in some scenarios, these manual interactions on the cached data are needed.

I should mention that I can easily achieve my goal using normal redux-toolkit without RTK Query, but I would lose all the cool functionalities of RTK Query. i.e. I want to use RTK Query but also have the freedom to control the store via dispatching normal actions.

phryneas commented 3 years ago

You can use updateQueryData, but be aware that the next time that query is being refetched from the server, all your changes will be gone.

The job of RTK-Query is to accurately reflect the last known state of the server. If you are keeping local property changes for it, it might be a good idea to keep track of those in parallel in a dedicated slice.

ghasemikasra39 commented 3 years ago

Dear @phryneas Does it work for a mutation query as well? Because I am trying to make it to work, but my UpdateRecipe function is not called:


const playgroundApi = createApi({
  reducerPath: 'playgrounds',
  baseQuery: fetchBaseQuery({baseUrl: Global.baseUrl}),
  endpoints: builder => ({
    getPlaygrounds: builder.mutation({query: getPlaygroundsHandler})
  })
})

 import {useDispatch} from 'react-redux';

 const dispatch = useDispatch()
 const updateQuery = playgroundApi.util.updateQueryData('getPlaygrounds', undefined, (draft) => {
     console.log('It is not called !!')
     draft.push({ id: 1, name: 'Teddy' })
  })
 const patchCollection = dispatch(updateQuery)
phryneas commented 3 years ago

No, just for queries, as mutation results are "throwaway" and not long-living cache entries.

I am pretty sure that something called getPlaygrounds should be a query though - why is it a mutation in your case?

ghasemikasra39 commented 3 years ago

Dear @phryneas

Yes, you're right, getPlaygrounds should be a query. Thank you for your help, you saved my day. I will close this issue.

bkempe commented 3 years ago

@phryneas Can you elaborate on why mutation results are throwaway? Let's say there are two endpoints GET /api/profile and PUT /api/profile. The latter returns the updated user profile. Is there a way for RTK Query to use that response and update the cache, or does the updated profile have to be re-fetched via the GET endpoint?

phryneas commented 3 years ago

You can use optimistic updates to update that other endpoint - but refetching is surely less work.

As for mutations being throwaway. Imagine you have two components, A and B. Both trigger the same mutation (let's assume something like spending money and getting a receipt id back) and show the results. Now you would not expect the receipt id from the money sending in component A to suddenly override the value in component B.

That is, why a mutation result is tightly bound to the hook instance that triggered it - and if the parent component of that hook unmounts or that hook just triggers another mutation, there is no real way of "getting" that result, since it is cached by request id. So it is removed at that moment.

markerikson commented 3 years ago

@bkempe I haven't tried it, but you could probably use the cache lifecycle methods in association with myApi.util.updateQueryData to do it:

See these references:

bkempe commented 3 years ago

Thanks!!

phryneas commented 3 years ago

Also, https://redux-toolkit.js.org/rtk-query/usage/optimistic-updates

ghasemikasra39 commented 3 years ago

Is there a way to check why the function I pass to api.util.updateQueryData is not get called?

this is what I get after dispatching my updateQuery:


{
   patches: []
   inversePatches: []
   undo: ƒ undo()
   __proto__: Object
}

and I don't see any queryResultPatched action being dispatch!

phryneas commented 3 years ago

It does not get called only if there is no cache value to update.

ghasemikasra39 commented 3 years ago

But I can see the cache value using my RN Debugger. I can see the respective piece of state:

Screenshot 2021-08-13 at 11 40 06


export const scrollingNoteApi = createApi({
  reducerPath: 'scrollingNote',
  baseQuery: fetchBaseQuery({
    baseUrl: Global.baseUrl,
    prepareHeaders: async (headers, {getState}) => {
      const token = await AsyncStorage.getItem('token')
      headers.set('authorization', `Bearer ${token}`)
      return headers
    }
  }),
  endpoints: builder => ({
    getScrollingNote: builder.query({query: getScrollingNoteHandler}),
  })
})

 const action = scrollingNoteApi.util.updateQueryData('getScrollingNote', undefined,
     (draftNotes) => {
       draftNotes.push({'new': 'data'})
     })
 const res = dispatch(action)
phryneas commented 3 years ago

That seems to have been called with a different argument than undefined. Your code would update the cache entry for useGetScrollingNoteQuery() or useGetScrollingNoteQuery(undefined). That does not seem to exist.

ghasemikasra39 commented 3 years ago

You are right, I am using it this way:

const {data, isLoading, refetch} = useGetScrollingNoteQuery(notesList)

okay so firstly, I fixed it by passing notesList (I still don't understand why):

const action = scrollingNoteApi.util.updateQueryData('getScrollingNote', notesList,
        (draftNotes) => {
          draftNotes.push(...newNotes)
        })

So I think I still did not fully understand updateQueryData. Honestly, I read this section multiple times, but still don't understand the usage of args: args: a cache key, used to determine which cached dataset needs to be updated

When I am using getScrollingNote, does not RTK understand which cached dataset needs to be updated? I mean, I am explicitly mentioning getScrollingNote.

Is args used by RTK Query internals or can it be used by end-users? I mean, I really don't understand what I it expects for this argument. I saw it was always passed as undefined in the doc.

phryneas commented 3 years ago

Assume you use useGetScrollingNoteQuery(5) and useGetScrollingNoteQuery(4). Then you have two cache entries in the store at the same time. Calling const action = scrollingNoteApi.util.updateQueryData('getScrollingNote', 4, ... will update one and const action = scrollingNoteApi.util.updateQueryData('getScrollingNote', 5, ... will update the other

ghasemikasra39 commented 3 years ago

I got it now. But why can I have two cache entries of the same piece of state in the store at the same time? Redux is said to be so-called Single Source of Truth, so when changing a piece of state in redux via an action, regardless of who and from where the action is dispatch, and regardless of the action effects, should update the value for all subscribers, right?

phryneas commented 3 years ago

RTK Query is a document cache, not a normalized cache. That data is stored as-is and everything is treated as completely independent data.

Building a normalized api cache that takes all the infinite possible different api response formats into account is just not possible for us - much bigger teams with much more money have failed at that. It is specifically not a design goal.

ghasemikasra39 commented 3 years ago

Got it. Let me know if I need to create a new issue for this: So scenarios like this are not possible, right?

Scenario: What if another hook (hookB) wants to just read the same cache that is populated via hookA, but hookB does not have access to the expensive arg value?

function hookA(props) {
  const {data, isLoading} = useGetScrollingNoteQuery(expensiveArgThatIsOnlyAvailbleHere)
}

function hookB(props) {
  const {data, isLoading} = useGetScrollingNoteQuery()
}

Or should I use something like this?

function hookB(props) {
   const {data, isLoading} = scrollingNoteApi.endpoints.getScrollingNote.useQueryState(skipToken)
}
phryneas commented 3 years ago

At that point, you should probably store the arg in a Redux slice and make it available to both components that way