Hello! Welcome back to our module on advanced state management in your Solana dApp.
In our last lesson, we focused on efficiently managing "server state"—the on-chain data your application consumes. You learned how to use client-side caching libraries like SWR or React Query to avoid redundant RPC calls and keep your UI snappy and up-to-date.
Today, we'll shift our focus to the other side of the coin: "client state." This is the data that originates and lives within your application, such as user interface state, session information, and, most importantly in a dApp, the user's wallet connection status. You will learn how to implement a global state management solution (using React Context and Zustand) for wallet and user data. By the end of this lesson, you'll understand how to leverage built-in solutions for wallet state and how to create your own flexible state stores for other global data.
The Two Sides of State in a dApp
As an experienced frontend developer, you're well-versed in state management. In the context of a dApp, it's helpful to categorize state into two types:
- Server State: Data that lives outside your application, in our case, on the Solana blockchain. We use libraries like SWR (as we saw last lesson) to fetch, cache, and synchronize this data.
- Client State: Data that is local to your application. This includes UI state (e.g., is a modal open?), user preferences (e.g., dark mode), and session information (e.g., is the user's wallet connected? What is their public key?).
While caching libraries are perfect for server state, we need a different set of tools for managing client state, especially when it needs to be shared across many components. This is where global state management comes in.
The Standard: React Context for Wallet Management
The most common piece of global state in any dApp is the wallet connection. Your entire application—from the header displaying the user's address to the button that initiates a transaction—needs to know if a wallet is connected and which one it is. Passing this information down through props ("prop drilling") would be highly impractical.
The Solana Wallet Adapter library, which you've already started using, solves this problem elegantly using React Context. It provides a set of "Provider" components that wrap your application and make wallet and connection data available everywhere.
If you need a quick refresher on the core mechanics of React Context, this short video is an excellent summary.
Explained in the fastest way: React Context API
This video from onjsdev quickly explains the problem of prop drilling and the three steps of using the Context API: create, provide, and consume.
Watch the full video to get a concise overview of how React Context works. It will serve as a great foundation for understanding how the Solana Wallet Adapter is structured.
Implementing Wallet Context with Solana Wallet Adapter
Now, let's see this pattern in action. The Wallet Adapter provides ConnectionProvider, WalletProvider, and WalletModalProvider to do the heavy lifting for us. Our job is simply to configure and place them at the root of our application.
The Solana Cookbook provides the standard boilerplate for this setup.
How to Connect a Wallet with React
This Solana Cookbook entry demonstrates the canonical way to integrate the wallet adapter's context providers into a React or Next.js application.
Focus on the code blocks under the 'Create Solana Provider' and 'Wrap the Application in the Solana Provider' sections. Observe how ConnectionProvider and WalletProvider are used to wrap the {children}, making the context available to the entire app.
As the guide shows, you create a top-level provider component (e.g., SolanaProvider) that configures the RPC endpoint and available wallets. You then wrap your entire application layout with this provider.
Once the providers are in place, any child component can access the wallet and connection state using the custom hooks useConnection and useWallet.
Let's watch a practical demonstration of how these providers and hooks work together.
Solana Smart Contract Tutorial: Using Phantom Wallet to create a DApp | React, Anchor
In this tutorial, Josh's DevBox walks through setting up and using the wallet adapter, explaining how the providers create a global context.
Watch from 10:22 to 14:35. Pay close attention to how the ConnectionProvider and WalletProvider are set up in App.tsx, and then how useConnection and useWallet are used in a child component to access the global state and send a transaction.
By using the Wallet Adapter's context, you get:
- Global Access: The
connectionobject and thewalletobject (containing the public key, signTransaction function, etc.) are available everywhere. - Reactivity: Your components will automatically re-render when the wallet state changes (e.g., when the user connects or disconnects).
- Clean Architecture: You avoid prop drilling and keep your components focused on their specific tasks.
Test your understanding!
A teammate suggests using the useWallet hook inside a single, low-level component and then passing the publicKey up through several parent components via callback functions to display it in the header. Why is this approach not ideal, and what pattern should be used instead?
Show answer
This approach is not ideal because it is an example of "state lifting" that reintroduces the complexity that global state management is meant to solve. It creates unnecessary coupling between components and makes the code harder to maintain.
The correct pattern is to have the header component also call the useWallet hook directly. Since the WalletProvider is at the root of the app, both the low-level component and the header can independently access the same global wallet state without needing to pass data between them.
Beyond Context: Flexible State Management with Zustand
React Context is excellent for providing stable, app-wide data like the wallet connection. However, for more dynamic or complex client state, relying exclusively on Context can lead to performance issues. Every time a value in a context changes, all components consuming that context will re-render, which can be inefficient.
This is where lightweight state management libraries like Zustand shine. Zustand provides a simple, hook-based API for creating shared state stores without the need for provider wrappers.
Key benefits of Zustand include:
- Minimal Boilerplate: Define a store with a simple
createfunction. - No Providers: You don't need to wrap your app. Just import the hook and use it.
- Performance: Components only re-render when the specific piece of state they subscribe to actually changes.
Professional Web3 libraries often recommend tools like Zustand for managing application state.
The documentation for the LI.FI Widget, a complex Web3 component, explicitly recommends using a state management library like Zustand for managing its configuration.
Read the paragraph under 'State management with widget config'. Note how it highlights Zustand for its ability to optimize re-renders and manage state from any part of the application.
Creating a Zustand Store
Let's imagine our dApp has some user-configurable settings that we want to manage globally, like slippage tolerance for a DEX or a theme preference. A Zustand store is perfect for this.
First, you'd install it: npm install zustand.
Then, you create a store in its own file (e.g., src/store/settings.ts):
import { create } from 'zustand'
// Define the shape of your state and the actions that can modify it
interface SettingsState {
slippageBps: number; // Slippage in basis points (e.g., 50 for 0.5%)
priorityFee: number; // In micro-lamports
theme: 'light' | 'dark';
setSlippageBps: (bps: number) => void;
setPriorityFee: (fee: number) => void;
toggleTheme: () => void;
}
// Create the store
export const useSettingsStore = create<SettingsState>((set) => ({
// Initial state
slippageBps: 50,
priorityFee: 1000,
theme: 'dark',
// Actions to update the state
setSlippageBps: (bps) => set({ slippageBps: bps }),
setPriorityFee: (fee) => set({ priorityFee: fee }),
toggleTheme: () => set((state) => ({
theme: state.theme === 'dark' ? 'light' : 'dark'
})),
}));
Using the Store in a Component
Now, any component can use this store without any providers.
import { useSettingsStore } from '../store/settings';
function SlippageSelector() {
// Select only the state and actions you need.
// This component will ONLY re-render if slippageBps or setSlippageBps changes.
const slippageBps = useSettingsStore((state) => state.slippageBps);
const setSlippageBps = useSettingsStore((state) => state.setSlippageBps);
return (
<div>
<label>Slippage Tolerance</label>
<input
type="number"
value={slippageBps / 100} // Display as percentage
onChange={(e) => setSlippageBps(Number(e.target.value) * 100)}
step="0.1"
/> %
</div>
);
}
function ThemeToggler() {
// This component subscribes to a different piece of state.
const toggleTheme = useSettingsStore((state) => state.toggleTheme);
const theme = useSettingsStore((state) => state.theme);
return <button onClick={toggleTheme}>Switch to {theme === 'dark' ? 'Light' : 'Dark'} Mode</button>
}
Notice how SlippageSelector and ThemeToggler are completely decoupled. When the theme is toggled, only ThemeToggler will re-render, even though they both use the same store. This is the power of selector-based subscriptions that Zustand provides out of the box.
Conclusion
In this lesson, we explored how to manage global client state in a Solana dApp, making a clear distinction between pre-built solutions for wallet data and flexible tools for your own custom state.
Here are your key takeaways:
- Client state (like wallet connection) and server state (on-chain data) require different management strategies.
- The Solana Wallet Adapter uses React Context to provide a robust, ready-to-use global state solution for wallet and connection data via its Providers and hooks (
useWallet,useConnection). - For custom global state (e.g., UI settings, user preferences), lightweight libraries like Zustand offer a more performant and flexible alternative to React Context, avoiding provider boilerplate and minimizing re-renders.
- The best practice is to use the right tool for the job: leverage the wallet adapter's context for wallet information and adopt a tool like Zustand for other complex, shared client-side state.
Now that you have solid patterns for both fetching on-chain data and managing global client state, you're ready to tackle more complex user flows. In our next lesson, we'll see how a state management tool can help us design and implement a state machine to model the entire lifecycle of a transaction—from composing it to seeing it confirmed on the blockchain.
Can't find a good explanation? Sign up and we'll make it for you
Sign up