Inicio / Blockchain / Blockchain: De Cero a Desarrollador / DApps: Arquitectura Frontend

DApps: Arquitectura Frontend

React + Ethers.js, hooks, MetaMask, componentes y redes.

Avanzado
🔒 Solo lectura
📖

Estás en modo lectura

Puedes leer toda la lección, pero para marcar progreso, hacer ejercicios y ganar XP necesitas una cuenta Pro.

Desbloquear por $9/mes

DApps: Arquitectura Frontend

Una DApp (Decentralized Application) es una aplicación web que interactúa con smart contracts en la blockchain. En esta lección construiremos la arquitectura frontend de una DApp moderna usando React, Ethers.js y MetaMask.

Arquitectura de una DApp

┌──────────────────────────────────────────────┐
│              Frontend (React)                │
│  ┌────────────┐  ┌───────────┐  ┌─────────┐│
│  │ Components  │  │   Hooks   │  │  State  ││
│  └─────┬──────┘  └─────┬─────┘  └────┬────┘│
│        └────────────────┼─────────────┘     │
│                         │                    │
│                  ┌──────┴───────┐            │
│                  │  Ethers.js   │            │
│                  └──────┬───────┘            │
└─────────────────────────┼────────────────────┘
                          │
                   ┌──────┴───────┐
                   │   MetaMask   │
                   │  (Signer)    │
                   └──────┬───────┘
                          │
               ┌──────────┴──────────┐
               │    RPC Provider     │
               │ (Alchemy / Infura)  │
               └──────────┬──────────┘
                          │
               ┌──────────┴──────────┐
               │   Ethereum Network  │
               │  (Smart Contracts)  │
               └─────────────────────┘

Setup del Proyecto

# Crear proyecto React con Vite
npm create vite@latest mi-dapp -- --template react
cd mi-dapp

# Instalar dependencias
npm install ethers
npm install react-hot-toast  # Notificaciones

# Estructura
src/
├── components/
│   ├── ConnectWallet.jsx
│   ├── TokenBalance.jsx
│   └── TransferForm.jsx
├── hooks/
│   ├── useWallet.js
│   └── useContract.js
├── contracts/
│   └── MiToken.json      ← ABI del contrato
├── App.jsx
└── main.jsx

Hook: useWallet

// src/hooks/useWallet.js
import { useState, useEffect, useCallback } from "react";
import { ethers } from "ethers";

export function useWallet() {
    const [account, setAccount] = useState(null);
    const [provider, setProvider] = useState(null);
    const [signer, setSigner] = useState(null);
    const [chainId, setChainId] = useState(null);
    const [isConnecting, setIsConnecting] = useState(false);

    const connect = useCallback(async () => {
        if (!window.ethereum) {
            alert("Instala MetaMask");
            return;
        }

        setIsConnecting(true);
        try {
            const browserProvider = new ethers.BrowserProvider(window.ethereum);
            const accounts = await browserProvider.send("eth_requestAccounts", []);
            const network = await browserProvider.getNetwork();
            const signer = await browserProvider.getSigner();

            setProvider(browserProvider);
            setSigner(signer);
            setAccount(accounts[0]);
            setChainId(Number(network.chainId));
        } catch (error) {
            console.error("Error conectando:", error);
        } finally {
            setIsConnecting(false);
        }
    }, []);

    const disconnect = useCallback(() => {
        setAccount(null);
        setProvider(null);
        setSigner(null);
        setChainId(null);
    }, []);

    // Escuchar cambios de cuenta y red
    useEffect(() => {
        if (!window.ethereum) return;

        const handleAccountsChanged = (accounts) => {
            if (accounts.length === 0) {
                disconnect();
            } else {
                setAccount(accounts[0]);
            }
        };

        const handleChainChanged = () => {
            window.location.reload();
        };

        window.ethereum.on("accountsChanged", handleAccountsChanged);
        window.ethereum.on("chainChanged", handleChainChanged);

        return () => {
            window.ethereum.removeListener("accountsChanged", handleAccountsChanged);
            window.ethereum.removeListener("chainChanged", handleChainChanged);
        };
    }, [disconnect]);

    return {
        account,
        provider,
        signer,
        chainId,
        isConnecting,
        connect,
        disconnect,
        isConnected: !!account,
    };
}

Hook: useContract

// src/hooks/useContract.js
import { useState, useEffect, useCallback } from "react";
import { ethers } from "ethers";
import MiTokenABI from "../contracts/MiToken.json";

const TOKEN_ADDRESS = "0x5FbDB2315678afecb367f032d93F642f64180aa3";

