Ctrl+k

Get Started

You can see the complete documentation of react query on their website TanStack Start Overview

Install react query

npm install @tanstack/react-router
// or
pnpm add @tanstack/react-router
// or
yarn add @tanstack/react-router
// or
bun add @tanstack/react-router

Set up react query provider

// providers.tsx
"use client"

import {
    QueryClient,
    QueryClientProvider,
} from '@tanstack/react-query'
import * as React from 'react'

export function Providers({ children }: { children: React.ReactNode }) {
    const [queryClient] = React.useState(() => new QueryClient());

    return (
        <QueryClientProvider client={queryClient}>
            {children}
        </QueryClientProvider>
    )
}

Wrap layout with react query provider

// layout.tsx
<html lang="en">
    <body>
        <Providers>
            {children}
        </Providers>
     </body>
</html>
The above code snippets are the basic setup for react query in NextJS application. You can add more configurations as per your requirements.

Data Fetching

// actions.ts

"use server"

import { apiClient } from "@/hooks"

export const fetchLaunches = async (): Promise<launchProps[]> => {
    const launchResponse = await apiClient.get<launchProps[]>('/launches')
    const launches = launchResponse.data

    const launchWithRocketNames = await Promise.all(
        launches.map(async (data) => {
            try {
                const rocketResponse = await apiClient.get<rocketName>('/rockets/'$'{data.rocket}')
                return {
                    ...data,
                    rocketName: rocketResponse.data.name,
                    company: rocketResponse.data.company,
                    country: rocketResponse.data.country
                }
            } catch (error) {
                return { ...data, rocketName: "Unknown", company: "Unknown", country: "Unknown" }
            }
        })
    )

    return launchWithRocketNames
}

Custom react query hooks

// hooks/queries.ts

import { fetchLaunches } from "@/lib/actions"
import { launchProps } from "@/types"
import { useQuery } from "@tanstack/react-query"

export const useLaunches = () => {
    return useQuery<launchProps[], Error>(
        {
            queryKey: ['launches'],
            queryFn: fetchLaunches,
            staleTime: 5 * 60 * 1000,
            retry: 2,
        }
    )
}
This custom hook uses the fetchLaunches function and React Query's useQuery to manage data fetching, caching, and errors. The useQuery hook takes a query key and a function that fetches the data. The query key is used to identify the query in the cache and to refetch the data when needed. The fetchLaunches function fetches the data from the SpaceX API and returns the response. The useQuery hook handles the loading, error, and data states and provides a set of functions to refetch the data, invalidate the cache, and more.

Data Handling

Display data

"use client"

import { useLaunches } from '@/hooks/queries';

const DataHandling = () => {
    const { data: launches, isLoading, isFetching } = useLaunches()

    return (
        {isLoading ? (
            <Skeleton />
        ): (
            ...launches.map
        )}
    )
}
In this component, DataHandling, I'm using a custom hook called useLaunches to fetch data about launches. This hook leverages React Query, which helps manage server state and automatically handles caching, background updates, and synchronization with the server.This structure provides a smooth and efficient way to fetch and display data while handling loading states gracefully.

Filtered Data

"use client"

import React, { useMemo, useState } from 'react'
import { useLaunches } from '@/hooks/queries'
import { launchProps } from '@/types';

const DataHandling = () => {
    const { data: launches, isLoading, isFetching } = useLaunches()
    const [filteredData, setFilteredData] = useState<launchProps[]>(launches ?? [])
    const [query, setQuery] = useState('')
    const [date, setDate] = React.useState<Date>()
    const [status, setStatus] = useState<"success" | "failure" | "unknown" | null>(null)

    ...previous code

     useMemo(() => {
        const filtered = launches?.filter((data) => {
            return data.name.toLowerCase().includes(query.toLowerCase())
        }).filter((data) => {
            if (status === "success") return data.success === true
            if (status === "failure") return data.success === false
            if (status == "unknown") return data.success === null
            return true; // if status is null
        }).filter((data) => {
            if (!date) return true // if date is null
            return formatDate(data.date_utc) === formatDate(date)
        })

        setFilteredData(filtered ?? [])
    }, [query, launches, status, date])

    return (
        ...filteredData.map
    )
}
I'm handling data filtering on the frontend because I don't have direct access to an API endpoint for filtering. The useLaunches hook fetches all the launch data, and I use local states to manage the filters. The useMemo hook optimizes the filtering process by recalculating the filtered data only when specific dependencies change, like query, status, date, or launches. The filtering logic itself is straightforward: it first filters launches based on the query entered, then checks for the selected status (whether the launch was successful, failed, or unknown), and finally filters by the date if one is specified.This way, the component allows users to interactively filter the data without unnecessary re-renders, providing a smooth experience even without server-side filtering.

