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?
- Benefits of using TanStack Query for data fetching
- How TanStack Query caches data to improve performance
- How TanStack Query handles fetching and updating data
- WebSockets
- How WebSockets work and how they differ from HTTP requests
- Benefits of WebSockets for real-time data fetching
- How to use WebSocket in a React application
- Combining TanStack Query and WebSockets to fetch real-time data
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:
- Data fetching: Provides a simple and efficient way to fetch data from APIs or other data sources
- Caching: Automatically caches fetched data to minimize redundant requests and improve application performance
- 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:
- Don't miss a moment with The Replay, a curated newsletter from LogRocket
- Learn how LogRocket's Galileo cuts through the noise to proactively resolve issues in your app
- Use React's useEffect to optimize your application's performance
- Switch between multiple versions of Node
- Discover how to animate your React app with AnimXYZ
- Explore Tauri, a new framework for building binaries
- Advisory boards aren’t just for executives. 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.
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:
- Creates an HTTP server and a WebSocket server on top of it
- Stores chat messages in the
chatMessages
array - Listens for incoming WebSocket connections
- When a new client connects, sends the initial chat messages to them
- When a new message is received, checks its type. If it is
SEND_MESSAGE
, it appends the message tochatMessages
and broadcasts it to all connected clients - Listens for the
close
event and logs when a client disconnects - Starts the server and listens on a specific port; the default is
3001
if the environment variablePORT
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.

Focus on the React bugs that matter — try LogRocket today.