Connecting RTK Query API with redux reducer and selector
Asked Answered
F

1

17

I think I'm missing something with regards to Redux and RTK Query tools. I'm also using RTK Query OpenAPI codegen for this.

Right now I have an API that looks like this ->

export const api = generatedApi.enhanceEndpoints({
  endpoints: {
    getMaterials: {
      transformResponse: response => normalize(response),
    },
  },
})

This gives me back normalized data fine in my component:

const Materials = () => {
  const { data, isLoading, error } = useGetMaterialsQuery()

  /*
    Cool this gives me data back like:
    {
      [id]: {
        id,
        prop1: 'blah',
        prop2: 'blah2'
      }
    }
  */

  console.log(data)

  // but now I want to structure this data differently using selector

  const newDataStructure = useSelector(addSomeMetaDataAndStructureDifferentlySelector)

  return <MyComponent structuredData={newDataStructure} />
}

But when I look at state in that selector it looks like:

data snippet

When in the selector I really want to use something like ->

const addSomeMetaDataAndStructureDifferentlySelector = state => 
    // create new data structure from state.materials.byId that I will pass to `MyComponent`

My store looks like this right now ->

import { configureStore } from '@reduxjs/toolkit'
import { setupListeners } from '@reduxjs/toolkit/query'
import { api } from 'gen/rtk-openapi'
// import materialsReducer from 'state/materials/materialsSlice'

const store = configureStore({
  reducer: {
    [api.reducerPath]: api.reducer,
  },
  middleware: getDefaultMiddleware =>
    getDefaultMiddleware().concat(api.middleware),
})

setupListeners(store.dispatch)

export default store

So do I need to pass the data directly from useGetMaterialsQuery into a function and just transform it?

Or can I somehow create a new reducer slice that somehow initializes with the data correctly so that I can access state.materials.byId in a selector?

Or am I fundamentally missing something here?

Fumatorium answered 23/7, 2021 at 21:30 Comment(0)
P
21

You missed an important bit: RTK-Query is not a normalized cache, but a Document cache.

It caches your endpoints and each request to those, using the request arguments as cache keys. Each of those responses is treated as a "document" and cached separately.

That might seem like a drawback, but doing a real "normalized" cache is almost impossible as a generic library, especially for something ambiguous as REST apis. Even for GraphQL it is a very hard problem and even solutions specialized on GraphQL (like apollo) have lots of problems and end up with lots of hand-written code to handle cases like additions/removals from collections.

So in the end, you are left with two choices: write your own normalized caching solution by hand, explicitly geared towards your data structures or use a document cache.

In most situations, a document cache is just enough. And that's where RTK-Query comes in and tries to give you as many tools as possible to keep data "in sync" between your documents: automated refetching and where you want to avoid that the option to do manual optimistic updates to manipulate specific cache entries.

Generally, I would recommend against copying that data over to hand-written slices. You will lose a lot of benefits of RTK-Query that way, for example subscription tracking and automatic cache cleanup after no more components use a value after 60 seconds. It might sound uncommon after using Redux with hand-written slices for a while, but it works very well in most situations - with a lot less code.

As for how to use it: Generally, you just call your useGetMaterialsQuery() in all components that need the materials and just filter out what you need in your component, or use the selectFromResult option to just select what you really need.

Precisian answered 23/7, 2021 at 23:15 Comment(6)
how is selectFromResult used?Fumatorium
const result = useGetMaterialsQuery(undefined, { selectFromResult: result => ({ ...result, data: result.data.foo.bar }) })Precisian
This answer makes sense to me but I’m still struggling to figure out where in rtk-query you store changes to the document cache that you need for your components. I know copying over the cache into another slice is frowned upon, so should I create more custom hooks to add/filter/update values (client-side only nothing going back to the server) or should I place any business-logic adjustments to my server response in somewhere like transformResponse? For example, if I wanted to combine two fields on an object response into a new key value, where does that happen?Turbidimeter
@Turbidimeter if everywhere you consume that endpoint, you consume it in the same form, then in transformResponse. If one component wants only a part of a response, I would use selectFromResult and if it needs to do a data transformation on top of that, I'd just useMemo.Precisian
@Precisian do you suggest using useFooQuery(...) over api.endpoints.foo.select('cache-key') with useSelector?Tabshey
@Tabshey In almost all cases, if you can use hooks, yes.Precisian

© 2022 - 2024 — McMap. All rights reserved.