Tauri 2 + React : Construire une application desktop multiplateforme avec Rust

Introduction
Electron a longtemps été le choix par défaut pour construire des applications desktop avec les technologies web, mais il embarque un navigateur Chromium complet avec chaque application — ce qui produit des binaires de 150 Mo+ et une consommation mémoire élevée. Tauri 2 change la donne en utilisant la webview native du système d'exploitation, produisant des applications 10 à 100 fois plus petites et nettement plus rapides.
Avec Tauri 2, vous écrivez votre interface en React (ou tout autre framework frontend) et votre logique backend en Rust. Le résultat est une application desktop sécurisée et performante qui fonctionne sur Windows, macOS, Linux — et même iOS et Android.
Dans ce tutoriel, vous allez construire une application de notes de bureau — une application de prise de notes simple mais fonctionnelle avec accès au système de fichiers, une icône dans le system tray et un packaging multiplateforme.
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 18+ installé
- Rust installé via rustup
- Des connaissances de base en React et TypeScript
- Un éditeur de code (VS Code recommandé avec l'extension
rust-analyzer)
Exigences spécifiques par plateforme
macOS :
xcode-select --installWindows :
- Outils de build Microsoft Visual Studio C++
- WebView2 (préinstallé sur Windows 10/11)
Linux (Ubuntu/Debian) :
sudo apt update
sudo apt install libwebkit2gtk-4.1-dev build-essential curl wget file libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-devCe que vous allez construire
Une application de notes de bureau avec les fonctionnalités suivantes :
- Créer, modifier et supprimer des notes
- Sauvegarder les notes dans le système de fichiers local
- Intégration du system tray avec des actions rapides
- Packaging multiplateforme (Windows, macOS, Linux)
- Contrôles de fenêtre et menus natifs
Étape 1 : Créer le projet Tauri
Utilisez l'outil officiel create-tauri-app pour initialiser votre projet :
npm create tauri-app@latest notes-app -- --template react-tsLorsque vous y êtes invité, sélectionnez :
- Gestionnaire de paquets : npm
- Template UI : React
- Variante UI : TypeScript
Naviguez vers votre projet et installez les dépendances :
cd notes-app
npm installStructure du projet
Votre projet comporte deux parties principales :
notes-app/
├── src/ # Frontend React
│ ├── App.tsx
│ ├── main.tsx
│ └── styles.css
├── src-tauri/ # Backend Rust
│ ├── src/
│ │ └── lib.rs
│ ├── Cargo.toml
│ ├── tauri.conf.json
│ └── capabilities/
└── package.json
Le répertoire src/ contient votre application React, tandis que src-tauri/ contient le backend Rust et la configuration Tauri.
Étape 2 : Configurer Tauri
Ouvrez src-tauri/tauri.conf.json et mettez à jour la configuration de l'application :
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Notes App",
"version": "1.0.0",
"identifier": "com.notes.app",
"build": {
"frontendDist": "../dist",
"devUrl": "http://localhost:1420",
"beforeDevCommand": "npm run dev",
"beforeBuildCommand": "npm run build"
},
"app": {
"title": "Notes App",
"windows": [
{
"title": "Notes App",
"width": 900,
"height": 680,
"resizable": true,
"fullscreen": false
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}Étape 3 : Ajouter les plugins Tauri
Tauri 2 utilise un système de plugins pour accéder aux API système. Installez les plugins de système de fichiers et de dialogue :
npm run tauri add fs
npm run tauri add dialogCela met à jour à la fois le package.json (bindings JavaScript) et le Cargo.toml (dépendances Rust).
Configurer les permissions
Tauri 2 exige des permissions explicites pour l'accès aux plugins. Mettez à jour src-tauri/capabilities/default.json :
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"fs:default",
"fs:allow-read-text-file",
"fs:allow-write-text-file",
"fs:allow-exists",
"fs:allow-mkdir",
"fs:allow-read-dir",
"fs:allow-remove",
"fs:scope-app-data",
"dialog:allow-open",
"dialog:allow-save",
"dialog:allow-message"
]
}Étape 4 : Créer les commandes Rust du backend
Les commandes Tauri sont des fonctions Rust que votre frontend peut appeler. Ouvrez src-tauri/src/lib.rs et ajoutez la logique de gestion des notes :
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
use tauri::Manager;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Note {
pub id: String,
pub title: String,
pub content: String,
pub created_at: String,
pub updated_at: String,
}
fn get_notes_dir(app: &tauri::AppHandle) -> PathBuf {
let data_dir = app.path().app_data_dir().expect("Failed to get app data dir");
let notes_dir = data_dir.join("notes");
if !notes_dir.exists() {
fs::create_dir_all(¬es_dir).expect("Failed to create notes directory");
}
notes_dir
}
#[tauri::command]
fn list_notes(app: tauri::AppHandle) -> Result<Vec<Note>, String> {
let notes_dir = get_notes_dir(&app);
let mut notes: Vec<Note> = Vec::new();
let entries = fs::read_dir(¬es_dir).map_err(|e| e.to_string())?;
for entry in entries {
let entry = entry.map_err(|e| e.to_string())?;
let path = entry.path();
if path.extension().map_or(false, |ext| ext == "json") {
let content = fs::read_to_string(&path).map_err(|e| e.to_string())?;
let note: Note = serde_json::from_str(&content).map_err(|e| e.to_string())?;
notes.push(note);
}
}
notes.sort_by(|a, b| b.updated_at.cmp(&a.updated_at));
Ok(notes)
}
#[tauri::command]
fn save_note(app: tauri::AppHandle, note: Note) -> Result<Note, String> {
let notes_dir = get_notes_dir(&app);
let file_path = notes_dir.join(format!("{}.json", note.id));
let json = serde_json::to_string_pretty(¬e).map_err(|e| e.to_string())?;
fs::write(&file_path, json).map_err(|e| e.to_string())?;
Ok(note)
}
#[tauri::command]
fn delete_note(app: tauri::AppHandle, id: String) -> Result<(), String> {
let notes_dir = get_notes_dir(&app);
let file_path = notes_dir.join(format!("{}.json", id));
if file_path.exists() {
fs::remove_file(&file_path).map_err(|e| e.to_string())?;
}
Ok(())
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_dialog::init())
.invoke_handler(tauri::generate_handler![list_notes, save_note, delete_note])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}Ajoutez la dépendance serde_json dans src-tauri/Cargo.toml :
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tauri = { version = "2", features = [] }
tauri-plugin-fs = "2"
tauri-plugin-dialog = "2"Étape 5 : Construire le frontend React
Maintenant, créons l'interface utilisateur. D'abord, installez quelques paquets utiles :
npm install uuid
npm install -D @types/uuidDéfinition du type Note
Créez src/types.ts :
export interface Note {
id: string;
title: string;
content: string;
created_at: string;
updated_at: string;
}Composant Liste des notes
Créez src/components/NotesList.tsx :
import { Note } from "../types";
interface NotesListProps {
notes: Note[];
selectedId: string | null;
onSelect: (note: Note) => void;
onDelete: (id: string) => void;
onCreate: () => void;
}
export function NotesList({ notes, selectedId, onSelect, onDelete, onCreate }: NotesListProps) {
return (
<aside className="notes-sidebar">
<div className="sidebar-header">
<h2>Notes</h2>
<button onClick={onCreate} className="btn-new" title="Nouvelle note">
+
</button>
</div>
<ul className="notes-list">
{notes.map((note) => (
<li
key={note.id}
className={`note-item ${selectedId === note.id ? "active" : ""}`}
onClick={() => onSelect(note)}
>
<div className="note-item-title">
{note.title || "Sans titre"}
</div>
<div className="note-item-date">
{new Date(note.updated_at).toLocaleDateString("fr")}
</div>
<button
className="btn-delete"
onClick={(e) => {
e.stopPropagation();
onDelete(note.id);
}}
title="Supprimer"
>
×
</button>
</li>
))}
</ul>
</aside>
);
}Composant Éditeur de notes
Créez src/components/NoteEditor.tsx :
import { useState, useEffect } from "react";
import { Note } from "../types";
interface NoteEditorProps {
note: Note | null;
onSave: (note: Note) => void;
}
export function NoteEditor({ note, onSave }: NoteEditorProps) {
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
useEffect(() => {
if (note) {
setTitle(note.title);
setContent(note.content);
}
}, [note?.id]);
if (!note) {
return (
<main className="editor-empty">
<p>Sélectionnez une note ou créez-en une nouvelle</p>
</main>
);
}
const handleSave = () => {
onSave({
...note,
title,
content,
updated_at: new Date().toISOString(),
});
};
return (
<main className="editor">
<input
type="text"
className="editor-title"
value={title}
onChange={(e) => setTitle(e.target.value)}
onBlur={handleSave}
placeholder="Titre de la note..."
/>
<textarea
className="editor-content"
value={content}
onChange={(e) => setContent(e.target.value)}
onBlur={handleSave}
placeholder="Commencez à écrire..."
/>
</main>
);
}Composant principal de l'application
Remplacez src/App.tsx :
import { useState, useEffect } from "react";
import { invoke } from "@tauri-apps/api/core";
import { v4 as uuidv4 } from "uuid";
import { NotesList } from "./components/NotesList";
import { NoteEditor } from "./components/NoteEditor";
import { Note } from "./types";
import "./App.css";
function App() {
const [notes, setNotes] = useState<Note[]>([]);
const [selectedNote, setSelectedNote] = useState<Note | null>(null);
useEffect(() => {
loadNotes();
}, []);
async function loadNotes() {
try {
const result = await invoke<Note[]>("list_notes");
setNotes(result);
} catch (error) {
console.error("Failed to load notes:", error);
}
}
async function createNote() {
const newNote: Note = {
id: uuidv4(),
title: "",
content: "",
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
};
try {
await invoke("save_note", { note: newNote });
setNotes((prev) => [newNote, ...prev]);
setSelectedNote(newNote);
} catch (error) {
console.error("Failed to create note:", error);
}
}
async function saveNote(note: Note) {
try {
await invoke("save_note", { note });
setNotes((prev) =>
prev.map((n) => (n.id === note.id ? note : n))
);
setSelectedNote(note);
} catch (error) {
console.error("Failed to save note:", error);
}
}
async function deleteNote(id: string) {
try {
await invoke("delete_note", { id });
setNotes((prev) => prev.filter((n) => n.id !== id));
if (selectedNote?.id === id) {
setSelectedNote(null);
}
} catch (error) {
console.error("Failed to delete note:", error);
}
}
return (
<div className="app">
<NotesList
notes={notes}
selectedId={selectedNote?.id ?? null}
onSelect={setSelectedNote}
onDelete={deleteNote}
onCreate={createNote}
/>
<NoteEditor note={selectedNote} onSave={saveNote} />
</div>
);
}
export default App;Styles
Remplacez src/App.css avec un design propre et natif :
:root {
--sidebar-width: 280px;
--bg-primary: #1e1e2e;
--bg-secondary: #181825;
--bg-surface: #313244;
--text-primary: #cdd6f4;
--text-secondary: #a6adc8;
--accent: #89b4fa;
--danger: #f38ba8;
--border: #45475a;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
}
.app {
display: flex;
height: 100vh;
}
/* Barre latérale */
.notes-sidebar {
width: var(--sidebar-width);
background: var(--bg-secondary);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
}
.sidebar-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid var(--border);
}
.sidebar-header h2 {
font-size: 18px;
font-weight: 600;
}
.btn-new {
width: 32px;
height: 32px;
border-radius: 8px;
border: none;
background: var(--accent);
color: var(--bg-primary);
font-size: 20px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.btn-new:hover {
opacity: 0.85;
}
/* Liste des notes */
.notes-list {
list-style: none;
overflow-y: auto;
flex: 1;
}
.note-item {
padding: 12px 16px;
border-bottom: 1px solid var(--border);
cursor: pointer;
position: relative;
transition: background 0.15s;
}
.note-item:hover {
background: var(--bg-surface);
}
.note-item.active {
background: var(--bg-surface);
border-left: 3px solid var(--accent);
}
.note-item-title {
font-size: 14px;
font-weight: 500;
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.note-item-date {
font-size: 11px;
color: var(--text-secondary);
}
.btn-delete {
position: absolute;
top: 8px;
right: 8px;
width: 24px;
height: 24px;
border-radius: 4px;
border: none;
background: transparent;
color: var(--text-secondary);
font-size: 16px;
cursor: pointer;
opacity: 0;
transition: opacity 0.15s;
}
.note-item:hover .btn-delete {
opacity: 1;
}
.btn-delete:hover {
background: var(--danger);
color: white;
}
/* Éditeur */
.editor {
flex: 1;
display: flex;
flex-direction: column;
padding: 24px;
}
.editor-empty {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
}
.editor-title {
font-size: 28px;
font-weight: 700;
border: none;
background: transparent;
color: var(--text-primary);
outline: none;
margin-bottom: 16px;
padding: 4px 0;
}
.editor-content {
flex: 1;
border: none;
background: transparent;
color: var(--text-primary);
outline: none;
font-size: 15px;
line-height: 1.7;
resize: none;
font-family: inherit;
}Étape 6 : Lancer le serveur de développement
Démarrez le serveur de développement Tauri :
npm run tauri devCela lance à la fois le serveur Vite (pour le rechargement à chaud de React) et la fenêtre Tauri. Vous devriez voir votre application de notes avec une barre latérale et un éditeur. Rust se recompile automatiquement lorsque vous modifiez les fichiers dans src-tauri/.
Étape 7 : Ajouter le system tray
Tauri 2 prend en charge nativement l'intégration du system tray. Mettez à jour src-tauri/src/lib.rs pour ajouter une icône dans la barre :
use tauri::{
menu::{Menu, MenuItem},
tray::TrayIconBuilder,
Manager,
};
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_dialog::init())
.setup(|app| {
let quit = MenuItem::with_id(app, "quit", "Quitter", true, None::<&str>)?;
let show = MenuItem::with_id(app, "show", "Afficher la fenêtre", true, None::<&str>)?;
let menu = Menu::with_items(app, &[&show, &quit])?;
TrayIconBuilder::new()
.menu(&menu)
.tooltip("Application Notes")
.on_menu_event(|app, event| match event.id.as_ref() {
"quit" => app.exit(0),
"show" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}
_ => {}
})
.build(app)?;
Ok(())
})
.invoke_handler(tauri::generate_handler![list_notes, save_note, delete_note])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}Ajoutez une image d'icône pour la barre dans src-tauri/icons/tray.png (PNG 32x32 avec transparence).
Étape 8 : Compiler et empaqueter
Lorsque votre application est prête, compilez-la pour la distribution :
npm run tauri buildCela compile le backend Rust en mode release et empaquète votre application :
| Plateforme | Sortie | Taille |
|---|---|---|
| macOS | .dmg, .app | ~8 Mo |
| Windows | .msi, .exe | ~5 Mo |
| Linux | .deb, .AppImage | ~7 Mo |
Les bundles sont générés dans src-tauri/target/release/bundle/.
Comparez cela à une application Electron équivalente de 150-200 Mo — les applications Tauri sont considérablement plus petites.
Étape 9 : Mise à jour automatique (optionnel)
Tauri 2 inclut un système de mise à jour intégré. Ajoutez-le à votre projet :
npm run tauri add updaterConfigurez le point de terminaison de mise à jour dans tauri.conf.json :
{
"plugins": {
"updater": {
"endpoints": [
"https://releases.myapp.com/{{target}}/{{arch}}/{{current_version}}"
],
"pubkey": "YOUR_PUBLIC_KEY"
}
}
}Générez les clés de signature avec :
npm run tauri signer generate -- -w ~/.tauri/myapp.keyDépannage
Erreurs de compilation Rust
Assurez-vous que votre chaîne d'outils Rust est à jour :
rustup updateFenêtre blanche sous Linux
Installez la bonne version de WebKitGTK :
sudo apt install libwebkit2gtk-4.1-devErreurs de permission refusée
Vérifiez que votre capabilities/default.json inclut les permissions requises pour chaque plugin que vous utilisez. Tauri 2 est strict sur les permissions par conception.
Première compilation lente
La première compilation Rust télécharge et compile toutes les dépendances. Les compilations suivantes sont beaucoup plus rapides grâce au cache. Utilisez cargo clean uniquement lorsque c'est nécessaire.
Prochaines étapes
Maintenant que vous avez une application Tauri 2 fonctionnelle, vous pouvez :
- Ajouter des raccourcis clavier avec le plugin de raccourcis globaux de Tauri
- Implémenter le rendu Markdown dans l'éditeur
- Ajouter une fonctionnalité de recherche dans les notes
- Mettre en place un CI/CD avec GitHub Actions pour des compilations automatisées
- Ajouter un stockage en base de données avec SQLite via le plugin
tauri-plugin-sql - Compiler pour le mobile (iOS/Android) grâce au support mobile de Tauri 2
Conclusion
Vous avez construit une application desktop complète avec Tauri 2 et React. Comparée à Electron, votre application est plus petite, plus rapide et plus sécurisée — tirant parti des performances de Rust et du modèle de sécurité basé sur les permissions de Tauri. Le système de plugins facilite l'accès aux API natives tout en maintenant une empreinte minimale. Tauri 2 représente l'avenir du développement d'applications desktop multiplateformes pour les développeurs web.
Discutez de votre projet avec nous
Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.
Trouvons les meilleures solutions pour vos besoins.
Articles connexes

Optimiser Cursor AI pour le développement React et Next.js
Un guide complet pour configurer les paramètres et fonctionnalités de Cursor AI pour une efficacité maximale lors du travail avec React, Next.js et Tailwind CSS.

Flouci : Le Compte Bancaire Professionnel Gratuit pour Auto-entrepreneurs en Tunisie
Découvrez comment Flouci offre un compte bancaire professionnel gratuit et entièrement digital aux auto-entrepreneurs et professionnels (personne physique) en Tunisie, simplifiant la gestion financière.
Meilleures Pratiques pour la Sauvegarde et la Restauration de Base de Données
Découvrez des conseils essentiels et les meilleures pratiques pour garantir des sauvegardes et des restaurations de base de données fiables, protégeant vos données contre les pertes inattendues et les interruptions.