Hello! Welcome to the next lesson in our journey to becoming a Solana developer.
In our previous module, we focused on building the user-facing side of our dApps with React. We learned how to connect to wallets, display data, and construct and send transactions. A key part of that was fetching data from the Solana network to display in our UI, like a user's SOL balance or the state of a program account.
This lesson kicks off our new module on advanced state management. We'll build on what you've learned and address a crucial aspect of frontend development that becomes even more important in the Web3 world: efficient data fetching. We'll explore why the standard useEffect and useState approach can be inefficient for dApps and how modern caching libraries can dramatically improve performance and user experience.
Today, you will learn how to integrate a client-side caching library (like SWR or React Query) to manage fetching and caching of on-chain data. By the end of this lesson, you'll understand the core principles of these libraries and see how they are used in professional Solana development kits to create fast, responsive, and efficient dApps.
The Problem with "Naive" Data Fetching
As an experienced frontend developer, you're likely familiar with the challenges of managing asynchronous data. In a typical React application, a common pattern for fetching data is using useEffect to make an API call when a component mounts and useState to store the result, along with loading and error states.
// A simplified version of what we've done so far
function MyComponent() {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
const { connection } = useConnection();
useEffect(() => {
const fetchData = async () => {
try {
setIsLoading(true);
const result = await connection.getAccountInfo(somePublicKey);
setData(result);
} catch (e) {
setError(e);
} finally {
setIsLoading(false);
}
};
fetchData();
}, [connection]); // Dependency array can get complex
// ... render logic based on isLoading, error, data ...
}
This works, but it has several drawbacks, which are amplified in a dApp context:
- Boilerplate: You repeat the
isLoading/error/datastate logic for every piece of data you fetch. - No Caching: If two different components on the page need the same data (e.g., the user's SOL balance), they will both fetch it independently, making redundant calls to the RPC endpoint.
- Stale Data: Once fetched, the data is static. On-chain data changes frequently. How do you re-fetch it? You might add a refresh button, but what about re-fetching automatically when the user switches back to your browser tab after signing a transaction in their wallet?
- RPC Costs & Rate-Limiting: Redundant calls can lead to slower performance and potentially get you rate-limited by your RPC provider, or even incur extra costs on paid plans.
Modern data-fetching libraries solve all of these problems with an elegant and powerful approach.
The Solution: Stale-While-Revalidate
The two leading libraries in the React ecosystem for this are React Query (now TanStack Query) and SWR. Both are built around a caching strategy named stale-while-revalidate. The idea is simple:
- When a component needs data, the library first returns the cached (stale) data immediately. This makes the UI feel fast.
- Then, it sends a re-validation request in the background to fetch fresh data.
- Once the fresh data arrives, it updates the UI.
This strategy provides a great user experience by showing data instantly while ensuring it's kept up-to-date. These libraries also automatically handle caching, request deduplication, and background updates.
An Introduction to React Query and SWR
Let's get a quick overview of both libraries. They are conceptually very similar, so understanding one makes it easy to pick up the other.
First, watch this quick introduction to React Query to grasp the core benefits.
This video from Fireship provides a fast-paced, high-level overview of what React Query does and why it's so useful.
Watch the full video. It's short and covers the key concepts: the problem it solves, the useQuery hook for fetching, and the useMutation hook for updating data and invalidating the cache.
Next, let's look at SWR, which stands for stale-while-revalidate. This video provides a slightly more hands-on demonstration of its core features.
SWR the data fetching hero of ReactJS // 5 minute tutorial
This tutorial on SWR clearly demonstrates how it simplifies data fetching and automatically handles request deduplication, a key feature for optimizing dApps.
Watch the video from the beginning until 04:31. Pay close attention to how useSWR replaces useEffect and useState, and notice the section where two components request the same data but only one API call is made.
To summarize, both libraries offer:
- A hook (
useQueryoruseSWR) that takes a unique key and a fetcher function. - The key uniquely identifies the data. The library uses it for caching.
- The fetcher is your actual data-fetching logic (e.g., a call to
connection.getAccountInfo). - They return objects containing
data,isLoading(orisValidating), anderror. - Automatic refetching on window focus, network reconnection, etc.
- Deduplication of simultaneous requests for the same key.
Applying Caching to Solana Data
Now, let's see how to apply this pattern to our Solana dApp. Imagine we want to create a reusable hook, useAccountInfo, to fetch data for any given public key.
Our fetcher function will simply be a wrapper around the @solana/web3.js call. The key is the most important part. It needs to contain all the information required to uniquely identify and re-run the request.
Here's how we could build useAccountInfo using SWR:
import useSWR from 'swr';
import { useConnection } from '@solana/wallet-adapter-react';
import { PublicKey } from '@solana/web3.js';
// The fetcher function receives the key as its argument.
// We'll structure our key as an array: [connection, 'getAccountInfo', publicKeyString]
const solanaFetcher = async ([connection, method, ...args]) => {
if (!connection || !method) {
throw new Error('Connection or method not available');
}
// This is a generic fetcher that can call any method on the connection object.
return await connection[method](...args);
};
export const useAccountInfo = (publicKey: PublicKey | null | undefined) => {
const { connection } = useConnection();
// The key is an array. SWR will only run the fetcher if the key is not null.
// We stringify the public key to ensure the key is serializable and stable.
const key = publicKey ? [connection, 'getAccountInfo', publicKey.toBase58()] : null;
// We need to pass the public key object itself to the fetcher, not the string.
// SWR doesn't directly support this, so we modify the fetcher slightly.
const fetcherWithPublicKey = async ([conn, method, pubKeyStr]) => {
return await conn[method](new PublicKey(pubKeyStr));
}
const { data, error, isValidating } = useSWR(key, fetcherWithPublicKey, {
// Optional: configuration like refresh interval
// refreshInterval: 30000, // Refresh every 30 seconds
});
return {
accountInfo: data,
loading: isValidating,
error,
};
};
Look how clean that is! All the complex logic of state management, caching, and re-fetching is handled by SWR. If we call useAccountInfo(myPublicKey) in five different components, only one request will be sent to the RPC node. When the user clicks back to our tab, SWR will automatically re-fetch the account info to check for updates.
Test your understanding!
You need to create a new hook, useTokenBalance, that fetches the balance of a specific SPL Token account. The relevant @solana/web3.js method is connection.getTokenAccountBalance(tokenAccountPublicKey).
How would you define the key for the useSWR hook to ensure the balance is fetched and cached correctly?
Show answer
A good key would be an array that includes the connection object, the name of the method, and the public key of the token account. For example:
const key = tokenAccountPublicKey ? [connection, 'getTokenAccountBalance', tokenAccountPublicKey.toBase58()] : null;
This key is unique for each token account and contains all the information needed for the fetcher to execute the request. SWR will use this exact key to cache the result.
Caching in the Wild: Professional Solana SDKs
This pattern of building custom hooks on top of a caching library is a professional standard. You will often find it abstracted away inside high-level SDKs. Let's look at two real-world examples.
First, the OneShot SDK is a tool for building DeFi swap interfaces. Read the following documentation, which explicitly mentions its use of SWR.
OneShot SDK (v2) - Shogun Docs
The documentation for the OneShot SDK explains its core concepts and how it uses SWR to power its React hooks for fetching balances and quotes.
Read the sections 'Core Concepts', 'Key Hooks', and 'State Management'. Notice how they describe their hooks (useQuote, useBalances) as being 'backed by SWR' to provide a 'central cache'.
As you can see, they've done exactly what we discussed: created domain-specific hooks like useBalances that use SWR under the hood. This gives developers a simple, powerful, and efficient API for fetching on-chain data without worrying about the implementation details of caching.
Here is another example. The Solana App Kit is a community project that provides a set of reusable modules for building dApps. Its "Data Module" is designed for fetching on-chain information.
The Solana App Kit's Data Module provides a set of hooks for fetching token data, NFTs, and market information, with caching built-in for performance.
First, skim the 'Core Functionalities' to see what kind of data it handles. Then, read the 'Essential Hooks' section to see the API of hooks like useFetchTokens. Finally, look at the 'Performance Optimization' section, which explicitly confirms that the module uses caching.
The useFetchTokens hook from this kit returns { tokens, loading, error, refetch }—an API that should now look very familiar to you. While the documentation doesn't specify whether it uses SWR or React Query, it's clear that it implements the same principles. It provides a high-level abstraction for fetching data, with performance optimizations like caching handled for you.
Conclusion
In this lesson, we've elevated our approach to data fetching from a basic useEffect pattern to a robust, production-grade strategy using client-side caching libraries.
Here are the key takeaways:
- Relying solely on
useEffectanduseStatefor data fetching in dApps leads to performance issues, redundant RPC calls, and a poor user experience. - Libraries like SWR and React Query solve these problems by implementing the
stale-while-revalidatecaching strategy. - They provide hooks that manage loading/error states, deduplicate requests, and automatically keep data fresh.
- The standard pattern in modern dApp development is to create custom, domain-specific hooks (e.g.,
useAccountInfo,useTokenBalance) that use a caching library internally. - Many Solana-focused SDKs already use this pattern to provide developers with efficient, easy-to-use hooks for accessing on-chain data.
By integrating a caching library, you can build dApps that are not only functional but also fast, efficient, and responsive to on-chain events.
In our next lesson, we will tackle the other side of client-side state: global application state. We'll explore how to manage data that needs to be shared across your entire application, such as wallet connection status and user information, using tools like Zustand and React Context.
Can't find a good explanation? Sign up and we'll make it for you
Sign up