export function useContract(signer, provider) {
    const [contract, setContract] = useState(null);
    const [tokenInfo, setTokenInfo] = useState({ name: "", symbol: "", decimals: 18 });

    useEffect(() => {
        if (!provider) return;

        const connection = signer || provider;
        const instance = new ethers.Contract(TOKEN_ADDRESS, MiTokenABI.abi, connection);
        setContract(instance);

        // Cargar info del token
        (async () => {
            const [name, symbol, decimals] = await Promise.all([
                instance.name(),
                instance.symbol(),
                instance.decimals(),
            ]);
            setTokenInfo({ name, symbol, decimals: Number(decimals) });
        })();
    }, [signer, provider]);

    const getBalance = useCallback(async (address) => {
        if (!contract) return "0";
        const balance = await contract.balanceOf(address);
        return ethers.formatUnits(balance, tokenInfo.decimals);
    }, [contract, tokenInfo.decimals]);

    const transfer = useCallback(async (to, amount) => {
        if (!contract || !signer) throw new Error("No conectado");
        const parsedAmount = ethers.parseUnits(amount, tokenInfo.decimals);
        const tx = await contract.transfer(to, parsedAmount);
        return await tx.wait();
    }, [contract, signer, tokenInfo.decimals]);

    return { contract, tokenInfo, getBalance, transfer };
}

Componente: ConnectWallet

// src/components/ConnectWallet.jsx
export function ConnectWallet({ account, isConnecting, onConnect, onDisconnect }) {
    if (account) {
        return (
            <div className="wallet-connected">
                <span className="account">
                    {account.slice(0, 6)}...{account.slice(-4)}
                </span>
                <button onClick={onDisconnect} className="btn-disconnect">
                    Desconectar
                </button>
            </div>
        );
    }

    return (
        <button
            onClick={onConnect}
            disabled={isConnecting}
            className="btn-connect"
        >
            {isConnecting ? "Conectando..." : "Conectar Wallet"}
        </button>
    );
}

Componente: TransferForm

// src/components/TransferForm.jsx
import { useState } from "react";
import toast from "react-hot-toast";

export function TransferForm({ transfer, isConnected }) {
    const [to, setTo] = useState("");
    const [amount, setAmount] = useState("");
    const [loading, setLoading] = useState(false);

    const handleSubmit = async (e) => {
        e.preventDefault();
        if (!to || !amount) return;

        setLoading(true);
        try {
            const receipt = await transfer(to, amount);
            toast.success(`Transferencia exitosa! Block: ${receipt.blockNumber}`);
            setTo("");
            setAmount("");
        } catch (error) {
            toast.error(error.reason || error.message);
        } finally {
            setLoading(false);
        }
    };

    return (
        <form onSubmit={handleSubmit} className="transfer-form">
            <h3>Transferir Tokens</h3>
            <input
                type="text"
                placeholder="Dirección destino (0x...)"
                value={to}
                onChange={(e) => setTo(e.target.value)}
            />
            <input
                type="number"
                step="0.01"
                placeholder="Cantidad"
                value={amount}
                onChange={(e) => setAmount(e.target.value)}
            />
            <button type="submit" disabled={loading || !isConnected}>
                {loading ? "Enviando..." : "Transferir"}
            </button>
        </form>
    );
}

Manejo de Redes

// Solicitar cambio de red a Sepolia
async function switchToSepolia() {
    try {
        await window.ethereum.request({
            method: "wallet_switchEthereumChain",
            params: [{ chainId: "0xaa36a7" }], // 11155111 en hex
        });
    } catch (error) {
        if (error.code === 4902) {
            // La red no existe en MetaMask, agregarla
            await window.ethereum.request({
                method: "wallet_addEthereumChain",
                params: [{
                    chainId: "0xaa36a7",
                    chainName: "Sepolia Testnet",
                    rpcUrls: ["https://rpc.sepolia.org"],
                    nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 },
                    blockExplorerUrls: ["https://sepolia.etherscan.io"],
                }],
            });
        }
    }
}

Mejores Prácticas para DApps

UX/UI:
☐ Mostrar estado de la transacción (pending, confirmed)
☐ Formatear direcciones (0x742d...bD09)
☐ Mostrar enlaces a Etherscan para transacciones
☐ Manejar errores de MetaMask con mensajes claros
☐ Solicitar la red correcta automáticamente

Seguridad:
☐ NUNCA almacenar claves privadas en el frontend
☐ Validar inputs antes de enviar transacciones
☐ Proteger contra front-running cuando sea relevante
☐ Verificar que el contrato está en la red correcta

Performance:
☐ Usar Multicall para batch de llamadas read
☐ Cachear datos que no cambian (name, symbol)
☐ Usar WebSocket provider para eventos en tiempo real
☐ Implementar polling eficiente para balances

Resumen

Una DApp moderna combina React, Ethers.js y MetaMask para crear interfaces que interactúan con smart contracts. Los hooks useWallet y useContract encapsulan la lógica de blockchain, los componentes manejan la UI, y los eventos de MetaMask mantienen sincronizado el estado. La experiencia de usuario debe comunicar claramente estados de transacción, errores y cambios de red.

🔒

Ejercicio práctico disponible

Estado y lógica de DApp

Desbloquear ejercicios
// Estado y lógica de DApp
// Desbloquea Pro para acceder a este ejercicio
// y ganar +50 XP al completarlo

function ejemplo() {
    // Tu código aquí...
}

¿Te gustó esta lección?

Con Pro puedes marcar progreso, hacer ejercicios, tomar quizzes, ganar XP y obtener tu constancia.

Ver planes desde $9/mes