Blockchain15 min de lecture

Créer sa Première DApp Web3 avec React et Ethers.js

Par Mathias Pellegrin12 Juillet 2025

Introduction

Le développement Web3 ouvre de nouvelles possibilités pour créer des applications décentralisées (DApps). Ce tutoriel complet vous guide dans la création de votre première DApp avec React et Ethers.js, en couvrant tous les aspects essentiels du développement blockchain. Si vous cherchez une expertise professionnelle, découvrez nos services de développement.

Prérequis

Avant de commencer, assurez-vous d'avoir les connaissances et outils suivants :

Requis pour ce tutoriel :

  • Connaissance de React et JavaScript/TypeScript
  • Compréhension de base de la blockchain Ethereum
  • Node.js et npm installés sur votre machine
  • Extension MetaMask installée dans votre navigateur

Configuration du Projet

Commençons par créer un nouveau projet React et installer toutes les dépendances nécessaires pour le développement Web3.

# Créer un nouveau projet React
npx create-react-app my-dapp
cd my-dapp

# Installer les dépendances Web3
npm install ethers
npm install @metamask/detect-provider

# Installer les dépendances UI (optionnel)
npm install @mui/material @emotion/react @emotion/styled

Connexion à MetaMask

La première étape consiste à établir une connexion avec le wallet MetaMask de l'utilisateur. Créons un hook React personnalisé pour gérer cette interaction.

Fonctionnalités du hook

  • • Connexion automatique au wallet
  • • Gestion des erreurs de connexion
  • • État de connexion persistant
  • • Accès au provider et signer

Avantages d'Ethers.js

  • • API simple et intuitive
  • • Support TypeScript natif
  • • Documentation complète
  • • Performance optimisée
// hooks/useWallet.js
import { useState, useEffect } from 'react';
import { ethers } from 'ethers';

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

  const connectWallet = async () => {
    if (typeof window.ethereum !== 'undefined') {
      try {
        setIsConnecting(true);
        
        // Demander l'accès au wallet
        await window.ethereum.request({ 
          method: 'eth_requestAccounts' 
        });
        
        // Créer le provider et signer
        const provider = new ethers.BrowserProvider(window.ethereum);
        const signer = await provider.getSigner();
        const address = await signer.getAddress();
        
        setProvider(provider);
        setSigner(signer);
        setAccount(address);
      } catch (error) {
        console.error('Erreur de connexion:', error);
        alert('Erreur lors de la connexion au wallet');
      } finally {
        setIsConnecting(false);
      }
    } else {
      alert('MetaMask n\'est pas installé! Veuillez l\'installer pour continuer.');
    }
  };

  // Vérifier si déjà connecté au chargement
  useEffect(() => {
    const checkConnection = async () => {
      if (typeof window.ethereum !== 'undefined') {
        const accounts = await window.ethereum.request({ 
          method: 'eth_accounts' 
        });
        if (accounts.length > 0) {
          connectWallet();
        }
      }
    };
    
    checkConnection();
  }, []);

  return { account, provider, signer, connectWallet, isConnecting };
};

Interaction avec un Smart Contract

Maintenant que nous pouvons nous connecter à MetaMask, créons un composant pour interagir avec un smart contract. Nous allons couvrir les opérations de lecture et d'écriture.

Concepts clés :

ABI (Application Binary Interface)

Interface qui définit comment interagir avec le smart contract.

Adresse du contrat

Identifiant unique du smart contract sur la blockchain.

Transactions vs Appels

Les lectures sont gratuites, les écritures coûtent du gaz.

// components/ContractInteraction.js
import React, { useState, useEffect } from 'react';
import { ethers } from 'ethers';

const CONTRACT_ADDRESS = '0x...'; // Remplacez par l'adresse de votre contrat
const CONTRACT_ABI = [
  // Définition de l'ABI de votre contrat
  "function getValue() view returns (uint256)",
  "function setValue(uint256 _value) external",
  "function owner() view returns (address)",
  "event ValueChanged(uint256 indexed newValue, address indexed changedBy)"
];

