This document includes code snippets for query (fetch) tools including TANSTACK Query all in React.
Vanilla query (using useState and useEffect) is simple but inefficient. The programmer is in charge of implementing extra features such as caching, updating the cache, error checking, etc.
-
Select any required fetch tool (native fetch, axios, etc):
npm i axios
In any component that requires automatic fetching (at loading time, at given intervals, etc), use this pattern:
import axios from "axios";
import { useEffect, useState } from "react";
export default function Component() {
const [isLoading, setIsLoading] = useState(true);
const [data, setData] = useState<DataType[] | null>(null);
const [error, setError] = useState("");
useEffect(() => {
let ignore = false;
axios
.get("given URL")
.then((res) => {
if (!ignore) {
setData(res.data);
}
setIsLoading(false);
})
.catch((error) => {
setError(error.message);
setIsLoading(false);
});
return () => {
ignore = true;
};
}, []); // perform the fetch at mount stage
if (isLoading) {
// data loading phase
return <h2>Loading...</h2>;
}
if (error) {
// data capture failed
return <h2>{error}</h2>;
}
return (
// data capture successful
<div>
{data
? data.map((item) => {
return <div key={item.id}>{item.name}</div>;
})
: null}
</div>
);
}If the data is fetched on demand (after a click, etc), use this pattern:
import axios from "axios";
import { useState } from "react";
export default function Component() {
const [isLoading, setIsLoading] = useState(false);
const [data, setData] = useState<DataType[] | null>(null);
const [error, setError] = useState("");
async function fetchData() {
let ignore = false;
axios
.get("given URL")
.then((res) => {
if (!ignore) {
setData(res.data);
}
setIsLoading(false);
})
.catch((error) => {
setError(error.message);
setIsLoading(false);
});
return () => {
ignore = true;
};
}
if (isLoading) {
// data loading phase
return <h2>Loading...</h2>;
}
if (error) {
// data capture failed
return <h2>{error}</h2>;
}
return (
<div>
<h2>Data Page</h2>
<button
onClick={async () => {
// fetch the data
setIsLoading(true);
await fetchData();
}}
>
Fetch Data
</button>
{data
? data.map((item) => {
return <div key={item.id}>{item.name}</div>;
})
: null}
</div>
);
}It is a powerful tool to perform all kinds of query (fetching data, GET, POST, etc). It provides features such as caching, data selection, timeouts, etc.
-
To install:
npm i @tanstack/react-query npm i -D @tanstack/eslint-plugin-query npm i -D @tanstack/react-query-devtools
-
Add the plugin to the project 1.
-
Add TanStack Query DevTools extension to the browser.
-
Add the context provider component to the top level parent.
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
// Create a client
const queryClient = new QueryClient();
// This code is only for TypeScript
declare global {
interface Window {
__TANSTACK_QUERY_CLIENT__: import("@tanstack/query-core").QueryClient;
}
}
// This code is for all users
window.__TANSTACK_QUERY_CLIENT__ = queryClient;
<QueryClientProvider client={queryClient}>
function App() {
return (
<QueryClientProvider client={queryClient}>
<ProjectTree />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}Since caching is active, the data is fetched automatically and updated if needed. Note that stale queries are re-fetched automatically in the background when:
- New instances of the query mount
- The window is refocused
- The network is reconnected
Query results that have no more active instances of useQuery, useInfiniteQuery or query observers are labelled as inactive and remain in the cache in case they are used again at a later time.
import { useQuery } from "@tanstack/react-query";
import axios, { type AxiosResponse } from "axios";
import { useEffect } from "react";
const fetchData = ({ queryKey }) => {
// given query key is passed into the query function as part of the QueryFunctionContext.
const [_key, { url, port }] = queryKey
return axios.get(url + ":" + port);
};
// function used for selecting specific parts of the results
const selectFn = (
data: AxiosResponse<DataType[], unknown, object> | undefined
) => {
return (data?.data as DataType[]).map((item) => item.name);
};
export default function Component() {
const url = "given URL";
const port = "3000"
const {
isLoading, // the query is in-flight phase
isPending, // the query has no data yet
data, // if the query is in an isSuccess state, the data is available via the data property
isError, // the query encountered an error
isSuccess, // the query was successful and data is available
error, // if the query is in an isError state, the error is available via the error property
isFetching // in any state, if the query is fetching at any time (including background refetching) isFetching will be true
} = useQuery({
queryKey: ["data", {url, port}], // an array of serializable objects as the key used to identify the data in the cache
queryFn: fetchData, // fetch function that returns a promise to either resolve the data or throw an error
gcTime: 300000, // cache life time (ms). The inactive/unused cache data will be garbage collected after this duration
staleTime: 3000, // time (ms) after which the data is considered old
enabled: true, // set this to `false` to disable this query from automatically running
select: selectFn, // by default, all fetched data are put into `data` output. `select` function is used to override this behaviour
});
const onError = () => {
// Perform side effects after encountering error
console.log("After encountering error");
};
const onSuccess = () => {
// Perform side effects after fetching data
console.log("After data fetching");
};
// useEffect block to react to error and success
useEffect(() => {
if (isError) {
onError();
} else if (isSuccess) {
onSuccess();
}
}, [isError, isSuccess]);
if (isPending) {
// data is pending.
return <h2>Pending...</h2>
}
if (isLoading) {
// data is being loaded.
return <h2>Loading...</h2>;
}
if (isFetching) {
// data is being fetched.
return <h2>Fetching...</h2>;
}
if (isError) {
// data fetching failed
return <h2>{error.message}</h2>;
}
return (
<div>
<h2>Data Page</h2>
{data
? data.map((item, index) => {
return <div key={index}>{item}</div>;
})
: null}
</div>
);
}Use the following pattern to fetch the data when needed:
import { useQuery } from "@tanstack/react-query";
import axios from "axios";
const fetchData = () => {
return axios.get("given URL");
};
export default function Component() {
const { isLoading, data, isError, error, isFetching, refetch } = useQuery({
queryKey: ["data"],
queryFn: fetchData,
enabled: false, // disable automatic fetch and use `refetch` output to fetch data on demand
});
if (isLoading) {
// data is being loaded.
return <h2>Loading...</h2>;
}
if (isFetching) {
// data is being fetched.
return <h2>Fetching...</h2>;
}
if (isError) {
// data fetching failed
return <h2>{error.message}</h2>;
}
return (
<div>
<h2>Data Page</h2>
<button onClick={
() => refetch() // call `refetch` function to fetch the data when needed.
}>Fetch List</button>
{data?.data
? (data?.data as DataType[]).map((item) => {
return (
<div key={item.id}>
<Link to={`/single-data/${item.id}`}>{item.name}</Link>
</div>
);
})
: null}
</div>
);
}To perform parallel fetching either create separate queries using useQuery (useful when the queries are fixed and not changing):
// manual parallel queries
function App () {
// The following queries will execute in parallel
const usersQuery = useQuery({ queryKey: ['users'], queryFn: fetchUsers })
const teamsQuery = useQuery({ queryKey: ['teams'], queryFn: fetchTeams })
const projectsQuery = useQuery({ queryKey: ['projects'], queryFn: fetchProjects })
...
}or use useQueries hook (useful when queries are dynamically added or removed):
// dynamic parallel queries
function App({ users }) {
const userQueries = useQueries({
queries: users.map((user) => {
return {
queryKey: ["user", user.id],
queryFn: () => fetchUserById(user.id),
};
}),
});
}If a query depends on another queries to finish before it can execute, use enabled input to control the sequence:
// Get the user
const { data: user } = useQuery({
queryKey: ["user", email],
queryFn: getUserByEmail,
});
const userId = user?.id;
// Then get the user's projects
const {
status,
fetchStatus,
data: projects,
} = useQuery({
queryKey: ["projects", userId],
queryFn: getProjectsByUser,
// The query will not execute until the userId exists
enabled: !!userId,
});Initial data can be added to the query to
- benefit the caching
- provide the initial data which is already available
import { useQueries, useQuery, useQueryClient } from "@tanstack/react-query";
import axios from "axios";
const fetchSingleData = (dataId: string) => {
return axios.get(`given URL/?id=${dataId}`);
};
export default function Component() {
const { dataId } = useParams();
const queryClient = useQueryClient();
const {
data: rxData, // alias `rxData` is defined
isLoading,
error,
isError,
} = useQuery({
queryKey: ["single-data", dataId],
queryFn: () => fetchSingleData(dataId ?? "1"),
initialData: () => {
const dataCache: { data: DataType[] } | undefined =
queryClient.getQueryData(["data"]); // check the cache that may contain the data already
const data = dataCache?.data?.find(
(item: DataType) => item.id === parseInt(dataId ?? "1")
);
return {
data: data,
};
},
});
//...
}In TANQ, add page number (id) to the query key and use the data from the last successful query as the placeholder:
import { keepPreviousData, useQuery } from "@tanstack/react-query";
export default function Component() {
const [page, setPage] = React.useState(0);
const fetchProjects = (page = 0) =>
fetch("/api/projects?page=" + page).then((res) => res.json());
const { isPending, isError, error, data, isFetching, isPlaceholderData } =
useQuery({
queryKey: ["projects", page],
queryFn: () => fetchProjects(page),
placeholderData: keepPreviousData,
});
//...
}To perform POST operation, use useMutation hook.
import { useMutation, useQueryClient } from "@tanstack/react-query";
import axios from "axios";
const addData = (data: DataType) => {
// function definition to post the new data.
// new data is the argument passed on to `mutate` function.
return axios.post("given URL", data);
};
export default function Component() {
const { mutate: addDataMutate, isSuccess } = useMutation({
mutationFn: addData, // a function that performs an asynchronous task and returns a promise.
});
const queryClient = useQueryClient();
// use `useEffectEvent` to avoid adding `queryClient` as a dependency of useEffect
const queryInvalidation = useEffectEvent(() => {
queryClient.invalidateQueries({
queryKey: ["data"],
exact: true,
});
});
// use `useEffect` to invalidate the data after the new data is posted to the server.
// This way the new data will be fetched.
useEffect(() => {
if (isSuccess) {
queryInvalidation(); // invalidate the cache
}
}, [isSuccess]);
return (
<div>
<button
onClick={() => {
// call mutate function with given new data as parameter
addDataMutate(data);
}}
></button>
</div>
);
}Instead of invalidating the whole data cache, it is possible to update it. This is the optimum way of data invalidation (optimistic updates).
import { useMutation, useQueryClient } from "@tanstack/react-query";
import axios, { type AxiosResponse } from "axios";
const addData = (data: DataType) => {
return axios.post("given URL", data);
};
export default function Component() {
const { mutate: addDataMutate } = useMutation({
mutationFn: addData,
onMutate: async (newData: DataType, context) => {
// callback used for UI update during the mutation process.
// the function updates the cache in the hope that the mutation will succeed.
await await context.client.cancelQueries({ queryKey: ["data"] }); // cancel ongoing queries for the given cache
const prevData = context.client.getQueryData(["data"]); // take a snapshot of the cache
// update the date optimistically
context.client.setQueryData(["data"], (old) => [...old, newData]);
// return snapshotted result
return {
prevData,
};
},
onError: (err, newData, onMutateResult, context) => {
// if the mutation fails, use the result returned from onMutate to roll back
context.client.setQueryData(["data"], onMutateResult.prevData);
},
onSettled: (data, error, variables, onMutateResult, context) => {
// always refetch after error or success:
context.client.invalidateQueries({ queryKey: ["data"] });
},
});
return (
<div>
<h2>Add Data Page</h2>
<button
onClick={() => {
addDataMutate(data);
}}
></button>
</div>
);
}