filipovici bogdan

2024-02-19 · 8 min read

react query: a game-changer for data management

react
react query
state management
frontend

react query has revolutionized how we handle server state in react applications. let's dive deep into why it's become such a crucial tool in modern web development.

the problem react query solves

traditional data fetching and caching in react applications often leads to:

  • repetitive fetch logic across components
  • complex loading and error states management
  • stale data issues
  • unnecessary server requests
  • no built-in caching mechanism

key benefits

1. automatic caching

react query implements a smart caching system that:

  • caches query results automatically
  • provides configurable cache times
  • enables background data updates
  • supports cache invalidation

it also doubles as a replacement for global state management:

  • access cached data from any component without prop drilling
  • no need for redux/zustand for server state
  • automatic cache updates across components
// component A - fetches and caches data
function ProductsList() {
  const { data: products } = useQuery({
    queryKey: ['products'],
    queryFn: fetchProducts,
  });
  return <div>{/* render products */}</div>;
}
 
// component B - instantly accesses cached data
function ProductCount() {
  const { data: products } = useQuery({
    queryKey: ['products'],
    queryFn: fetchProducts,
  });
  return <div>total products: {products?.length}</div>;
}
 
// component C - updates cache automatically
function AddProduct() {
  const queryClient = useQueryClient();
  const mutation = useMutation({
    mutationFn: addProduct,
    onSuccess: () => {
      // invalidates cache and triggers refetch
      queryClient.invalidateQueries({ queryKey: ['products'] });
    },
  });
  return <button onClick={() => mutation.mutate(newProduct)}>add</button>;
}

2. built-in state management

  • manages loading states automatically
  • handles error states elegantly
  • provides retry mechanisms
  • supports optimistic updates

3. data synchronization

  • automatic background refetching
  • real-time data synchronization
  • window focus refetching
  • parallel queries handling

4. developer experience

  • minimal boilerplate code
  • declarative data fetching
  • devtools for debugging
  • typescript support

potential drawbacks

1. learning curve

  • new concepts to understand
  • different mental model from traditional state management

2. bundle size

  • adds ~12kb (minified + gzipped) to your bundle
  • may be significant for smaller applications

3. configuration complexity

  • many options to configure
  • might be overwhelming at first

when to use react query

react query is ideal when your application:

  • makes frequent api calls
  • needs real-time data updates
  • requires complex data caching
  • deals with server state management

best practices

1. separate queries

// good - each resource gets its own cache entry
const { data: user } = useQuery({
  queryKey: ['user'],
  queryFn: fetchUser,
});
const { data: posts } = useQuery({
  queryKey: ['posts'],
  queryFn: fetchPosts,
});
 
// avoid - couples unrelated resources under a single cache key
const { data } = useQuery({
  queryKey: ['userdata'],
  queryFn: fetchUserAndPosts,
});

2. structure query keys hierarchically

// good - include all dependencies in the key so the cache updates automatically
useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId),
});
 
// better - nest related queries under a shared prefix
useQuery({
  queryKey: ['users', userId, 'posts'],
  queryFn: () => fetchUserPosts(userId),
});

3. error handling

method 1: component-level error handling

const { data, error, isError } = useQuery({
  queryKey: ['data'],
  queryFn: fetchData,
});
 
if (isError) {
  return <ErrorComponent error={error} />;
}

method 2: query error boundary

import { QueryErrorResetBoundary } from '@tanstack/react-query';
 
function ProductsPage() {
  return (
    <QueryErrorResetBoundary>
      {({ reset }) => (
        <ErrorBoundary
          onReset={reset}
          fallbackRender={({ resetErrorBoundary }) => (
            <div>
              there was an error!
              <button onClick={() => resetErrorBoundary()}>try again</button>
            </div>
          )}
        >
          <Products />
        </ErrorBoundary>
      )}
    </QueryErrorResetBoundary>
  );
}

method 3: next.js error and loading handling

next.js provides a file-convention based approach for error and loading states. by simply creating these special files next to your page.tsx, next.js automatically wraps your page component with the appropriate boundaries:

// app/products/page.tsx
async function ProductsPage() {
  const products = await fetchProducts();
  return <ProductsList products={products} />;
}
 
// app/products/error.tsx - automatically used as error boundary fallback
'use client';
 
export default function Error() {
  return (
    <div>
      <h2>something went wrong loading products!</h2>
    </div>
  );
}
 
// app/products/loading.tsx - automatically used as suspense fallback
export default function Loading() {
  return <LoadingSpinner />;
}

behind the scenes, next.js automatically generates something like this:

// what next.js does automatically
<ErrorBoundary fallback={<ErrorComponent />}>
  <Suspense fallback={<LoadingComponent />}>
    <ProductsPage />
  </Suspense>
</ErrorBoundary>

4. loading states

method 1: component-level loading

in v5 the first-load flag was renamed from isLoading to isPending. a derived isLoading = isPending && isFetching still exists if you need it.

// handling multiple parallel queries
const {
  data: products,
  isPending: productsPending,
} = useQuery({
  queryKey: ['products'],
  queryFn: fetchProducts,
});
 
const {
  data: categories,
  isPending: categoriesPending,
} = useQuery({
  queryKey: ['categories'],
  queryFn: fetchCategories,
});
 
if (productsPending || categoriesPending) {
  return <LoadingSpinner />;
}

method 2: suspense

import { Suspense } from 'react';
 
function ProductsPage() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <Products />
    </Suspense>
  );
}

conclusion

react query is more than just a data-fetching library; it's a complete solution for managing server state in react applications. while it may have a learning curve, the benefits it provides in terms of developer experience, performance, and code maintainability make it a valuable addition to any react project.

remember, like any tool, it's important to evaluate whether react query fits your specific use case. for simple applications with minimal data fetching needs, it might be overkill. however, for complex applications with frequent server interactions, react query can significantly simplify your codebase and improve user experience.