Okay, before you even think about components, snag this: write a tiny copyIdl.js script in your Anchor project root. It grabs that mysolanaapp.json from target/idl/ and dumps it straight into your React app's src/idl.json. Run it after every program build. Why? Manually copying sucks, and you'll forget. Boom, state stays fresh across refreshes. In my experience, this saves like 20 minutes of swearing per dev session.
Look, every Solana React app needs wallet connection. @solana/wallet adapter react and its UI buddies are still king in 2026. Don't overthink it - install the full stack:
@solana/wallet adapter react@solana/wallet adapter react ui@solana/wallet adapter wallets@solana/wallet adapter baseThat's it. Wrap your App like this:
Define wallets with Phantom first - new PhantomWalletAdapter(). Users click WalletMultiButton, boom connected. Fees? Localhost is free, devnet airdrop 2 SOL via solana airdrop 2 YOUR_ADDRESS. Mainnet? ~0.000005 SOL per tx. Sound familiar? Yeah, it's that cheap.
But here's the thing - if wallet's not connected, just show the button centered. No value? Nudge 'em to create an account first. I usually add a state check: if (!wallet.connected) return <WalletMultiButton />;
{ useWallet } from '@solana/wallet adapter react';const wallet = useWallet();wallet.publicKey for signer, wallet.connected for gates.await wallet.signTransaction(tx);Potential issue? Network mismatch. Phantom on localhost? Switch in wallet settings. Forgot? App errors with "invalid network." Fix: hardcode endpoint to match, like devnet "https://api.devnet.solana.com".
So you've got your Rust program with Anchor. Built it? IDL ready? Drop it in src/idl.json. Now build a Program hook component. I call mine SolanaProgram.jsx. Import:
Make a getProvider func:
Then const program = new Program(idl, programID, provider);. programID is new PublicKey(idl.metadata.address). Why does this matter? Anchor auto generates RPC methods like program.rpc.create(). No manual instruction building. Pretty much writes your tx for you.
In my experience, generate a baseAccount Keypair once: const baseAccount = Keypair.generate();. Store its pubkey. Refresh loses it otherwise - users hate re creating counters.
Here's a full Counter.jsx I ripped from a real project. Super short. Works.
jsx import { useState } from 'react'; import { Keypair } from '@solana/web3.js'; const Counter = ({ baseAccountPubkey }) => { const [value, setValue] = useState(null); const createCounter = async () => { const provider = await getProvider(); const program = new Program(idl, programID, provider); const baseAccount = Keypair.generate(); // Or load existing await program.rpc.create({ accounts: { baseAccount: baseAccount.publicKey, user: provider.wallet.publicKey, systemProgram: SystemProgram.programId, }, signers: [baseAccount] }); const account = await program.account.baseAccount.fetch(baseAccount.publicKey); setValue(account.count.toString()); }; const increment = async () => { // Similar, but rpc.increment({ accounts: { baseAccount: baseAccountPubkey } }); // Fetch and setValue }; return (Test it: anchor test first, then npm start. Tx fails? Check logs with solana logs. Common bug: space allocation wrong in Rust #[account(init, payer = user, space = 16 + 16)]. Bump to 8 + 8 + 8 for u64 count.
Now, scale it. This is your base for any stateful dApp. Blog posts? Swap counter for posts array.
Passing program everywhere? Nah. Make a SolanaContext.jsx. Like this:
jsx import React, { createContext, useContext, useEffect, useState } from 'react'; import { Connection, clusterApiUrl } from '@solana/web3.js'; const SolanaContext = createContext(); export const useSolana = () => useContext(SolanaContext); export const SolanaProvider = ({ children }) => { const [connection, setConnection] = useState(null); const [wallet, setWallet] = useState(null); useEffect(() => { const conn = new Connection(clusterApiUrl('devnet')); setConnection(conn); }, []); return (Wrap App.js with it. Now any component: const { connection, wallet } = useSolana();. Clean. No globals. In 2026, everyone's using this for multi wallet support too - add Backpack, Brave.
Issue? Context updates lag. Fix: useEffect on wallet change to refetch program state. I usually debounce fetches at 500ms.
| Component | Use Case | Install Size | Tx Fee Example | Gotchas |
|---|---|---|---|---|
| WalletMultiButton | Connect any wallet | Tiny | 0 SOL (UI only) | Styles must import |
| useSolanaSigner (Account Kit) | Smart wallets | Medium | ~0.000005 SOL | Loading states |
| Program (Anchor) | Smart contract calls | Small | 0.00001 SOL avg | IDL copy script |
| SolanaProvider (Custom) | App wide state | None | N/A | Missing deps crash |
| CounterState | Read PDA state | Tiny | 0 SOL (read) | Fetch errors on invalid |
Why table? Visual. See WalletMultiButton wins for speed. But Anchor Program for real apps. Mix 'em.
Want dynamic? Build a BlogPost.jsx. Needs user init first. Rust side: add initialize_user instruction. Frontend:
program.account.posts.fetchAll() - wait, custom. Map PDAs.program.rpc.createPost({ title: 'Yo', content: 'Solana rocks' })showModal state.Posts array empty? Map makes zero articles. Add one, posts.map(post => <Article={post.id}>{post.title}</Article>). Router it: dashboard for list, /post/:id for single. React Router NPM install, done.
Honestly, avatar uploads? Base64 to instruction data. Fees spike to 0.0001 SOL for big ones. Compress first.
npx create react app solana bloganchor init solana bloganchor test, npm startStuck on "no posts"? Check posts provider has data. Console.log everything.
Break out buttons. IncrementButton.tsx:
tsx import { useConnection, useWallet } from '@solana/wallet adapter react'; import { program } from './anchor/setup'; // Your program export const IncrementButton = ({ accountPubkey }) => { const { connection, publicKey } = useConnection(), wallet = useWallet(); const increment = async () => { await program.methods.increment().accounts({ baseAccount: accountPubkey }).rpc(); }; return ; };TypeScript? Yeah, 2026 standard. Add npm i -D @types/node. Readers like CounterState: useEffect fetch on mount, display {count}.
Pro tip: Toast notifications. NPM react hot toast. toast.success('Counter up!'). UX jumps.
Thinking Android? MobileWalletAdapter from Solana docs. Expo + React Native. Install @solana mobile/mobile wallet adapter protocol web3js. Similar hooks: useSolanaSigner spits address. Send tx same way. Fees identical.
But desktop first. Native apps lag behind web speed.
Tx rejected? 90% payer lacks SOL. Airdrop.
Program not found? Deploy: anchor deploy, update IDL address.
React crash on refresh? Persist baseAccount pubkey in localStorage. useEffect(() => { if (!localStorage.getItem('baseAccount')) generateNew(); }, []);
Devnet slow? QuickNode endpoint, free tier 25 req/s. Swap clusterApiUrl('devnet') for theirs.
Why bother? Solana tx 100x cheaper than ETH. 50k TPS. Your dApp flies.
Scale to voting. Redux for candidates state? Nah, Zustand lighter. Components: CandidateList, VoteButton. Fetch all: loop PDAs or global account. Serialize with Borsh. Vote: check user voted flag, rpc.vote(candidateId).
Deploy devnet: anchor deploy --provider.cluster devnet. 0.1 SOL rent. Register candidates same flow.