Intégration de l'Interface Utilisateur React avec les Smart Contracts
Bienvenue dans cette leçon du cours "Maîtrisez le Développement Web3 : Créer des DApps avec Solidity et React". Aujourd'hui, nous allons aborder un aspect crucial du développement de DApps : l'intégration harmonieuse entre votre interface utilisateur (UI) développée avec React et les smart contracts déployés sur la blockchain. C'est ici que la magie opère, transformant un simple contrat intelligent en une application décentralisée interactive et utilisable.
Introduction : Le Pont entre le Frontend et la Blockchain
Une DApp (Application Décentralisée) est bien plus qu'un simple smart contract. C'est une application complète qui offre une expérience utilisateur riche et intuitive, tout en tirant parti de la sécurité, de la transparence et de l'immuabilité de la blockchain. Pour que cette vision devienne réalité, votre interface utilisateur React doit être capable de "parler" à votre smart contract.
Dans cette leçon, nous allons explorer les mécanismes et les outils qui permettent à votre application React de :
- Se connecter à un fournisseur de blockchain (comme Metamask).
- Lire des données depuis un smart contract.
- Envoyer des transactions pour modifier l'état du smart contract.
- Réagir aux événements émis par le smart contract pour des mises à jour en temps réel.
Préparez-vous à transformer vos smart contracts dormants en applications dynamiques !
Prérequis
Pour tirer le meilleur parti de cette leçon, il est recommandé d'avoir :
- Une connaissance de base de React (composants, hooks, gestion d'état).
- Une compréhension fondamentale de ce qu'est un smart contract et de son déploiement.
- Node.js et npm/yarn installés sur votre machine.
- L'extension de navigateur Metamask installée et configurée avec un réseau de test (ex: Sepolia).
Fondations de l'Intégration : Les Outils Essentiels
L'interaction entre votre frontend React et la blockchain ne se fait pas directement. Nous avons besoin d'intermédiaires, de bibliothèques qui facilitent cette communication.
Le Rôle des Bibliothèques Web3 : ethers.js et web3.js
Ces bibliothèques sont les traducteurs universels entre votre code JavaScript et le protocole Ethereum. Elles encapsulent les appels RPC (Remote Procedure Call) nécessaires pour interagir avec un nœud Ethereum.
web3.js: Historiquement la première bibliothèque majeure, elle offre une suite complète d'outils pour interagir avec Ethereum.ethers.js: Plus moderne, plus légère, souvent préférée aujourd'hui pour les projets React en raison de son approche plus idiomatique en JavaScript moderne, de sa meilleure gestion des erreurs et de sa prise en charge native de TypeScript. Nous nous concentrerons surethers.jsdans cette leçon.
Ces bibliothèques permettent de :
- Se connecter à un fournisseur de blockchain (ex: Metamask).
- Accéder aux comptes des utilisateurs.
- Interagir avec les smart contracts (lire des données, envoyer des transactions).
- Écouter les événements de la blockchain.
Fournisseurs (Providers) et Signataires (Signers)
Comprendre ces deux concepts est fondamental pour interagir avec la blockchain.
- Fournisseur (Provider) : C'est une connexion en lecture seule à un nœud Ethereum. Il permet de lire des informations de la blockchain (solde d'un compte, état d'un smart contract, blocs récents, etc.).
- Exemples :
JsonRpcProvider(pour se connecter à un nœud RPC public ou local comme Hardhat/Ganache),Web3Provider(pour se connecter à un injecteur de portefeuille comme Metamask).
- Exemples :
- Signataire (Signer) : C'est une entité qui peut signer des transactions et donc modifier l'état de la blockchain. Un
Signerest toujours associé à un compte (une adresse avec une clé privée) et nécessite l'approbation de l'utilisateur pour envoyer une transaction.- Exemple : Le compte actuellement sélectionné dans Metamask est un
Signerqueethers.jspeut utiliser via unWeb3Provider.
- Exemple : Le compte actuellement sélectionné dans Metamask est un
Pour lire des données (méthodes view ou pure de Solidity), vous n'avez besoin que d'un Provider. Pour envoyer des transactions (méthodes payable ou qui modifient l'état), vous avez besoin d'un Signer.
Adresses de Contrat et ABI
Pour qu'un Provider ou Signer puisse interagir avec un smart contract spécifique, il a besoin de deux informations cruciales :
- Adresse du Contrat : L'adresse unique à laquelle votre smart contract a été déployé sur la blockchain. C'est son identifiant numérique.
- ABI (Application Binary Interface) : C'est une description JSON de l'interface de votre smart contract. Elle indique à
ethers.js(ouweb3.js) quelles fonctions le contrat expose, quels arguments elles attendent, et quels types elles retournent. Sans l'ABI,ethers.jsne saurait pas comment formater les appels pour interagir avec les fonctions de votre contrat. L'ABI est générée automatiquement lors de la compilation de votre smart contract (par Hardhat, Truffle, Foundry, etc.).
Mise en Place de l'Environnement React
Commençons par créer un projet React et installer les dépendances nécessaires.
-
Créer un projet React (si vous n'en avez pas déjà un) :
npx create-react-app my-dapp cd my-dappOu avec Vite, qui est plus rapide :
npm create vite@latest my-dapp -- --template react cd my-dapp npm install -
Installer
ethers.js:npm install ethers # ou yarn add ethers
Maintenant que notre environnement est prêt, nous pouvons commencer l'intégration.
Connexion au Smart Contract
L'objectif est de permettre à l'utilisateur de connecter son portefeuille (ex: Metamask) et d'instancier notre smart contract dans notre application React.
Structure du Contrat Exemple (Solidity)
Imaginons un smart contract Solidity simple que nous voulons interagir avec. Ce contrat stocke un message et permet de le lire et de le modifier.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SimpleStorage {
string public message;
event MessageChanged(string oldMessage, string newMessage, address indexed caller);
constructor(string memory _initialMessage) {
message = _initialMessage;
}
function getMessage() public view returns (string memory) {
return message;
}
function setMessage(string memory _newMessage) public {
string memory oldMsg = message;
message = _newMessage;
emit MessageChanged(oldMsg, _newMessage, msg.sender);
}
}
Note : Après la compilation et le déploiement de ce contrat, vous obtiendrez son ABI (dans un fichier .json, souvent sous artifacts/contracts/SimpleStorage.json) et son adresse de déploiement.
Code React : Connexion et Instanciation
Nous allons modifier src/App.js pour gérer la connexion Metamask et l'instanciation du contrat.
import React, { useState, useEffect } from 'react';
import { ethers } from 'ethers';
// Importez l'ABI de votre contrat. L'ABI est un fichier JSON.
// Assurez-vous que ce chemin est correct pour votre projet (ex: depuis les artifacts de Hardhat)
import SimpleStorageABI from './contracts/SimpleStorage.json'; // Adaptez le chemin
function App() {
const [provider, setProvider] = useState(null);
const [signer, setSigner] = useState(null);
const [contract, setContract] = useState(null);
const [account, setAccount] = useState(null);
const [contractMessage, setContractMessage] = useState('');
const [newMessage, setNewMessage] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
// Remplacez par l'adresse de votre contrat déployé
const contractAddress = "0x5FbDB2315678afecb367f032d93F642f64180aa3"; // Exemple, CHANGEZ CETTE ADRESSE !
useEffect(() => {
// Initialiser la connexion Metamask au chargement de la page
const initConnection = async () => {
if (window.ethereum) {
try {
// Demande de connexion aux comptes
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
setAccount(accounts[0]);
// Création du Provider (lecture seule)
const newProvider = new ethers.BrowserProvider(window.ethereum); // Utilisez BrowserProvider pour Metamask
setProvider(newProvider);
// Création du Signer (écriture)
const newSigner = await newProvider.getSigner();
setSigner(newSigner);
// Instanciation du contrat
const newContract = new ethers.Contract(contractAddress, SimpleStorageABI.abi, newSigner);
setContract(newContract);
console.log('Connecté à Metamask et contrat initialisé.');
} catch (err) {
console.error("Erreur de connexion à Metamask :", err);
setError("Veuillez connecter votre portefeuille Metamask.");
}
} else {
setError("Metamask ou un portefeuille compatible Ethereum n'est pas détecté.");
}
};
initConnection();
// Écouter les changements de compte ou de réseau dans Metamask
if (window.ethereum) {
window.ethereum.on('accountsChanged', (accounts) => {
setAccount(accounts[0]);
// Recharger le provider et le signer pour s'assurer qu'ils sont à jour
window.location.reload();
});
window.ethereum.on('chainChanged', (chainId) => {
// Recharger la page si le réseau change pour s'assurer de la bonne configuration
window.location.reload();
});
}
// Nettoyage des listeners lors du démontage du composant
return () => {
if (window.ethereum) {
window.ethereum.removeListener('accountsChanged', () => {});
window.ethereum.removeListener('chainChanged', () => {});
}
};
}, []); // Le tableau vide signifie que cet effet s'exécute une seule fois au montage
// Fonction pour lire le message du contrat
const fetchMessage = async () => {
if (contract) {
setLoading(true);
setError(null);
try {
const msg = await contract.getMessage();
setContractMessage(msg);
} catch (err) {
console.error("Erreur lors de la lecture du message :", err);
setError("Impossible de lire le message du contrat.");
} finally {
setLoading(false);
}
}
};
// Fonction pour modifier le message du contrat
const updateMessage = async () => {
if (contract && newMessage) {
setLoading(true);
setError(null);
try {
// Envoi de la transaction
const tx = await contract.setMessage(newMessage);
console.log("Transaction envoyée :", tx.hash);
alert(`Transaction envoyée ! Hash: ${tx.hash}`);
// Attendre la confirmation de la transaction
await tx.wait();
console.log("Transaction confirmée !");
alert("Transaction confirmée ! Le message a été mis à jour.");
setNewMessage(''); // Vider le champ d'entrée
fetchMessage(); // Recharger le message après la mise à jour
} catch (err) {
console.error("Erreur lors de la mise à jour du message :", err);
setError("Erreur lors de l'envoi de la transaction. Vérifiez votre portefeuille.");
} finally {
setLoading(false);
}
}
};
return (
<div style={{ padding: '20px', fontFamily: 'Arial, sans-serif' }}>
<h1>Intégration React & Smart Contract</h1>
{error && <p style={{ color: 'red' }}>{error}</p>}
{account ? (
<>
<p>Compte connecté : <strong>{account}</strong></p>
{contract ? (
<div>
<h2>Interagir avec SimpleStorage</h2>
<p>Message actuel du contrat : {loading ? 'Chargement...' : contractMessage}</p>
<button onClick={fetchMessage} disabled={loading}>
{loading ? 'Chargement...' : 'Rafraîchir Message'}
</button>
<div style={{ marginTop: '20px' }}>
<h3>Modifier le message</h3>
<input
type="text"
value={newMessage}
onChange={(e) => setNewMessage(e.target.value)}
placeholder="Nouveau message"
style={{ marginRight: '10px', padding: '8px' }}
disabled={loading}
/>
<button onClick={updateMessage} disabled={loading || !newMessage}>
{loading ? 'Envoi...' : 'Envoyer Nouveau Message'}
</button>
</div>
</div>
) : (
<p>Initialisation du contrat en cours ou échec...</p>
)}
</>
) : (
<p>Veuillez connecter votre portefeuille Metamask pour commencer.</p>
)}
</div>
);
}
export default App;
Explications du Code :
import { ethers } from 'ethers';: Importe la bibliothèqueethers.js.import SimpleStorageABI from './contracts/SimpleStorage.json';: Importe l'ABI de votre smart contract. L'ABI est essentielle pour qu'ethers.jssache comment interagir avec les fonctions de votre contrat. Assurez-vous que le chemin est correct.useStateHooks: Utilisés pour gérer l'état de l'application :provider: L'instance du fournisseurethers.signer: L'instance du signataireethers(pour les transactions).contract: L'instance du smart contractethers.account: L'adresse du compte Metamask connecté.contractMessage: Le message actuellement lu depuis le contrat.newMessage: Le message que l'utilisateur veut écrire.loading: Indique si une opération est en cours (pour désactiver les boutons).error: Pour afficher les messages d'erreur à l'utilisateur.
contractAddress: C'est l'adresse à laquelle votre contratSimpleStorageest déployé. C'est une valeur à changer après votre déploiement sur un réseau de test.useEffectpour l'Initialisation:- Ce hook s'exécute une fois au montage du composant (
[]en deuxième argument). - Il vérifie
window.ethereum, qui est injecté par Metamask. await window.ethereum.request({ method: 'eth_requestAccounts' });: Demande à Metamask de connecter les comptes de l'utilisateur.new ethers.BrowserProvider(window.ethereum): Crée unProviderpour interagir avec Metamask.await newProvider.getSigner(): Obtient leSigner(le compte Metamask actuellement sélectionné) à partir duProvider. C'est leSignerqui pourra autoriser les transactions.new ethers.Contract(contractAddress, SimpleStorageABI.abi, newSigner): C'est l'étape clé ! Elle crée une instance du contratethers.- Le premier argument est l'adresse du contrat.
- Le deuxième est l'ABI du contrat.
- Le troisième est le
signer(ouprovidersi vous ne faites que des lectures). Pour notre application, nous passons lesignerafin de pouvoir à la fois lire et écrire.
- Gestion des événements Metamask (
accountsChanged,chainChanged): Il est crucial d'écouter ces événements pour réagir aux changements de compte ou de réseau de l'utilisateur dans Metamask, souvent en rechargeant la page pour s'assurer que l'état de l'application React est synchronisé avec Metamask.
- Ce hook s'exécute une fois au montage du composant (
fetchMessage():- Appelle
contract.getMessage()pour lire l'état actuel du message. Comme c'est une fonctionviewoupureen Solidity, elle n'entraîne pas de coût en gaz et ne nécessite pas de transaction. L'appel se fait directement via leprovider(ou lesignerqui contient unprovider).
- Appelle
updateMessage():- Appelle
contract.setMessage(newMessage)pour envoyer une transaction qui modifiera l'état du contrat. await tx.wait(): Très important ! Après l'envoi d'une transaction, il faut attendre sa confirmation sur la blockchain.tx.wait()renvoie une promesse qui se résout une fois que la transaction est incluse dans un bloc et a un certain nombre de confirmations. C'est seulement après cette attente que vous pouvez être sûr que la modification a eu lieu.
- Appelle
Interagir avec le Smart Contract : Lecture et Écriture
Maintenant que notre application est connectée au smart contract, explorons plus en détail les interactions de lecture et d'écriture.
Appels en Lecture (Read-only calls)
Les fonctions de votre smart contract marquées view ou pure en Solidity sont des appels de lecture. Elles ne modifient pas l'état de la blockchain et ne coûtent donc pas de gaz. Elles sont exécutées localement par le nœud auquel votre Provider est connecté.
- Syntaxe avec
ethers.js:contract.yourViewFunction() - Exemple :
contract.getMessage()dans notrefetchMessagefonction.
Transactions (Write calls)
Les fonctions qui modifient l'état du smart contract (celles sans view ou pure) nécessitent l'envoi d'une transaction. Cela implique un coût en gaz et doit être signé par l'utilisateur via son portefeuille (Metamask).
- Syntaxe avec
ethers.js:contract.yourWriteFunction(arg1, arg2, ...) - Processus:
- Votre application React appelle la fonction du contrat via l'instance
contract(qui est liée auSigner). - Metamask intercepte l'appel et affiche une fenêtre de confirmation à l'utilisateur.
- Si l'utilisateur confirme, Metamask signe la transaction et l'envoie au réseau.
- Votre code React reçoit un objet
TransactionResponsequi contient lehashde la transaction. - Vous pouvez ensuite utiliser
tx.wait()pour attendre que la transaction soit minée et confirmée sur la blockchain.
- Votre application React appelle la fonction du contrat via l'instance
- Exemple :
contract.setMessage(newMessage)dans notreupdateMessagefonction.
Gestion des Événements (Events)
Les événements sont un mécanisme puissant dans Solidity pour "logguer" des informations sur la blockchain. Ces logs sont immuables et peuvent être écoutés par votre application React pour des mises à jour en temps réel sans avoir à sonder constamment le smart contract.
Pourquoi utiliser les événements ?
- Mises à jour en temps réel : Votre UI peut réagir immédiatement aux changements sur la blockchain.
- Preuve auditable : Les événements sont stockés sur la blockchain, fournissant un historique des actions.
- Efficacité : Plus performant que de sonder constamment l'état du contrat.
Écouter les Événements avec ethers.js
// ... dans le useEffect d'App.js, après l'instanciation du contrat
useEffect(() => {
// ... code de connexion existant
const initConnection = async () => {
// ... code de connexion existant pour provider, signer, contract
if (newContract) {
// Écouter l'événement MessageChanged
newContract.on("MessageChanged", (oldMessage, newMessage, caller) => {
console.log(`Événement 'MessageChanged' reçu :`);
console.log(` Ancien message : ${oldMessage}`);
console.log(` Nouveau message : ${newMessage}`);
console.log(` Appelé par : ${caller}`);
setContractMessage(newMessage); // Mettre à jour l'UI avec le nouveau message
alert(`Le message du contrat a été mis à jour par ${caller} : "${newMessage}"`);
});
console.log('Écoute des événements MessageChanged activée.');
}
};
initConnection();
// Fonction de nettoyage des listeners pour éviter les fuites de mémoire
return () => {
if (contract) {
contract.off("MessageChanged"); // Désactiver le listener lors du démontage
}
// ... nettoyage des listeners Metamask existants
};
}, [contract]); // Dépend du contrat pour s'assurer qu'il est instancié
Explications :
contract.on("MessageChanged", (oldMessage, newMessage, caller) => { ... });:contract.on()est la méthode d'ethers.jspour écouter les événements.- Le premier argument est le nom de l'événement tel que défini dans Solidity (
MessageChanged). - Le deuxième argument est une fonction de rappel qui sera exécutée chaque fois que l'événement est émis. Les arguments de cette fonction correspondent aux arguments de l'événement Solidity, dans le même ordre.
- Dans notre exemple, lorsque
MessageChangedest émis, nous mettons à jour l'étatcontractMessagepour refléter le nouveau message et affichons une alerte à l'utilisateur.
- Nettoyage du Listener (
contract.off("MessageChanged")): Il est crucial de désactiver les listeners d'événements lorsque le composant est démonté pour éviter les fuites de mémoire. Cela se fait dans la fonction de retour duuseEffect.
Considérations Avancées
- Gestion des États de Chargement et des Erreurs : Comme montré dans l'exemple, l'utilisation de
useStatepourloadingeterrorest fondamentale pour une expérience utilisateur robuste. Informez l'utilisateur de l'état des opérations et des problèmes rencontrés. - Pattern
try-catch-finally: Entourez toujours vos appels blockchain asynchrones (await) avec des blocstry-catchpour gérer gracieusement les erreurs (par exemple, l'utilisateur annule la transaction, ou la transaction échoue). - Utilisation de Hooks Personnalisés : Pour des DApps plus complexes, vous pouvez abstraire la logique d'interaction avec les smart contracts dans des hooks React personnalisés (ex:
useContract,useContractRead,useContractWrite). Cela améliore la réutilisabilité et la lisibilité de votre code. Des bibliothèques commewagmiouthirdwebfournissent des hooks très puissants pour cela. - Context API ou Redux : Pour les DApps de grande envergure, la gestion de l'état global du Web3 (provider, signer, contract) peut bénéficier d'une solution de gestion d'état comme le Context API de React ou Redux, évitant le prop-drilling.
Conclusion
L'intégration de votre interface utilisateur React avec les smart contracts est le cœur même du développement de DApps. En maîtrisant l'utilisation de bibliothèques comme ethers.js, en comprenant le rôle des Providers et Signers, et en exploitant l'ABI de vos contrats, vous pouvez construire des applications décentralisées complètes et interactives.
Nous avons couvert les étapes essentielles :
- La configuration de l'environnement React.
- La connexion à un portefeuille comme Metamask.
- L'instanciation de votre smart contract en JavaScript.
- L'exécution d'appels en lecture (pas de gaz).
- L'envoi de transactions en écriture (nécessite du gaz et confirmation utilisateur).
- L'écoute des événements pour des mises à jour en temps réel.
Le développement Web3 est un domaine en constante évolution. N'hésitez pas à explorer davantage les bibliothèques et frameworks dédiés comme wagmi, thirdweb ou web3modal qui simplifient encore plus l'intégration et offrent des fonctionnalités avancées (gestion de plusieurs chaînes, connexion simplifiée de plusieurs portefeuilles, etc.).
Continuez à expérimenter, à déployer et à construire ! La prochaine étape logique est d'approfondir la gestion des erreurs, l'optimisation des performances et la sécurité de vos DApps.