DEV Community

metadevdigital
metadevdigital

Posted on

Solana Frontend Development: Building Functional Web3 UIs from Scratch

Solana Frontend Development: Building Functional Web3 UIs from Scratch

I've spent the last year shipping Solana dApps, and I'm gonna be real with you—the frontend side is where most developers struggle. Not because Solana's hard, but because the ecosystem moves fast and most tutorials are either outdated or don't show you how to actually build something people want to use.

This guide covers the practical stuff: setting up a Solana wallet integration, reading on-chain data, and handling transactions on the frontend. No fluff, just what you actually need.

Why Solana Frontend Development is Different

When you're building on Solana, you're not just throwing data on a blockchain and hoping it sticks. You need to:

  • Handle keypair signing locally (not with MetaMask magic)
  • Interact with the Solana JSON RPC directly for most operations
  • Deal with transaction finality that's actually fast enough to matter
  • Build UIs that don't make users wait 30 seconds for confirmation

The good news? Once you get the patterns down, it's cleaner than traditional web3 development.

Getting Your Dev Environment Set Up

First things first. Install the essentials:

npm install @solana/web3.js @solana/wallet-adapter-react @solana/wallet-adapter-wallets @solana/wallet-adapter-base
Enter fullscreen mode Exit fullscreen mode

If you're starting fresh, I recommend using the Solana CLI too:

sh -c "$(curl -sSfL https://release.solana.com/stable/install)"
solana-cli 1.18.0
Enter fullscreen mode Exit fullscreen mode

Verify your setup:

solana --version
Enter fullscreen mode Exit fullscreen mode

Building Your First Wallet Connection

This is the foundation. You need users to connect their wallets before anything meaningful happens.

Here's a working example with React and the Wallet Adapter:

import React, { useMemo } from 'react';
import { ConnectionProvider, WalletProvider } from '@solana/wallet-adapter-react';
import { WalletModalProvider, WalletMultiButton } from '@solana/wallet-adapter-react-ui';
import { PhantomWalletAdapter } from '@solana/wallet-adapter-wallets';
import { clusterApiUrl } from '@solana/web3.js';

function App() {
  const network = 'devnet';
  const endpoint = useMemo(() => clusterApiUrl(network), [network]);
  const wallets = useMemo(() => [new PhantomWalletAdapter()], []);

  return (
    <ConnectionProvider endpoint={endpoint}>
      <WalletProvider wallets={wallets} autoConnect>
        <WalletModalProvider>
          <div style={{ padding: '20px' }}>
            <h1>Solana Frontend Starter</h1>
            <WalletMultiButton />
          </div>
        </WalletModalProvider>
      </WalletProvider>
    </ConnectionProvider>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

This handles the entire wallet connection flow. Users click the button, choose their wallet, and boom—you have access to their public key.

Reading On-Chain Data

Once you've got wallet connection, the next natural step is fetching data from the blockchain.

import { useConnection, useWallet } from '@solana/wallet-adapter-react';
import { useEffect, useState } from 'react';

export function BalanceChecker() {
  const { connection } = useConnection();
  const { publicKey } = useWallet();
  const [balance, setBalance] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    if (!publicKey) {
      setBalance(null);
      return;
    }

    setLoading(true);
    connection.getBalance(publicKey).then((lamports) => {
      setBalance(lamports / 1e9); // Convert to SOL
      setLoading(false);
    });
  }, [publicKey, connection]);

  if (!publicKey) return <p>Connect wallet first</p>;
  if (loading) return <p>Loading...</p>;

  return <p>Balance: {balance} SOL</p>;
}
Enter fullscreen mode Exit fullscreen mode

This is straightforward, but notice the pattern: we depend on publicKey from the wallet context, and we fetch data when it changes. Clean dependency management keeps your data in sync.

Sending Transactions

This is where things get interesting. Here's how to actually send a transaction:

import { useConnection, useWallet } from '@solana/wallet-adapter-react';
import { Transaction, SystemProgram, PublicKey } from '@solana/web3.js';
import { useState } from 'react';

export function TransferSOL() {
  const { connection } = useConnection();
  const { publicKey, signTransaction } = useWallet();
  const [loading, setLoading] = useState(false);

  const sendTransfer = async (recipientAddress, amount) => {
    if (!publicKey || !signTransaction) {
      alert('Wallet not connected');
      return;
    }

    try {
      setLoading(true);

      const transaction = new Transaction().add(
        SystemProgram.transfer({
          fromPubkey: publicKey,
          toPubkey: new PublicKey(recipientAddress),
          lamports: amount * 1e9, // Convert SOL to lamports
        })
      );

      // Get recent blockhash
      const { blockhash } = await connection.getLatestBlockhash();
      transaction.recentBlockhash = blockhash;
      transaction.feePayer = publicKey;

      // Sign and send
      const signedTx = await signTransaction(transaction);
      const signature = await connection.sendRawTransaction(signedTx.serialize());

      // Wait for confirmation
      await connection.confirmTransaction(signature);

      alert(`Transaction sent! Signature: ${signature}`);
    } catch (error) {
      console.error('Transfer failed:', error);
      alert('Transfer failed: ' + error.message);
    } finally {
      setLoading(false);
    }
  };

  return (
    <button 
      onClick={() => sendTransfer('YourRecipientAddressHere', 0.1)}
      disabled={loading}
    >
      {loading ? 'Sending...' : 'Send 0.1 SOL'}
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

Key takeaways here:

  • You always need a recent blockhash
  • Sign with the wallet adapter
  • sendRawTransaction sends the serialized transaction
  • confirmTransaction waits for actual confirmation (not just acceptance)

Handling Errors Like a Professional

Real apps fail gracefully. Here's what you should actually handle:

const handleTransaction = async () => {
  try {
    // Your transaction code
  } catch (error) {
    if (error.message.includes('insufficient funds')) {
      setError('Not enough SOL to cover transaction');
    } else if (error.message.includes('User rejected')) {
      setError('You rejected the transaction');
    } else if (error.code === -32002) {
      setError('Network is busy, try again');
    } else {
      setError(`Transaction failed: ${error.message}`);
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

Different errors need different messaging. Users shouldn't see raw RPC errors.

Common Gotchas I've Hit

1. Blockhash expiration: Don't cache blockhashes. Get a fresh one before every transaction.

2. Wallet not signing: Always check that signTransaction exists before using it. Not all wallets support all operations.

3. Devnet SOL vs mainnet: You're probably testing on devnet. Use the faucet, but know that devnet gets reset.

4. RPC rate limits: If you're doing a lot of requests, consider using Helius or QuickNode instead of the free public RPC.

What's Next?

This covers the basics, but once you've got this down:

  • Learn about SPL tokens (basically ERC20 but better)
  • Explore program interactions (calling smart contract functions)
  • Dive into NFT standards
  • Build swap UIs using Orca or Jupiter

The Solana ecosystem moves fast, so stay in the loop with the official docs and check out repos from projects you use.


Top comments (0)