export const ContractInteraction = ({ signer, account }) => {
  const [value, setValue] = useState('');
  const [contractValue, setContractValue] = useState('');
  const [owner, setOwner] = useState('');
  const [loading, setLoading] = useState(false);
  const [txHash, setTxHash] = useState('');

  // Créer l'instance du contrat
  const contract = new ethers.Contract(
    CONTRACT_ADDRESS, 
    CONTRACT_ABI, 
    signer
  );

  // Lire la valeur actuelle du contrat
  const readValue = async () => {
    try {
      setLoading(true);
      const result = await contract.getValue();
      setContractValue(result.toString());
      
      const contractOwner = await contract.owner();
      setOwner(contractOwner);
    } catch (error) {
      console.error('Erreur lors de la lecture:', error);
      alert('Erreur lors de la lecture du contrat');
    } finally {
      setLoading(false);
    }
  };

  // Écrire une nouvelle valeur dans le contrat
  const writeValue = async () => {
    if (!value || isNaN(value)) {
      alert('Veuillez entrer une valeur numérique valide');
      return;
    }

    try {
      setLoading(true);
      setTxHash('');
      
      // Envoyer la transaction
      const tx = await contract.setValue(value);
      setTxHash(tx.hash);
      
      // Attendre la confirmation
      const receipt = await tx.wait();
      
      if (receipt.status === 1) {
        alert('Transaction confirmée !');
        readValue(); // Relire la valeur mise à jour
        setValue(''); // Vider le champ
      } else {
        alert('Transaction échouée');
      }
    } catch (error) {
      console.error('Erreur lors de l\'écriture:', error);
      if (error.code === 'ACTION_REJECTED') {
        alert('Transaction annulée par l\'utilisateur');
      } else {
        alert('Erreur lors de l\'exécution de la transaction');
      }
    } finally {
      setLoading(false);
    }
  };

  // Effet pour charger les données initiales
  useEffect(() => {
    if (contract && signer) {
      readValue();
    }
  }, [contract, signer]);

  return (
    <div className="space-y-6">
      <h3 className="text-2xl font-bold text-white">Interaction avec le Smart Contract</h3>
      
      {/* Informations du contrat */}
      <div className="bg-gray-800/50 rounded-lg p-4">
        <h4 className="font-semibold text-white mb-2">Informations du contrat</h4>
        <p className="text-sm text-gray-300">Adresse: {CONTRACT_ADDRESS}</p>
        <p className="text-sm text-gray-300">Propriétaire: {owner}</p>
        <p className="text-sm text-gray-300">Valeur actuelle: {contractValue || 'Non chargée'}</p>
      </div>
      
      {/* Lecture de données */}
      <div className="bg-blue-900/20 rounded-lg p-4 border border-blue-500/30">
        <h4 className="font-semibold text-white mb-3">Lire les données</h4>
        <button 
          onClick={readValue}
          disabled={loading}
          className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg disabled:opacity-50"
        >
          {loading ? 'Chargement...' : 'Actualiser la valeur'}
        </button>
      </div>
      
      {/* Écriture de données */}
      <div className="bg-green-900/20 rounded-lg p-4 border border-green-500/30">
        <h4 className="font-semibold text-white mb-3">Modifier les données</h4>
        <div className="flex gap-3">
          <input
            type="number"
            value={value}
            onChange={(e) => setValue(e.target.value)}
            placeholder="Nouvelle valeur"
            className="flex-1 bg-gray-800 text-white px-3 py-2 rounded-lg border border-gray-600"
          />
          <button 
            onClick={writeValue} 
            disabled={loading || !value}
            className="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg disabled:opacity-50"
          >
            {loading ? 'En cours...' : 'Écrire la valeur'}
          </button>
        </div>
        
        {txHash && (
          <p className="text-sm text-green-300 mt-2">
            Transaction: {txHash}
          </p>
        )}
      </div>
    </div>
  );
};

Écoute des Événements

Les événements permettent à votre DApp de réagir en temps réel aux changements sur la blockchain. Implémentons l'écoute d'événements pour notre smart contract.

// Ajouter dans ContractInteraction.js
useEffect(() => {
  if (contract && signer) {
    // Fonction pour gérer les événements ValueChanged
    const handleValueChanged = (newValue, changedBy, event) => {
      console.log('Événement ValueChanged détecté:', {
        newValue: newValue.toString(),
        changedBy,
        blockNumber: event.blockNumber,
        transactionHash: event.transactionHash
      });
      
      // Mettre à jour l'état local
      setContractValue(newValue.toString());
      
      // Afficher une notification
      if (changedBy.toLowerCase() !== account.toLowerCase()) {
        alert(`Valeur mise à jour par un autre utilisateur: ${newValue.toString()}`);
      }
    };

    // Commencer à écouter l'événement
    contract.on('ValueChanged', handleValueChanged);

    // Fonction de nettoyage
    return () => {
      contract.off('ValueChanged', handleValueChanged);
    };
  }
}, [contract, signer, account]);

