The original tkdodo. Eu/blog/react -…

Welcome to Part 2 of “Things I Have to Say About React – Query.” As I get more involved with the code base and the community around it, I see more patterns that people often ask about. Initially, I wanted to write them all in one big article, but decided to break them down into more manageable pieces. The first is about a very common and important task: data transformation.

Data conversion

Let’s face it — most of us don’t use GraphQL. If you do, then you’ll be very happy because you have the luxury of requesting data in the format you want.

However, if you are using REST, you are limited by what the back end returns. So how and where is the best way to transform data when using React – Query? The only answer worth trying in software development also applies here:

It’s always up to each developer,

Here is the 3+1 method to transform the data according to their strengths and weaknesses:

0. In the back-end

This is my favorite method, if you can afford it. If the back end returns the data in the structure we want, we don’t need to do anything. While this may sound impractical in many cases, it is also likely to be implemented in enterprise applications when using public REST apis, for example. If you control the back end and have an endpoint that can return data for your exact use case, the preference is to deliver the data the way you want.

🟢 No front-end workload 🔴 but not always possible

1. In the queryFn

QueryFn is the function you pass to useQuery. It expects you to return a Promise, and the resulting data will end up in the query cache. This does not mean that you have to return data in the structure provided at the back end, you can convert it before doing so:

const fetchTodos = async() :Promise<Todos> => {
  const response = await axios.get('todos')
  const data: Todos = response.data

  return data.map((todo) = > todo.name.toUpperCase())
}

export const useTodosQuery = () = > useQuery(['todos'], fetchTodos)
Copy the code

On the front end, you can use this data “as if it came from the back end.” You don’t actually use non-uppercase to-do names in your code. You will also have no access to the original structure. If you look at react-query-devtools, you’ll see the transformed structure. If you look at the network trace, you will see the original structure. This can be confusing, so keep that in mind.

In addition, there are no optimizations that react- Query can do for you. Your transformation will run each time you perform an extract. If conversion is costly, consider one of the other options. Some companies also have a shared API layer that abstracts data retrieval, so you may not be able to access that layer for transformation.

🟢 is very “close to the back end” in terms of co-location 🟡 the converted structure ends up in the cache so you cannot access the original structure 🔴 run 🔴 on every fetch it is not feasible if you have a shared API layer that cannot be freely modified

2. in the render function

As mentioned in Part 1, if you create custom hooks, you can easily convert there:

const fetchTodos = async() :Promise<Todos> => {
  const response = await axios.get('todos')
  return response.data
}

export const useTodosQuery = () = > {
  const queryInfo = useQuery(['todos'], fetchTodos)

  return {
    ...queryInfo,
    data: queryInfo.data? .map((todo) = > todo.name.toUpperCase()),
  }
}
Copy the code

For now, this not only runs every fetch function run, but actually runs every render (even those that don’t involve data extraction). This may not be a problem at all, but if it is, you can optimize it using useMemo. Be careful to define the narrowest possible dependencies. Unless something has actually changed (in which case you want to recalculate the conversion), the data in queryInfo will be reference-stable, but queryInfo itself will not. If you add queryInfo as your dependency, the conversion will run again on each render:

export const useTodosQuery = () = > {
  const queryInfo = useQuery(['todos'], fetchTodos)

  return {
    ...queryInfo,
    // 🚨 don't do this - the useMemo does nothing at all here!
    data: React.useMemo(
      () = >queryInfo.data? .map((todo) = > todo.name.toUpperCase()),
      [queryInfo]
    ),

    // ✅ correctly memoizes by queryInfo.data
    data: React.useMemo(
      () = >queryInfo.data? .map((todo) = > todo.name.toUpperCase()),
      [queryInfo.data]
    ),
  }
}
Copy the code

This is a good choice, especially if your custom hooks have extra logic in them that relies on your data conversion. Note that the data may be undefined, so use optional chaining when using it.

🟢 can be optimized via useMemo 🟡 cannot check the exact structure in DevTools 🔴 the somewhat complicated syntax 🔴 data may be undefined

3. Use select options

V3 introduces built-in selectors that can also be used to convert data:

export const useTodosQuery = () = >
  useQuery(['todos'], fetchTodos, {
    select: (data) = > data.map((todo) = > todo.name.toUpperCase()),
  })
Copy the code

The selector will only be called when the data exists, so you don’t have to worry about undefined here. A selector like the one above will also run on each render because the function identifier has changed (it is an inline function). If your conversion is expensive, you can use useCallback or remember it by extracting it into a stable function reference:

const transformTodoNames = (data: Todos) = >
  data.map((todo) = > todo.name.toUpperCase())

export const useTodosQuery = () = >
  useQuery(['todos'], fetchTodos, {
    // ✅ uses a stable function reference
    select: transformTodoNames,
  })

export const useTodosQuery = () = >
  useQuery(['todos'], fetchTodos, {
    // ✅ memoizes with useCallback
    select: React.useCallback(
      (data: Todos) = > data.map((todo) = > todo.name.toUpperCase()),
      []
    ),
  })
Copy the code

In addition, the select option can be used to subscribe to only part of the data. That’s what makes this approach truly unique. Consider the following example:

export const useTodosQuery = (select) = >
  useQuery(['todos'], fetchTodos, { select })

// Secondary encapsulation Hook
export const useTodosCount = () = > useTodosQuery((data) = > data.length)
export const useTodo = (id) = >
  useTodosQuery((data) = > data.find((todo) = > todo.id === id))
Copy the code

Here, we create a Useselector-like API by passing our custom selector to our useTodosQuery. Custom hooks still work as before, because if you do not pass select they are undefined and therefore return the entire state.

But if you pass a selector, you now subscribe only to the results of the selector function. This is very powerful, because it means that even if we update the name of a todo, the component that we count by subscription only with useTodosCount will not be re-rendered. The count doesn’t change, so React-Query can choose not to notify the observer of the update 🥳 (note that this is a bit simplistic and technically not entirely correct – I’ll discuss rendering optimizations in more detail in Part 3).

🟢 optimum 🟢 allows partial subscription the 🟡 structure may be shared twice differently for each observer (I will also discuss this in more detail in Part 3)