Sorted Data

"use client"

import React, { useMemo, useState } from 'react'
import { useLaunches } from '@/hooks/queries'
import { launchProps } from '@/types';

const DataHandling = () => {
    const { data: launches, isLoading, isFetching } = useLaunches()
    const [filteredData, setFilteredData] = useState<launchProps[]>(launches ?? [])
    const [sort, setSort] = useState<"dateAsc" | "dateDesc" | "nameAsc" | "nameDesc" | null>(null)

    ...previous code

    const sortedData = useMemo(() => {
        return sort === "nameAsc"
            ? [...filteredData].sort((a, b) => a.name.localeCompare(b.name))
            : sort === "nameDesc"
                ? [...filteredData].sort((a, b) => b.name.localeCompare(a.name))
                : sort === "dateAsc"
                    ? [...filteredData].sort((a, b) => new Date(a.date_utc).getTime() - new Date(b.date_utc).getTime())
                    : sort === "dateDesc"
                        ? [...filteredData].sort((a, b) => new Date(b.date_utc).getTime() - new Date(a.date_utc).getTime())
                        : filteredData
    }, [filteredData, sort])

    return (
        ...sortedData.map
    )
}
I'm adding sorting functionality to organize the filtered launch data. I use a sort state to store the selected sorting order, whether by name or date in ascending or descending order. By using useMemo, I ensure that the sorting operation only recalculates when filteredData or sort changes, which improves performance by avoiding unnecessary re-sorting on every render. The sortedData array is derived from filteredData and is sorted according to the selected sort option. This structure lets me dynamically sort the data based on user selection, providing a responsive and efficient way to present the data in the desired order without impacting load times.

Pagination

"use client"

import React, { useMemo, useState } from 'react'
import { useLaunches } from '@/hooks/queries'
import { launchProps } from '@/types';

const DataHandling = () => {
    const { data: launches, isLoading, isFetching } = useLaunches()
    const [filteredData, setFilteredData] = useState<launchProps[]>(launches ?? [])
    const [page, setPage] = useState(1)

    ...previous code

    const itemsPerPage = 10
    const totalPages = Math.ceil(sortedData.length / itemsPerPage)

    const pageChangeHandler = (newPage: number) => {
        setPage(newPage)
    }

    const paginatedData = useMemo(() => {
        const startIndex = (page - 1) * itemsPerPage
        const endIndex = startIndex + itemsPerPage
        return sortedData.slice(startIndex, endIndex)
    }, [sortedData, page])

    // check if page is greater than 1
    const hasPrevPage = page > 1

    // check if page is less than total pages
    const hasNextPage = page < totalPages

    return (
        ...paginatedData.map

        // Pagination control
        <PaginationControl
            hasNextPage={hasNextPage}
            hasPrevPage={hasPrevPage}
            totalPages={totalPages}
            onPageChange={pageChangeHandler}
            page={page}
        />
    )
}
I'm implementing pagination to manage the display of launch data in smaller, manageable chunks. I set a fixed itemsPerPage value, which determines how many items are shown per page. Using the page state, I track the current page and update it through the pageChangeHandler function, allowing the user to navigate between pages.The paginatedData variable is created with useMemo to optimize performance, recalculating only when sortedData or page changes. It slices sortedData to retrieve only the items for the current page. The hasPrevPage and hasNextPage variables are helpful for controlling the visibility of pagination controls, ensuring users can only navigate to valid pages. This setup provides a smooth user experience by displaying data in a paginated format, keeping the interface responsive and organized, even with large datasets.

Here's the Result

Mission NameRocket NameLaunch DateStatus
Using useMemo for filtering, sorting, and pagination is ideal here because it optimizes performance by caching the results and recalculating only when necessary. Unlike useEffect, which is designed for handling side effects and often requires additional state to store results, useMemo provides a cleaner, more declarative approach. This keeps the code efficient by avoiding unnecessary re-renders and reducing the overhead of repeated calculations, especially when working with larger datasets. Overall, useMemo helps make the component more responsive and maintainable, handling these data operations smoothly without introducing extra complexity.Using axios alone handles requests well, but combining it with React Query creates a more efficient data-fetching system. React Query adds automatic caching, refetching, and background updates, improving performance and minimizing redundant requests. This approach enhances user experience with features like optimistic updates and easy access to loading and error states, making the app feel faster and more reliable.