// Également, vous pouvez récupérer les événements passés
const getHistoricalEvents = async () => {
  try {
    const currentBlock = await provider.getBlockNumber();
    const fromBlock = currentBlock - 1000; // 1000 blocs en arrière
    
    const events = await contract.queryFilter(
      'ValueChanged', 
      fromBlock, 
      currentBlock
    );
    
    console.log('Événements historiques:', events);
  } catch (error) {
    console.error('Erreur lors de la récupération des événements:', error);
  }
};

Gestion des Réseaux

Une DApp robuste doit gérer différents réseaux Ethereum (mainnet, testnets, Layer 2). Implémentons un système de gestion des réseaux.

Réseaux supportés :

Production

  • • Ethereum Mainnet
  • • Polygon
  • • Arbitrum
  • • Optimism

Test

  • • Goerli Testnet
  • • Sepolia Testnet
  • • Mumbai (Polygon)
  • • Localhost
// utils/networks.js
export const NETWORKS = {
  1: {
    name: 'Ethereum Mainnet',
    currency: 'ETH',
    rpcUrl: 'https://mainnet.infura.io/v3/YOUR_PROJECT_ID',
    blockExplorer: 'https://etherscan.io'
  },
  5: {
    name: 'Goerli Testnet',
    currency: 'ETH',
    rpcUrl: 'https://goerli.infura.io/v3/YOUR_PROJECT_ID',
    blockExplorer: 'https://goerli.etherscan.io'
  },
  137: {
    name: 'Polygon Mainnet',
    currency: 'MATIC',
    rpcUrl: 'https://polygon-rpc.com',
    blockExplorer: 'https://polygonscan.com'
  },
  80001: {
    name: 'Polygon Mumbai',
    currency: 'MATIC',
    rpcUrl: 'https://rpc-mumbai.maticvigil.com',
    blockExplorer: 'https://mumbai.polygonscan.com'
  }
};

export const switchNetwork = async (chainId) => {
  try {
    await window.ethereum.request({
      method: 'wallet_switchEthereumChain',
      params: [{ chainId: `0x${chainId.toString(16)}` }],
    });
    return true;
  } catch (error) {
    if (error.code === 4902) {
      // Réseau non ajouté, proposer de l'ajouter
      return await addNetwork(chainId);
    }
    console.error('Erreur changement réseau:', error);
    return false;
  }
};

const addNetwork = async (chainId) => {
  const network = NETWORKS[chainId];
  if (!network) return false;

  try {
    await window.ethereum.request({
      method: 'wallet_addEthereumChain',
      params: [{
        chainId: `0x${chainId.toString(16)}`,
        chainName: network.name,
        nativeCurrency: {
          name: network.currency,
          symbol: network.currency,
          decimals: 18
        },
        rpcUrls: [network.rpcUrl],
        blockExplorerUrls: [network.blockExplorer]
      }]
    });
    return true;
  } catch (error) {
    console.error('Erreur ajout réseau:', error);
    return false;
  }
};

Application Complète

Assemblons tous les composants dans notre application principale pour créer une DApp fonctionnelle.

// App.js
import React, { useState, useEffect } from 'react';
import { useWallet } from './hooks/useWallet';
import { ContractInteraction } from './components/ContractInteraction';
import { NETWORKS, switchNetwork } from './utils/networks';

