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:
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>
Would you be interested in joining LogRocket's developer community?
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 nowSOLID principles help us keep code flexible. In this article, we’ll examine all of those principles and their implementation using JavaScript.
JavaScript’s Date API has many limitations. Explore alternative libraries like Moment.js, date-fns, and the new Temporal API.
Explore use cases for using npm vs. npx such as long-term dependency management or temporary tasks and running packages on the fly.
Validating and auditing AI-generated code reduces code errors and ensures that code is compliant.