Huzaima Khan Huzaima is a software engineer with a keen interest in technology. He is always on the lookout for experimenting with new technologies. He is also passionate about aviation and travel.

TanStack Query and WebSockets: Real-time React data fetching

10 min read 2905 104

TanStack Query WebSockets Real-time Data Fetching

In today’s data-driven world, real-time data fetching has become increasingly critical to optimize the performance of modern web applications, like chat apps, social media platforms, and live dashboards. As user expectations evolve, the need for current, dynamic content has become paramount. Here, real-time data fetching comes into play, enabling applications to deliver fresh information without manual user intervention, enhancing the user experience, and ensuring that data remains relevant and accurate.

In this article, we’ll explore using TanStack Query, formerly known as React Query, along with WebSockets to implement real-time data fetching in a React application. Let’s get started!

Jump ahead:

What is TanStack Query?

TanStack Query is a powerful and versatile data-fetching library designed for frontend applications. It provides a streamlined, efficient, and flexible way to handle fetching, caching, and updating data from RESTful APIs, GraphQL, and other data-fetching interfaces. It abstracts complex fetching logic and provides a simple, declarative API to efficiently handle data fetching and management.

TanStack Query offers the following core functionalities:

  1. Data fetching: Provides a simple and efficient way to fetch data from APIs or other data sources
  2. Caching: Automatically caches fetched data to minimize redundant requests and improve application performance
  3. Updating: Seamlessly handles data updates and refetching, ensuring the displayed data is always accurate and up-to-date

Benefits of using TanStack Query for data fetching

Automatic caching and cache management

TanStack Query automatically caches fetched data to optimize performance and reduce the number of API calls, ensuring that repeated requests for the same data are served from the cache instead of initiating new API calls.

Background data updates

TanStack Query can automatically update cached data in the background, ensuring the application always has fresh and up-to-date data. This enables a smoother user experience by reducing load times and avoiding abrupt UI changes.

Query invalidation and refetching

When needed, TanStack Query can intelligently invalidate queries and refetch data. This feature ensures that the application displays the most recent data, even when the underlying data sources change.

Error and loading state handling

TanStack Query manages loading and error states internally, simplifying the handling of these states in the application’s UI. This allows developers to focus on the application’s core logic and presentation.

Flexibility and extensibility

TanStack Query supports various data fetching strategies and can be easily extended to handle custom use cases, thereby making it a versatile solution for any application that relies on external data.

How TanStack Query caches data to improve performance

TanStack Query uses an in-memory cache to store fetched data. When a request is made, the library first checks if the requested data is already available in the cache. If it is, the cached data is returned, and no additional API call is made. If the data is not in the cache, the library makes an API call to fetch it, stores it in the cache, and then returns it to the application.

Caching ensures that repeated requests for the same data are served quickly and efficiently, improving performance and reducing the load on the API or data source. The code below shows an example of how TanStack Query caches data:

import { useQuery } from '@tanstack/react-query';

async function fetchTodos() {
  ...
}