function App() {
  const { account, provider, signer, connectWallet, isConnecting } = useWallet();
  const [currentNetwork, setCurrentNetwork] = useState(null);
  const [balance, setBalance] = useState('0');

  // Obtenir les informations du réseau actuel
  useEffect(() => {
    const getNetworkInfo = async () => {
      if (provider) {
        const network = await provider.getNetwork();
        setCurrentNetwork(NETWORKS[Number(network.chainId)] || {
          name: 'Réseau inconnu',
          currency: 'ETH'
        });
        
        // Obtenir le solde
        if (account) {
          const balance = await provider.getBalance(account);
          setBalance(ethers.formatEther(balance));
        }
      }
    };
    
    getNetworkInfo();
  }, [provider, account]);

  // Gérer les changements de compte/réseau
  useEffect(() => {
    if (typeof window.ethereum !== 'undefined') {
      const handleAccountsChanged = (accounts) => {
        if (accounts.length === 0) {
          // Utilisateur déconnecté
          window.location.reload();
        } else {
          // Compte changé
          window.location.reload();
        }
      };

      const handleChainChanged = (chainId) => {
        // Réseau changé
        window.location.reload();
      };

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

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

  return (
    <div className="min-h-screen bg-gray-900 text-white p-8">
      <div className="max-w-4xl mx-auto">
        <header className="mb-12 text-center">
          <h2 className="text-4xl font-bold mb-4 bg-gradient-to-r from-cyan-400 to-purple-400 bg-clip-text text-transparent">
            Ma Première DApp Web3
          </h2>
          <p className="text-gray-400">
            Application décentralisée construite avec React et Ethers.js
          </p>
        </header>

        {account ? (
          <div className="space-y-8">
            {/* Informations du wallet */}
            <div className="bg-gray-800 rounded-lg p-6">
              <h2 className="text-2xl font-bold mb-4">Wallet connecté</h2>
              <div className="grid md:grid-cols-2 gap-4 text-sm">
                <div>
                  <p className="text-gray-400">Adresse</p>
                  <p className="font-mono break-all">{account}</p>
                </div>
                <div>
                  <p className="text-gray-400">Solde</p>
                  <p>{parseFloat(balance).toFixed(4)} {currentNetwork?.currency}</p>
                </div>
                <div>
                  <p className="text-gray-400">Réseau</p>
                  <p>{currentNetwork?.name}</p>
                </div>
                <div>
                  <p className="text-gray-400">Status</p>
                  <p className="text-green-400">✓ Connecté</p>
                </div>
              </div>
            </div>

            {/* Interface d'interaction avec le contrat */}
            <ContractInteraction 
              signer={signer} 
              account={account}
              provider={provider}
            />

            {/* Boutons de changement de réseau */}
            <div className="bg-gray-800 rounded-lg p-6">
              <h3 className="text-xl font-bold mb-4">Changer de réseau</h3>
              <div className="grid grid-cols-2 md:grid-cols-4 gap-3">
                {Object.entries(NETWORKS).map(([chainId, network]) => (
                  <button
                    key={chainId}
                    onClick={() => switchNetwork(Number(chainId))}
                    className="bg-blue-600 hover:bg-blue-700 text-white px-3 py-2 rounded-lg text-sm transition-colors"
                  >
                    {network.name}
                  </button>
                ))}
              </div>
            </div>
          </div>
        ) : (
          <div className="text-center py-16">
            <div className="bg-gray-800 rounded-lg p-8 max-w-md mx-auto">
              <h2 className="text-2xl font-bold mb-4">Connexion requise</h2>
              <p className="text-gray-400 mb-6">
                Connectez votre wallet MetaMask pour commencer à utiliser la DApp
              </p>
              <button 
                onClick={connectWallet}
                disabled={isConnecting}
                className="bg-gradient-to-r from-cyan-500 to-purple-500 hover:from-cyan-600 hover:to-purple-600 text-white font-bold py-3 px-6 rounded-lg transition-all duration-300 disabled:opacity-50"
              >
                {isConnecting ? 'Connexion...' : 'Connecter MetaMask'}
              </button>
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

export default App;

Déploiement

Une fois votre DApp développée et testée, vous pouvez la déployer sur différentes plateformes pour la rendre accessible au public.

Déploiement centralisé

  • • Vercel (recommandé)
  • • Netlify
  • • GitHub Pages
  • • AWS S3 + CloudFront

Déploiement décentralisé

  • • IPFS + Pinata
  • • Arweave
  • • Fleek
  • • ENS + IPFS
# Build de production
npm run build

# Déploiement sur Vercel
npm install -g vercel
vercel --prod

# Déploiement sur IPFS avec Fleek
npm install -g @fleek-platform/cli
fleek site deploy

# Déploiement manuel sur IPFS
npm install -g ipfs-deploy
ipd build/

Bonnes Pratiques de Sécurité

La sécurité est cruciale en Web3. Voici les meilleures pratiques à suivre pour protéger vos utilisateurs et votre application.

Points de sécurité essentiels :

  • 🔒Toujours vérifier la présence et l'état de MetaMask
  • 🔒Gérer les erreurs de transaction de façon appropriée
  • 🔒Afficher les états de chargement pendant les transactions
  • 🔒Valider rigoureusement toutes les entrées utilisateur
  • 🔒Utiliser exclusivement des testnets pour le développement
  • 🔒Implémenter une gestion robuste des changements de réseau

Conclusion

Félicitations ! Vous avez maintenant créé votre première DApp Web3 fonctionnelle. Cette base solide vous permettra d'explorer des fonctionnalités plus avancées comme les NFTs, les tokens ERC-20, les protocoles DeFi, ou même des interactions cross-chain.

Le développement Web3 évolue rapidement. Continuez à vous tenir informé des dernières technologies et bonnes pratiques pour créer des applications décentralisées toujours plus performantes et sécurisées.

Besoin d'aide pour votre projet Web3 ?

Notre équipe peut vous accompagner dans le développement de votre DApp et l'intégration blockchain. Découvrez également nos autres tutoriels : Smart Contracts Solidityet Performance React Native.

Contactez-nous