Skip to content

Instantly share code, notes, and snippets.

@MansourM61
Last active January 15, 2026 14:45
Show Gist options
  • Select an option

  • Save MansourM61/f992683b9e940d0ad88fce9aec1a82f5 to your computer and use it in GitHub Desktop.

Select an option

Save MansourM61/f992683b9e940d0ad88fce9aec1a82f5 to your computer and use it in GitHub Desktop.

Query

This document includes code snippets for query (fetch) tools including TANSTACK Query all in React.

Vanilla Query

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.

  1. Select any required fetch tool (native fetch, axios, etc):

    npm i axios

GET Operation on Mount

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>
);
}

GET Operation on Demand

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>
  );
}

TANSTACK Query

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.

TANQ Setup

  1. To install:

    npm i @tanstack/react-query
    npm i -D @tanstack/eslint-plugin-query
    npm i -D @tanstack/react-query-devtools
  2. Add the plugin to the project 1.

  3. Add TanStack Query DevTools extension to the browser.

  4. 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>
  );
}

Automatic GET Operation

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>
  );
}

GET Operation on Demand

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>
  );
}

Parallel Fetch

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),
      };
    }),
  });
}

Dependent Queries (Serial Queries)

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 Query

Initial data can be added to the query to

  1. benefit the caching
  2. 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,
      };
    },
  });

  //...
}

Pagination

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,
    });

  //...
}

POST Operation with Data Invalidation

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>
  );
}

POST Operation with Data Update

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>
  );
}

Footnotes

  1. ESLint Plugin Query

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment