How do I properly changing response data in RTK Query?
Asked Answered
K

1

7

I'm trying to use RTK Query in currency-converter app. This app is based on 2 API.

First, I'm fetching an object with currencies. Then, I'm fetching an array of countries and filter it, depending whether the country has that kind of currency or not.

Code fo store:

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

Code for API:

export const currenciesAPI = createApi({
  reducerPath: 'currenciesAPI',
  baseQuery: fetchBaseQuery({ baseUrl: 'https://api.frankfurter.app'}),
  endpoints: (build) => ({
    fetchAllCurrencies: build.query<Currencies, void>({  
      query: () => ({
        url: '/currencies'
      }),
    })
  })
})

export const countriesAPI = createApi({
  reducerPath: 'countriesAPI',
  //tagTypes: ['Country'],
  baseQuery: fetchBaseQuery({ baseUrl: 'https://restcountries.com/v3.1'}),
  endpoints: (build) => ({
    fetchAllCountries: build.query<TransformedCountries[], string>({  
      query: () => ({
        url: '/all'
      }),
      transformResponse: (response: Countries[]) : TransformedCountries[] => {
        const countriesWithCurrency = response.filter((country: Countries) => country.currencies) // <-- MUTATING DATA HERE AND BELOW IS NORMAL?
        const transformedCountriesArray = countriesWithCurrency.map((item: Countries) => {
          const keys = Object.keys(item.currencies)
          const firstKey = keys[0]
          return {
            name: item.name.common,
            currencyFullName: item.currencies[firstKey].name,
            currencyShortName: firstKey,
            currencySymbol: item.currencies[firstKey].symbol,
            flag: item.flags.svg
          }
        })
        const finalCountriesArray = transformedCountriesArray.sort((a: TransformedCountries, b: TransformedCountries) => (a.name > b.name) ? 1 : (a.name < b.name) ? -1 : 0)
        return finalCountriesArray
      }
    })
  })
})

Code for component:

const App: React.FC = () => {

  const { 
    data: currenciesData, 
    error: currenciesError, 
    isLoading: currenciesIsLoading 
  } = useFetchAllCurrenciesQuery()

  const { 
    data: countriesData, 
    error: countriesError, 
    isLoading: countriesIsLoading
  } = useFetchAllCountriesQuery('', {
    selectFromResult: ({ data, error, isLoading }) => ({
      data: data?.filter((country: TransformedCountries) => currenciesData && currenciesData[country.currencyShortName]), // <-- MUTATING DATA HERE AND BELOW IS NORMAL?
      error,
      isLoading
    }),
    skip: !currenciesData
  })
...

Questions:

  1. Is it normal practise to use "skip" or "skipToken" option in my case if your second query depends on the result of the first? Is there any better pattern?
  2. Is it normal practise to mutate data in "transformResponse" like I did in countriesAPI and then mutate transformed response in "selectFromResult" option in useFetchAllCountriesQuery in App component? Is there any better pattern?
  3. What is a good pattern to handle two "isLoading" and two "error" in a component in that case?
Klee answered 24/12, 2021 at 10:2 Comment(0)
S
3

There are some more pitfalls that I can see, but answering the questions:

  1. Yes, ok in general, but looks not really meaningful in your case. If useFetchAllCountriesQuery had query params, that are expected from useFetchAllCurrenciesQuery results - it sounds ok. If those results are used in selectFromResult - it's not the case.

  2. Again - it's ok in general but looks like used in the wrong way in your case. All those transformations you can do OUT of transformResponse, just after both hooks calls.

  3. A bit depends on how you are going to use it later, but in general - just add new const with sum:

    const isLoading = currenciesIsLoading || countriesIsLoading;
    

and use it there you need in UI.

Sumarizing it, I would do it like:

const getCountriesWithCurrency = (currencies, countries) => {
    if (currencies && countries )  {
        return countries?.filter(country => currenciesData && 
            currencies[country.currencyShortName])
    }
}

const App: React.FC = () => {

  const { 
    data: currenciesData, 
    error: currenciesError, 
    isError: currenciesIsError,
    isLoading: currenciesIsLoading 
  } = useFetchAllCurrenciesQuery()

  const { 
    data: countriesData, 
    error: countriesError, 
    isError: countriesIsError,
    isLoading: countriesIsLoading
  } = useFetchAllCountriesQuery()

  const isLoading = currenciesIsLoading || countriesIsLoading;
  const isError = currenciesIsError || countriesIsError;
  const countries = getCountriesWithCurrency(currenciesData, countriesData);

...

error object can be somehow aggregated also, depending on what you need from it, and in what order. Most likely you won't need them aggregated, just show the error state, based on isError and log both error objects separately.

What worries me the most, and what makes it harder to define a proper code composition is your countriesAPI definition in transformResponse part where you resolve all the currencies inside the country.

I would say it's a bad pattern to have nested objects. It's enough to have a relation of which currencies are "belongs" to the country in an array of ids or codes. Or just use only first currency id like you did with, firstKey, but NOT to resolve it to full object. The API definition should be coupled only with one particular entity. It's country in your case, so it should nothing to do with currencies. ("Single Responsibility" and "Interface Segregation" from SOLID principles - is what they are about)

Btw, selectFromResult can be useful here, to aggregate all the currencies are used for countries, like

selectFromResult: ({ data, error, isLoading }) => ({
  data,
  error,
  isLoading,
  relatedCurrencies:  data?.reduce((acc, country) => ([...acc, ...Object.keys(country.currencies)]), [])
}),

Later, you may fetch the currency details by calling the currency API, but not ALL of them, but just what you really need - for one or several ids from related currencies, or for one country.

And overall suggestion - is to move these currencies and countries fetch, filtration, and isError | isLoading summing - out of react component into a separate custom hook, which will return just a prepared data and flags that component actually will use. It will make it really clear)

Sofer answered 14/3, 2022 at 11:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.