eMoosavi
Weekly frontend dev braindumps
Streamlining State Management with React-Query and Custom Hooks
Tooling and Libraries

Streamlining State Management with React-Query and Custom Hooks

Master the art of managing server state in your React applications with efficient patterns.

Aug 06, 2024 - 14:086 min read

In modern web development, especially within the React ecosystem, efficient state management is crucial for building responsive applications. Traditionally, local state management with hooks like useState and useReducer suffices for many UI interactions. However, when it comes to handling server state—data fetched from APIs—the approach becomes more complex. Enter React Query: a library that simplifies server state management and provides powerful features like caching, synchronization, and background updates. This post will deep-dive into advanced techniques for leveraging React Query alongside custom hooks to streamline state management in your React applications.

Understanding Server State

To fully appreciate the power of React Query, it’s essential first to understand what we mean by server state. Server state includes data that resides on a server and can be fetched through API calls. Unlike local state, which is confined to the lifecycle of the component, server state is dynamic and can change outside the component’s lifecycle. Therefore, managing server state often requires more than just local state handling—this is where React Query shines.

Common Challenges in Managing Server State

Before using React Query, developers often face several challenges while managing server states, including:

  • Fetching data: Knowing when and how to fetch data—e.g., on component mount vs. on user interaction.
  • Caching: Efficiently managing API responses to avoid redundant network requests.
  • Synchronization: Keeping the UI in sync with the server state, particularly when it changes outside of our control.
  • Error handling: Appropriately handling errors that can arise from network issues or API responses.

Introduction to React Query

React Query is a powerful data fetching library that abstracts these complexities and empowers developers to easily manage server state. It comes with built-in features like:

  • Automatic caching: Responses are cached and automatically updated in the background.
  • Query invalidation: Automatically refetching relies on the cache, ensuring your UI stays up-to-date.
  • Retries and error handling: Built-in support for retries and thorough error handling mechanisms.
  • Optimistic updates: Helps in implementing seamless user experiences while waiting for the server responses.

Getting Started with React Query

Let’s begin with React Query's basic configurations:

  1. Installation: Install React Query in your project using npm or yarn:

    npm install react-query
    
  2. Setting up QueryClient: Use QueryClientProvider to wrap your application and setup a QueryClient instance:

    import { QueryClient, QueryClientProvider } from 'react-query';
    const queryClient = new QueryClient();
    
    function App() {
      return (
        <QueryClientProvider client={queryClient}>
          <YourComponent />
        </QueryClientProvider>
      );
    }
    
  3. Using Queries: Utilize the useQuery hook to fetch server data. Here’s a simple example:

    import { useQuery } from 'react-query';
    import axios from 'axios';
    
    const fetchPosts = async () => {
      const { data } = await axios.get('https://jsonplaceholder.typicode.com/posts');
      return data;
    };
    
    function Posts() {
      const { data, error, isLoading } = useQuery('posts', fetchPosts);
      
      if (isLoading) return <div>Loading...</div>;
      if (error) return <div>Error: {error.message}</div>;
      
      return (
        <ul>
          {data.map((post) => (
            <li key={post.id}>{post.title}</li>
          ))}
        </ul>
      );
    }
    

Benefits of Using React Query

  1. Declarative code: Your data fetching logic is declared alongside your render, making it easier to read and maintain.
  2. Improved performance: Automatic caching and background synchronization help keep your application responsive.
  3. Less boilerplate: React Query abstracts away much of the fetching logic, significantly reducing the amount of code you write.

Custom Hooks with React Query

While React Query provides powerful hooks out of the box, creating custom hooks can enhance functionality and reuse logic across components. This approach allows you to bundle logic that interacts with your server into simple, reusable APIs.

Creating a Custom Hook

Here, we will create a custom hook to manage user authentication data:

import { useQuery } from 'react-query';
import axios from 'axios';

const fetchUser = async (userId) => {
  const { data } = await axios.get(`https://api.example.com/users/${userId}`);
  return data;
};

export const useUser = (userId) => {
  return useQuery(['user', userId], () => fetchUser(userId));
};

Using this custom hook is straightforward and keeps your components clean:

function UserProfile({ userId }) {
  const { data: user, error, isLoading } = useUser(userId);

  if (isLoading) return <div>Loading profile...</div>;
  if (error) return <div>Error loading profile: {error.message}</div>;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

Optimistic Updates in React Query

Optimistic updates are essential for a great user experience, especially when making changes that involve server-state transitions (like adding or deleting items). React Query allows you to perform these updates easily.

Implementing Optimistic Updates

Let's say we want to add a new post to our list optimistically. Here’s how we can do it:

const addPost = async (newPost) => {
  const { data } = await axios.post('https://jsonplaceholder.typicode.com/posts', newPost);
  return data;
};

export const useAddPost = () => {
  const queryClient = useQueryClient();

  return useMutation(addPost, {
    onMutate: async (newPost) => {
      await queryClient.cancelQueries('posts');

      const previousPosts = queryClient.getQueryData('posts');

      queryClient.setQueryData('posts', (old) => [...old, newPost]);

      return { previousPosts };
    },
    onError: (err, newPost, context) => {
      queryClient.setQueryData('posts', context.previousPosts);
    },
    onSettled: () => {
      queryClient.invalidateQueries('posts');
    },
  });
};

This hook will optimistically append the new post to the cache while simultaneously ensuring that on error, the cache reverts back to its previous state.

Integrating Optimistic Updates in a Component

You can use this mutation in a component as follows:

function AddPostForm() {
  const [title, setTitle] = useState('');
  const mutation = useAddPost();

  const handleSubmit = (e) => {
    e.preventDefault();
    mutation.mutate({ title });
    setTitle(''); // Clear input
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={title} onChange={(e) => setTitle(e.target.value)} />
      <button type="submit">Add Post</button>
      {mutation.isError && <p>Error adding post</p>}
    </form>
  );
}

Advanced Caching Strategies

While React Query handles caching out of the box, occasionally, you may want to implement a more customized caching strategy.

Stale-While-Revalidate Strategy

You might want to enable your UI to show stale data while refetching in the background:

const { data } = useQuery('posts', fetchPosts, {
  staleTime: 5000, // Keep data fresh for 5 seconds
});

This setup allows users to view the cached posts while re-fetching the data within the defined stale time.

Pagination and Infinite Queries

React Query makes it easy to handle pagination and infinite scrolling. By customizing the query keys and integrating fetchNextPage() with the useInfiniteQuery hook, you can streamline endless lists of data fetching effortlessly.

const fetchProjects = async ({ pageParam = 1 }) => {
  const { data } = await axios.get(`https://api.example.com/projects?page=${pageParam}`);
  return data;
};

const { data, fetchNextPage, hasNextPage } = useInfiniteQuery('projects', fetchProjects, {
  getNextPageParam: (lastPage) => lastPage.nextPage,
});

Utilizing these features, you can create smooth infinite scroll experiences in your apps.

Conclusion

Leveraging React Query in tandem with custom hooks truly transforms the way we manage server state in our applications. By encapsulating your data-fetching logic within custom hooks, you gain cleaner components, enhanced reusability, and better separation of concerns. With concurrent features, optimistic updates, and fine-grained caching strategies, you’ll be equipped to handle even the most complex data interactions in a scalable way. Implement these techniques in your next project, and take your React applications to the next level! Let’s embrace efficient data handling with React Query, creating a smoother and more intuitive user experience for our users.

Article tags
reactreact-querycustom-hooksstate-managementserver-state
Previous article

React Components

Understanding Render Prop Techniques in React

Next article

React Components

Optimizing React with Concurrent Features