React Query CheetSheet with CRUD Example

React, React Query, Cheat Sheet

Main project image

React Query is using to create a request state short-hand in your react apps. In this blog we gonna show how to use it in most use case and explain what happen when use react query.

This blog base on React Query v4

Fetch with useQuery

simple request with useQuery. It will return data, loading status or error

export const useProfile = () => {
  return useQuery(['profile'], () => {
    return axios.get('/profile')
  })
};

// React Component
const { status, error, data } = useProfile()

Notes

  • useQuery will retry when get and error response (status code = 5xx) from API

Status Handling

you can use status enum or boolean flag using isLoading, isFetching, isError, isSuccess to handle a status of data request.

// Status Flag
const { status, fetchStatus, error, data } = useProfile()
status = 'loading' | 'fetching' | 'error' | 'success'

// Boolean Flag
const { isLoading, isFetching, isError, isSuccess, error, data } = useProfile()

Difference between isLoading & isFetching

  1. Loading
    1. Loading handle normal state of API request
    2. Loading is trigger only first fetching. Background fetching or refetch() function will not trigger loading flag.
  2. Fetching
    1. Fetching handle a state of network status. Every API request include background request will trigger fetching state.
    2. Background fetching data and refetch() manually will trigger isFetching flag.

Global Fetching Flag

If your need to display loading on every request in your app. Such as global loading status.

import { useIsFetching } from '@tanstack/react-query'
const isFetching = useIsFetching()

Re-fetching Data Strategy

1. Window Focus

When your go to other tab on come back to your web application again. react-query will automatically reload data from api to get a fresh data. If it not useful for you. You can disable it.

*Note: Window Focus refetch is in background mode. loading = false / fetching = true

// Disable with useQuery with referchOnWindowFocus flag.
export const useFindProject = (id: string) => {
  return useQuery(["project", id], () => {
    return axios.get(`/products/${id}`);
  }, {
    refetchOnWindowFocus: false
  });
};

// Disable Globally via QueryClient Options
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      refetchOnWindowFocus: false,
    },
  },
})

2. Interval

You can tell react-query to make a re-fetching loop on every x seconds using interval. Useful for auto updating data or realtime data without using websocket.

*Note: Interval refetch is in background mode. loading = false / fetching = true. Only first request that loading flag = true.

export const useFindProject = (id: string) => {
  return useQuery(["project", id], () => {
    return axios.get(`/products/${id}`);
  }, {
    refetchInterval: 5000 // In milliseconds
  });
};

3. Manual

using refetch function to manual re-fetching data from API.

*Note: Window Focus refetch is in background mode. loading = false / fetching = true

export const useProfile = () => {
  return useQuery(['profile'], () => {
    return axios.get('/profile')
  })
};
const { status, error, data, refetch } = useProfile()

// Call refetch function to manually reload data from api
refetch()

4. Invalidate Query to clear out-of-date data.

Another way to make react query handle re-fetching data is using Invalidate Query.

const queryClient = useQueryClient()
queryClient.invalidateQueries({ queryKey: ['todos', 'tags'] })

// this useQuery will be clear data and fetch new one from api
useQuery(["todos"], () => {
  return axios.get(`/todos`);
});
useQuery(["tags"], () => {
  return axios.get(`/tags`);
});

A Pros of using invalidate query method is you can re-fetching another data using key of useQuery without import that function and use a refetch method. one or more useQuery key at once. Best suite for many situation. For example, you add new todo with tag and need to refetching a todos list and tags list from API.

To clear all of useQuery cache in your app at once.

const queryClient = useQueryClient()
// Clear all query cache and every useQuery will refetch new data
queryClient.invalidateQueries()

Retry in useQuery

By default, react-query will retry useQuery 4xx, 5xx error response. You can disable it with retry flag or limit number of retry

export const useResetPasswordSession = (token: string) => {
  return useQuery(["reset-password", token], () => {
    return axios.get(`/reset-password/${token}`);
  }, {
    enabled: token ? true : false,
    retry: false
  });
};

Custom Retry Function

Some of API use error 4xx to handle a response of data. For example, an API /products/12 that will find a product id = 12 but if it not exist, API return 404 Not Found. In this case, I think it should not to retry and error will raise to tell user that data not found ( 404 Not Found Page ).

export const useResetPasswordSession = (token: string) => {
  return useQuery(["reset-password", token], () => {
    return axios.get(`/reset-password/${token}`);
  }, {
    enabled: token ? true : false,
    retry: (failedCount: number, err: AxiosError) => {
			// No retry when api response is 404 Not Found
		  if(err.response.status == 404) {
		    return false;
		  }
		  return true;
		}
  });
};

Error Handler

when useQuery get error response with http status code 4xx, 5xx. isError flag will be true. But it will retry until reach retry limit. error will be error response from API.

For example using request with axios, error will be a AxiosError instance

const { isError, error } = useProfile()

if(isError) {
	console.log(error)
	// AxiosError
	// {
	// 	statusCode: 404,
	// 	data: {}
	// }
}

Handle error in useQuery should be careful with retry. when react query retry and make request to API. isError will be set to false.

useQuery with parameters

export const useProject = (id: string) => {
  return useQuery(['project', id], () => {
    return axios.get(`/products/${id}`)
  })
};
const { status, error, data } = useProject(1)

export const useProducts = (queryParams: object) => {
  return useQuery(['project', queryParams], () => {
    return axios.get(`/products`, {
			params: queryParams
		})
  })
};
const { status, error, data } = useProducts({ isActive: true })
// if query params changed useProducts will be reload data again

Conditional in useQuery

If your need to call API after some flag such as user should be authenticated before fetching this API. use enabled flag. useQuery will call and api when enabled flag is true.

export const useProducts = (queryParams: object) => {
  const { isAuthenticated } = useAuth();
  return useQuery(["project", queryParams], () => {
    return axios.get(`/products`, {
      params: queryParams,
    });
  }, {
    enabled: isAuthenticated
  });
};

POST using useMutation

When you need to send data to API via POST / PUT / DELETE method.

export const useUpdateProfile = () => {
  return useMutation((data: User) => {
    return axios.post('/profile', data)
  })
};

// Using
const updater = useUpdateProfile()
const onSubmit = async (dto: User) => {
	try {
		const result = await updater.mutateAsync(dto)
		// Success
	} catch(error) {
		// Error Handling
	}
}

// Status Flag
updater.status = 'loading' | 'error' | 'success'

CRUD with React Query

Here is full example when using react-query in full CRUD using useQuery to fetch data and useMutation to manipulate data.

// Fetch List without paginate
export const useGetProducts = (queryParams: object) => {
  const { isAuthenticated } = useAuth();

  return useQuery(["project", queryParams], () => {
    return axios.get(`/products`, {
      params: queryParams,
    });
  }, {
    enabled: isAuthenticated
  });
};

// Fetch One using ID
export const useFindProject = (id: string) => {
  const { isAuthenticated } = useAuth();

  return useQuery(["project", id], () => {
    return axios.get(`/products/${id}`);
  }, {
    enabled: (id && isAuthenticated) ? true : false
  });
};

// Create
export const useCreateProject = () => {
  return useMutation((data: Project) => {
    return axios.post("/products", data);
  });
};

// Update
export const useUpdateProject = (id: string) => {
  return useMutation((data: Project) => {
    return axios.put(`/products/${id}`, data);
  });
};

// Delete
export const useDeleteProject = (id: string) => {
  return useMutation(() => {
    return axios.delete(`/products/${id}`);
  });
};

References