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:
The Replay is a weekly newsletter for dev and engineering leaders.
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.
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:
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.
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.
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.
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.
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.
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.
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.
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.
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.
TanStack Query simplifies the process of fetching and updating data in web applications.
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.
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.
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 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.
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.
There are several advantages to using WebSockets for real-time data fetching, including:
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.
Because WebSockets use a single, long-lived connection, there is no need to establish a new connection for each data exchange, reducing latency significantly.
With WebSockets, only one connection is used for both sending and receiving data, which can reduce the overhead associated with multiple HTTP requests.
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 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.
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:
chatMessages arraySEND_MESSAGE, it appends the message to chatMessages and broadcasts it to all connected clientsclose event and logs when a client disconnects3001 if the environment variable PORT is not setLet’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.
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.
Install LogRocket via npm or script tag. LogRocket.init() must be called client-side, not
server-side
$ npm i --save logrocket
// Code:
import LogRocket from 'logrocket';
LogRocket.init('app/id');
// Add to your HTML:
<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>

Build an AI assistant with Vercel AI Elements, which provides pre-built React components specifically designed for AI applications.

line-clamp to trim lines of textMaster the CSS line-clamp property. Learn how to truncate text lines, ensure cross-browser compatibility, and avoid hidden UX pitfalls when designing modern web layouts.

Discover seven custom React Hooks that will simplify your web development process and make you a faster, better, more efficient developer.

Promise.all still relevant in 2025?In 2025, async JavaScript looks very different. With tools like Promise.any, Promise.allSettled, and Array.fromAsync, many developers wonder if Promise.all is still worth it. The short answer is yes — but only if you know when and why to use it.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up now