Skip to content

Avoiding Common Mistakes with TanStack Query Part 1

Published:

TanStack Query is undeniably one of the most popular and the most useful library for data fetching. Especially for React, data fetching can get complicated easily.

Although it is pretty commonly used, I noticed several common mistakes people do while using TanStack Query. I wanted to give some insights on these mistakes and explain why they are problematic.

Quick note: All of these mistakes can be in your code, they can work and they can be explained by various factors. The best code you can write is the one you can ship. Remember to ship first.

1. Mapping data to Redux/Context

A common mistake is mapping data fetched with TanStack Query to Redux or Context, a carryover from previous practices. I’ve seen two versions of this over time.

First example: Dispatch during query

This is unfortunately pretty common mistake to do. I’ve seen this mostly in codebases that used Redux/Context to store the data and migrated to TanStack afterwards.

Mostly the reasoning behind this is to call query once and use the existing state in other components in order to prevent fetching multiple times.

const useTodos = () => {
	const dispatch = useDispatch()

	return useQuery({
			queryKey: ['todos'],
			queryFn: async () => {
				dispatch(setLoading(true))
				const data = await apiService.getTodos()
				dispatch(setTodos(data))
				dispatch(setLoading(false))
				return data
			}
		})
}


const TodoList = () => {
	useTodos()

	const todos = useSelector(state => state.todos)
  
	return (
		<div>
			{todos.map(todo => (<div key={todo.id}>{todo.title}</div>))}
		</div>
	)
}

Several problems:

Second Example: useEffect

This is not as common as the first example. The reasoning is similar, however the execution in this case is even worse since this can cause even more renders than the previous one. When the data changes, it already causes a render, which causes the dispatch, which causes another render.

const useTodos = () => {
    return useQuery({
        queryKey: ['todos'],
        queryFn: async () => {
            return await apiService.getTodos()
        }
    })
}

const TodoList = () => {
    const dispatch = useDispatch()

    const { data } = useTodos()

    const todos = useSelector(state => state.todos)

    useEffect(() => {
        if (data) {
            dispatch({ type: 'SET_TODOS', payload: data })
        }
    }, [data])

    return (
        <div>
            {todos.map(todo => (
                <div key={todo.id}>{todo.title}</div>
            ))}
        </div>
    )
}

Solution

Just use the TanStack Query results. If the components using the same query key render at the same time it won’t result in an additional request either. If they are not, just adjusting the staleTime would solve the issue. TkDodo already has the best resource for this here.

const useTodos = () => {
    return useQuery({
        queryKey: ['todos'],
        queryFn: async () => {
            return await apiService.getTodos()
        },
    })
}


const TodoList = () => {
    const { data, isLoading, isError, ...rest } = useTodos()

    if (isLoading) {
        return <div>Loading...</div>
    }

    if (isError) {
        return <div>Error</div>
    }

    return (
        <div>
            {todos.map(todo => (
                <div key={todo.id}>{todo.title}</div>
            ))}
        </div>
    )
} 

2. Refetching data

This is another common mistake. I belive one of the reasoning behind this is the refetch function that TanStack Query provides. It is a powerful tool, but it is not meant to be used for every case.

Refetch function should only be used when the same query is called with exactly the same parameters. If you are using new parameters (new filters, pages etc.), you should use a new query key.

const useTodos = (page: number) => {
    return useQuery({
        queryKey: ['todos'],
        queryFn: async () => {
            return await apiService.getTodos(page)
        },
    })
}

const TodoList = () => {
    const [page, setPage] = useState(1)
    const {refetch} = useTodos(page)

    const onClick = () => {
        setPage((prev) => prev + 1)
        refetch()
    }

    return (
        <div>
            <button onClick={onClick}>click me!</button>
            ....
        </div>
    )
}

Problems:

Solution:

const useTodos = (page: number) => {
    return useQuery({
        queryKey: ['todos', page],
        queryFn: async () => {
            // or you can use the query key to get the page
            return await apiService.getTodos(page)
        },
    })
}

const TodoList = () => {
    const [page, setPage] = useState(1)
    const {data, ...rest} = useTodos(page)

    const onClick = () => {
        setPage((prev) => prev + 1)
    }

    return (
        <div>
            <button onClick={onClick}>click me!</button>
            ....
        </div>
    )
}

3. Transforming data after fetching

Backends rarely return the data frontend exactly needs. It is pretty common to transform the data after fetching. However, I see people doing this in the wrong place.

Not much I can add here because all that needs to be said is said again by TkDodo here.

I can just provide some examples of the bad ones:

  const [page, setPage] = useState(1)
  const {data} = useTodos(page)
  const [todos, setTodos] = useState([])
  
  useEffect(() => {
    if (data) {
      setTodos(filterTodos(data))
    }
  }, [data])

  return (
    <div>
      {todos.map((todo, index) => (
        <div key={index}>{todo}</div>
      ))}
    </div>
  )

I hope this helps you to avoid these common mistakes that I encountered. If you have any questions or comments, feel free to reach out to me!