Handling Asynchronous Operations with React Query

React Query is a powerful library for managing server-state in React applications. It simplifies fetching, caching, synchronizing, and updating server state. Here’s a guide to effectively handling asynchronous operations using React Query.

1. Setting Up the Project

  • Initialize the Project:

    npx create-react-app react-query-demo --template typescript
    cd react-query-demo
  • Install React Query:

    npm install react-query
    npm install @types/react-query

2. Setting Up React Query Provider

  • Wrap Your App with QueryClientProvider:
    // src/index.tsx
    import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import { QueryClient, QueryClientProvider } from 'react-query'; const queryClient = new QueryClient(); ReactDOM.render( <React.StrictMode> <QueryClientProvider client={queryClient}> <App /> </QueryClientProvider> </React.StrictMode>, document.getElementById('root') );

3. Fetching Data

  • Create a Fetch Function:

    // src/api.ts
    export const fetchUsers = async () => { const response = await fetch('https://jsonplaceholder.typicode.com/users'); if (!response.ok) { throw new Error('Network response was not ok'); } return response.json(); };
  • Use useQuery Hook:

    // src/components/UserList.tsx
    import React from 'react'; import { useQuery } from 'react-query'; import { fetchUsers } from '../api'; const UserList: React.FC = () => { const { data, error, isLoading } = useQuery('users', fetchUsers); if (isLoading) return <div>Loading...</div>; if (error) return <div>Error: {(error as Error).message}</div>; return ( <div> <h1>User List</h1> <ul> {data.map((user: { id: number; name: string }) => ( <li key={user.id}>{user.name}</li> ))} </ul> </div> ); }; export default UserList;

4. Mutations

  • Create a Mutation Function:

    // src/api.ts
    export const addUser = async (user: { name: string }) => { const response = await fetch('https://jsonplaceholder.typicode.com/users', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(user), }); if (!response.ok) { throw new Error('Network response was not ok'); } return response.json(); };
  • Use useMutation Hook:

    // src/components/AddUser.tsx
    import React, { useState } from 'react'; import { useMutation, useQueryClient } from 'react-query'; import { addUser } from '../api'; const AddUser: React.FC = () => { const [name, setName] = useState(''); const queryClient = useQueryClient(); const mutation = useMutation(addUser, { onSuccess: () => { queryClient.invalidateQueries('users'); }, }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); mutation.mutate({ name }); setName(''); }; return ( <form onSubmit={handleSubmit}> <input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder="Enter name" required /> <button type="submit">Add User</button> {mutation.isError && <div>Error adding user</div>} {mutation.isSuccess && <div>User added successfully</div>} </form> ); }; export default AddUser;

5. Error Handling

  • Handle Errors Gracefully:
    // src/components/UserList.tsx
    import React from 'react'; import { useQuery } from 'react-query'; import { fetchUsers } from '../api'; const UserList: React.FC = () => { const { data, error, isLoading } = useQuery('users', fetchUsers); if (isLoading) return <div>Loading...</div>; if (error) return <div>Error: {(error as Error).message}</div>; return ( <div> <h1>User List</h1> <ul> {data.map((user: { id: number; name: string }) => ( <li key={user.id}>{user.name}</li> ))} </ul> </div> ); }; export default UserList;

6. Optimistic Updates

  • Implement Optimistic Updates:
    // src/components/AddUser.tsx
    import React, { useState } from 'react'; import { useMutation, useQueryClient } from 'react-query'; import { addUser } from '../api'; const AddUser: React.FC = () => { const [name, setName] = useState(''); const queryClient = useQueryClient(); const mutation = useMutation(addUser, { onMutate: async (newUser) => { await queryClient.cancelQueries('users'); const previousUsers = queryClient.getQueryData('users'); queryClient.setQueryData('users', (old: any) => [...old, { id: Date.now(), ...newUser }]); return { previousUsers }; }, onError: (err, newUser, context) => { queryClient.setQueryData('users', context?.previousUsers); }, onSettled: () => { queryClient.invalidateQueries('users'); }, }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); mutation.mutate({ name }); setName(''); }; return ( <form onSubmit={handleSubmit}> <input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder="Enter name" required /> <button type="submit">Add User</button> {mutation.isError && <div>Error adding user</div>} {mutation.isSuccess && <div>User added successfully</div>} </form> ); }; export default AddUser;

7. Caching and Invalidating Data

  • Invalidate Queries:
    // src/components/UserList.tsx
    import React from 'react'; import { useQuery, useQueryClient } from 'react-query'; import { fetchUsers } from '../api'; const UserList: React.FC = () => { const queryClient = useQueryClient(); const { data, error, isLoading } = useQuery('users', fetchUsers); if (isLoading) return <div>Loading...</div>; if (error) return <div>Error: {(error as Error).message}</div>; return ( <div> <h1>User List</h1> <button onClick={() => queryClient.invalidateQueries('users')}>Refresh</button> <ul> {data.map((user: { id: number; name: string }) => ( <li key={user.id}>{user.name}</li> ))} </ul> </div> ); }; export default UserList;

By following these steps, you can effectively manage asynchronous operations in your React applications using React Query. This approach simplifies data fetching, caching, synchronization, and updating, leading to a more robust and scalable application.