function App() {
  const { data, isLoading, error } = useQuery('todos', fetchTodos);

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

  return (
    <ul>
      {data?.map(todo => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
}

In this example, useQuery checks if the todos data is already available in the cache. If it is, the cached data is returned. Otherwise, the fetchTodos function is called to fetch the data from the API. Let’s review the different approaches TanStack Query follows to cache data.

In-memory caching

By default, TanStack Query stores fetched data in an in-memory cache. This ensures that subsequent requests for the same data can be fulfilled directly from the cache and reduces the need for additional network requests.

Deduplication

TanStack Query automatically detects and deduplicates identical requests. Therefore, only one network request is made for each unique data query, further reducing the number of redundant requests and improving overall performance.

Cache invalidation and garbage collection

TanStack Query intelligently invalidates outdated cache entries and performs garbage collection to free up memory when necessary, thereby ensuring that the cache remains up-to-date and efficient.



How TanStack Query handles fetching and updating data

TanStack Query simplifies the process of fetching and updating data in web applications.

Fetching data

Developers can easily fetch data from APIs or other sources using TanStack Query’s simple API. The library handles network requests, caching, and error handling, allowing developers to focus on their application’s core functionality.

Updating data

When the underlying data changes or new data becomes available, TanStack Query can automatically update and refetch the data. This ensures that the displayed data in the application is always accurate and up-to-date.

Manual refetching

Developers can manually trigger a refetch of data using TanStack Query’s API, which is useful when certain user interactions or events require the data to be refreshed.

WebSockets

WebSockets is a communication protocol that enables bidirectional, full-duplex communication between a client and a server over a single, long-lived connection. WebSockets are designed to work over the same ports as HTTP, port 80, and HTTPS, port 443, making them compatible with the existing web infrastructure. Unlike HTTP, a stateless request-response protocol, WebSockets allow real-time data exchange between the client and the server.

How WebSockets work and how they differ from HTTP requests

WebSockets establish a connection between the client and server, which remains open until it is explicitly closed by either the client or the server. Once the connection is established, both parties can send and receive messages anytime without re-establishing the connection.

HTTP, on the other hand, is based on a request-response model, where the client sends a request to the server and waits for a response. Once the response is received, the connection is closed. For any new request, a new connection must be established, which can result in increased latency.

Benefits of WebSockets for real-time data fetching

There are several advantages to using WebSockets for real-time data fetching, including:

Bidirectional communication

WebSockets enable full-duplex communication, allowing clients and servers to send data to each other simultaneously, which is essential for real-time applications like chat, gaming, or live data feeds.


More great articles from LogRocket:


Lower latency

Because WebSockets use a single, long-lived connection, there is no need to establish a new connection for each data exchange, reducing latency significantly.

Efficient use of resources

With WebSockets, only one connection is used for both sending and receiving data, which can reduce the overhead associated with multiple HTTP requests.

How to use WebSocket in a React application

There are many npm packages available for using WebSockets in React, but for this example, we’ll use react-use-websocket. This package provides a custom React Hook that simplifies connecting and managing WebSocket connections in a React application.

To get started, instal the react-use-websocket package by running the following command in your terminal:

npm install react-use-websocket

Next, we’ll import the useWebSocket custom Hook and create a WebSocket connection in a React component:

import React, { useState, useCallback } from 'react';
import useWebSocket, { ReadyState } from "react-use-websocket";

const WebSocketExample = () => {
  // WebSocket URL
  const socketUrl = 'wss://example.com/websocket';

  // Initialize the WebSocket connection
  const {
    sendMessage,
    lastMessage,
    readyState,
  } = useWebSocket(socketUrl);

  // State to keep track of the messages
  const [messages, setMessages] = useState([]);

  // Callback to handle new messages
  const handleMessage = useCallback((event) => {
    const newMessage = JSON.parse(event.data);
    setMessages((prevMessages) => [...prevMessages, newMessage]);
  }, []);

  // Send a message
  const handleSendMessage = () => {
    sendMessage('Hello, WebSocket!');
  };

  return (
    <div>
      <button onClick={handleSendMessage}>Send Message</button>
      <p>ReadyState: {ReadyState[readyState]}</p>
      <ul>
        {messages.map((message, index) => (
          <li key={index}>{message}</li>
        ))}
      </ul>
    </div>
  );
};

export default WebSocketExample;

In the code above, we import the useWebSocket custom Hook from the react-use-websocket package to establish a WebSocket connection. We pass the WebSocket URL as a parameter to the hook, which returns an object containing the sendMessage function, the lastMessage received, and the readyState of the connection.

We use the useState Hook to store the messages received. We create a handleMessage callback to handle incoming messages and update the messages state. When a new message is received, it is added to the messages array.

We also create a handleSendMessage function to send messages using the sendMessage function provided by the custom hook. Finally, we render a button to send a message, display the current readyState, and list the received messages.

In this example, we demonstrated only the basic usage of the react-use-websocket package to set up a WebSocket connection in a React application. You can customize the connection and handle various events like connection open, close, or error using the additional options provided by the useWebSocket Hook.

Combining TanStack Query and WebSockets to fetch real-time data

Combining TanStack Query and WebSockets allows you to efficiently manage real-time data. You can handle WebSocket connections using the react-use-websocket package and manage server state using TanStack Query.

Set up the server

Below is a simple Node.js server that uses the ws library to handle WebSocket connections and perform the required actions:

// Import WebSocket and HTTP libraries
const WebSocket = require("ws");
const http = require("http");

// Create an HTTP server and a WebSocket server on top of it
const server = http.createServer();
const wss = new WebSocket.Server({ server });

// Initialize an array to store chat messages
let chatMessages = [];
const MESSAGE_TYPE = {
    INITIAL_DATA: "INITIAL_DATA",
    SEND_MESSAGE: "SEND_MESSAGE",
    NEW_MESSAGE: "NEW_MESSAGE",
};

// Listen for new WebSocket connections
wss.on("connection", (ws) => {
    console.log("Client connected.");

    // Send the initial chat messages to the newly connected client
    ws.send(
        JSON.stringify({
            type: MESSAGE_TYPE.INITIAL_DATA,
            payload: chatMessages,
        }),
    );

    // Listen for incoming messages from the client
    ws.on("message", (message) => {
        const parsedMessage = JSON.parse(message);
        if (parsedMessage.type === MESSAGE_TYPE.SEND_MESSAGE) {
            const newMessage = {
                content: parsedMessage.content,
                timestamp: new Date(),
            };
            chatMessages.push(newMessage);
            // Iterate through all connected clients and send the new message to them
            wss.clients.forEach((client) => {
                if (client.readyState === WebSocket.OPEN) {
                    client.send(
                        JSON.stringify({
                            type: MESSAGE_TYPE.NEW_MESSAGE,
                            payload: newMessage,
                        }),
                    );
                }
            });
        }
    });

    // Listen for the 'close' event to log when a client disconnects
    ws.on("close", () => {
        console.log("Client disconnected.");
    });
});

// Start the server and listen on a specific port
const port = process.env.PORT || 3001;
server.listen(port, () => {
    console.log(`Server is running on port ${port}`);
});

This server does the following:

  1. Creates an HTTP server and a WebSocket server on top of it
  2. Stores chat messages in the chatMessages array
  3. Listens for incoming WebSocket connections
  4. When a new client connects, sends the initial chat messages to them
  5. When a new message is received, checks its type. If it is SEND_MESSAGE, it appends the message to chatMessages and broadcasts it to all connected clients
  6. Listens for the close event and logs when a client disconnects
  7. Starts the server and listens on a specific port; the default is 3001 if the environment variable PORT is not set

Set up the client

Let’s create a new project and install the required dependencies:

npx create-react-app tanstack-query-ws
cd tanstack-query-ws
npm install @tanstack/react-query react-use-websocket

Next, create ChatMessageProvider.js to set up the WebSocket connection:

// Import necessary hooks and libraries
import { createContext, useCallback, useContext, useEffect } from "react";
import useWebSocket, { ReadyState } from "react-use-websocket";
import { useQueryClient } from "@tanstack/react-query";

// Create a context for chat messages
const ChatMessagesContext = createContext(null);
const SOCKET_URL = "ws://localhost:3001";
const MESSAGE_TYPE = {
    INITIAL_DATA: "INITIAL_DATA",
    SEND_MESSAGE: "SEND_MESSAGE",
    NEW_MESSAGE: "NEW_MESSAGE",
};
export const queryKey = ["messages"];

// Define the ChatMessagesProvider component to provide chat messages context
export const ChatMessagesProvider = ({ children }) => {
    // Initialize the WebSocket connection and retrieve necessary properties
    const {
        sendMessage: sM,
        lastMessage,
        readyState,
    } = useWebSocket(SOCKET_URL, {
        shouldReconnect: true,
    });
    // Initialize the queryClient from react-query
    const queryClient = useQueryClient();
    // Check if WebSocket connection is open and ready for sending messages
    const canSendMessages = readyState === ReadyState.OPEN;

    // Handle the incoming WebSocket messages
    useEffect(() => {
        if (lastMessage && lastMessage.data) {
            const { type, payload } = JSON.parse(lastMessage.data);
            // Update the local chat messages state based on the message type
            switch (type) {
                case MESSAGE_TYPE.INITIAL_DATA:
                    queryClient.setQueryData(queryKey, () => {
                        return payload;
                    });
                    break;
                case MESSAGE_TYPE.NEW_MESSAGE:
                    queryClient.setQueryData(queryKey, (oldData) => {
                        return [...oldData, payload];
                    });
                    break;
                default:
                    break;
            }
        }
    }, [lastMessage, queryClient]);

    // Define the sendMessage function to send messages through the WebSocket connection
    const sendMessage = useCallback(
        (content) => {
            if (canSendMessages)
                sM(
                    JSON.stringify({
                        type: MESSAGE_TYPE.SEND_MESSAGE,
                        content,
                    }),
                );
        },
        [canSendMessages, sM],
    );

    // Render the ChatMessagesContext.Provider component and pass the necessary values
    return (
        <ChatMessagesContext.Provider value={{ canSendMessages, sendMessage }}>
            {children}
        </ChatMessagesContext.Provider>
    );
};

// Define a custom hook to access the chat messages context
export const useChatMessagesContext = () => useContext(ChatMessagesContext);

The code above defines a chat messages context and a ChatMessagesProvider component using React, react-use-websocket, and @tanstack/react-query. It initializes a WebSocket connection, handles incoming messages, and updates the chat messages state in react-query.

We define the sendMessage function to send messages through the WebSocket connection; to access the chat messages context in other components, we use the custom provided useChatMessagesContext Hook.

In your index.js file, wrap your application with the ChatMessagesProvider and QueryClientProvider:

import React from "react";
import ReactDOM from "react-dom";
import { QueryClient, QueryClientProvider } from "react-query";
import { WebSocketProvider } from "./WebSocketProvider";
import App from "./App";

const queryClient = new QueryClient();

ReactDOM.render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <WebSocketProvider>
        <App />
      </WebSocketProvider>
    </QueryClientProvider>
  </React.StrictMode>,
  document.getElementById("root")
);

Now, in your App.js file, you can use the useQuery and useMutation Hooks to send and receive real-time data through the WebSocket connection:

import React, { useState } from "react";
import { useQuery, useMutation } from "@tanstack/react-query";
import { queryKey, useChatMessagesContext } from "./ChatMessageProvider";

const App = () => {
    // Initialize local state for message input
    const [message, setMessage] = useState("");

    // Use the chat messages from the query client
    const { data } = useQuery(queryKey, () => {}, {
        staleTime: Infinity,
        cacheTime: Infinity,
    });

    // Access the sendMessage function and canSendMessages from the context
    const { sendMessage, canSendMessages } = useChatMessagesContext();

    // Define the mutation for sending a new message
    const mutation = useMutation((newMessage) => {
        sendMessage(newMessage);
    });

    // Handle the form submission for sending a new message
    const onSubmit = (event) => {
        event.preventDefault();
        mutation.mutate(message);
        setMessage("");
    };

    return (
        <div>
            <h1>Chat Messages</h1>
            <form onSubmit={onSubmit}>
                {/* Update the message state with the input value */}
                <input
                    type="text"
                    value={message}
                    onChange={(event) => setMessage(event.target.value)}
                />
                {/* Disable the send button if the WebSocket connection is not open */}
                <button type="submit" disabled={!canSendMessages || !message}>
                    Send Message
                </button>
            </form>
            <div>
                {/* Display the chat messages */}
                {data?.map(({ content, timestamp }) => (
                    <>
                        <div>{new Date(timestamp).toLocaleString()}</div>
                        <div>{content}</div>
                        <hr />
                    </>
                ))}
            </div>
        </div>
    );
};

export default App;

The App component manages the state for the message input, uses the chat messages from the query client, and accesses the sendMessage function and canSendMessages from the context. It defines a mutation for sending a new message and handles form submission for sending it.

The component renders a form for the user to input, send, and display the chat messages. The send button is disabled if the WebSocket connection is not open or the message input is empty.

With these changes, your application can now send and receive real-time data using TanStack Query and WebSockets. The useQuery hook fetches the initial data, the useMutation hook sends mutations through the WebSocket connection, and the WebSocket provider listens for real-time updates to update the query data accordingly.

Conclusion

In this article, we demonstrated how to fetch real-time data in a React application using TanStack Query and WebSockets.

By leveraging TanStack Query’s powerful state management features, you can efficiently handle server state, cache data, and perform background fetching. Additionally, WebSockets provide low latency, bidirectional communication between the client and server, which is crucial for real-time applications. By combining these technologies, you can efficiently create real-time applications like chat applications, live data feeds, or collaborative editing tools.

This combination enables seamless real-time data fetching and synchronization, enhancing the user experience and performance of your web applications. As you integrate TanStack Query and WebSockets into your React applications, you’ll be able to handle large-scale, real-time data without compromising on responsiveness or user experience. Furthermore, the example provided in this guide can serve as a foundation for more complex scenarios, like handling multiple data streams, managing user authentication, or scaling your application.

Overall, using TanStack Query and WebSockets in your React applications will empower you to build robust and scalable real-time applications, allowing you to stay ahead in the competitive landscape of modern web development.

Cut through the noise of traditional React error reporting with LogRocket

LogRocket is a React analytics solution that shields you from the hundreds of false-positive errors alerts to just a few truly important items. LogRocket tells you the most impactful bugs and UX issues actually impacting users in your React applications. LogRocket automatically aggregates client side errors, React error boundaries, Redux state, slow component load times, JS exceptions, frontend performance metrics, and user interactions. Then LogRocket uses machine learning to notify you of the most impactful problems affecting the most users and provides the context you need to fix it.

Focus on the React bugs that matter — .

Huzaima Khan Huzaima is a software engineer with a keen interest in technology. He is always on the lookout for experimenting with new technologies. He is also passionate about aviation and travel.

Leave a Reply