Es hora de poner en práctica todo lo aprendido. Vamos a construir una aplicación de notas completa que combina SQLite (capítulo 10) y diálogos nativos (capítulo 11). La app tendrá una barra lateral con la lista de notas y un editor a la derecha, todo con un diseño oscuro y profesional.
Este es un proyecto integrador: verás cómo los plugins que aprendiste por separado trabajan juntos en una aplicación real. Al terminar tendrás una app de notas funcional con creación, edición, eliminación con confirmación y persistencia en base de datos.
Crear el proyecto
Primero, si tienes otra aplicación Tauri corriendo en la terminal, detenla con Ctrl+C. Luego crea el proyecto nuevo:
npm create tauri-app@latest
El asistente te hará varias preguntas. Responde así:
- Project name: notas-pro
- Identifier: com.tutorial.notaspro
- Frontend language: TypeScript / JavaScript
- Package manager: npm
- UI template: React
- UI flavor: JavaScript
Cuando termine, entra en la carpeta e instala las dependencias:
cd notas-pro
npm install
Instalar los plugins
Necesitamos dos plugins: sql para la base de datos SQLite y dialog para los diálogos nativos de confirmación. Instálalos uno por uno:
npm run tauri add sql
npm run tauri add dialog
Verificar la configuración de SQLite
Abre el archivo src-tauri/Cargo.toml y busca la línea de tauri-plugin-sql. Es importante que tenga habilitada la feature de SQLite. Si ves algo como esto:
tauri-plugin-sql = "2"
Cámbialo a esto para activar SQLite:
tauri-plugin-sql = { version = "2", features = ["sqlite"] }
Si ya aparece con features = ["sqlite"], no necesitas cambiar nada.
Registrar los plugins en Rust
Abre src-tauri/src/lib.rs y verifica que la función run() tenga los tres plugins registrados. Debería verse así:
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_sql::Builder::default().build())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_opener::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
El comando npm run tauri add normalmente añade los plugins automáticamente, pero siempre es bueno verificar. El plugin opener ya viene incluido con el proyecto por defecto.
Configurar los permisos
Abre src-tauri/capabilities/default.json y reemplaza su contenido con lo siguiente:
{
"identifier": "default",
"windows": ["main"],
"permissions": [
"core:default",
"opener:default",
"sql:default",
"sql:allow-execute",
"sql:allow-select",
"dialog:default"
]
}
Veamos qué hace cada permiso:
core:default– Permisos básicos de Tauri (siempre necesario).opener:default– Permite abrir enlaces externos en el navegador.sql:default– Permite conectarse a la base de datos.sql:allow-execute– Permite ejecutar sentencias que modifican datos:CREATE TABLE,INSERT,UPDATEyDELETE.sql:allow-select– Permite ejecutar consultasSELECTpara leer datos.dialog:default– Permite mostrar diálogos nativos del sistema operativo.
Importante: sin sql:allow-execute y sql:allow-select, la app podría conectarse a la base de datos pero no podría crear tablas ni leer o escribir datos. Este es un error común que produce mensajes de «permission denied» difíciles de depurar.
Construir la aplicación
Nuestra app va a tener cuatro archivos en la carpeta src/:
App.jsx– Componente principal: inicializa la base de datos, gestiona el estado y las funciones CRUD.ListaNotas.jsx– Barra lateral con la lista de notas.EditorNota.jsx– Editor para la nota seleccionada.App.css– Todos los estilos de la aplicación.
Vamos a crearlos uno por uno.
App.jsx – El componente principal
Abre src/App.jsx y reemplaza todo su contenido con lo siguiente:
import { useState, useEffect } from "react";
import Database from "@tauri-apps/plugin-sql";
import ListaNotas from "./ListaNotas";
import EditorNota from "./EditorNota";
import "./App.css";
function App() {
const [db, setDb] = useState(null);
const [notas, setNotas] = useState([]);
const [notaActual, setNotaActual] = useState(null);
useEffect(() => {
let cancelado = false;
async function iniciarDB() {
const database = await Database.load("sqlite:notas.db");
await database.execute(`
CREATE TABLE IF NOT EXISTS notas (
id INTEGER PRIMARY KEY AUTOINCREMENT,
titulo TEXT NOT NULL,
contenido TEXT DEFAULT "",
fecha TEXT DEFAULT CURRENT_TIMESTAMP
)
`);
if (cancelado) return;
setDb(database);
const resultado = await database.select(
"SELECT * FROM notas ORDER BY fecha DESC"
);
if (!cancelado) setNotas(resultado);
}
iniciarDB();
return () => { cancelado = true; };
}, []);
async function cargarNotas() {
if (!db) return;
const resultado = await db.select(
"SELECT * FROM notas ORDER BY fecha DESC"
);
setNotas(resultado);
}
async function crearNota() {
if (!db) return;
const result = await db.execute(
"INSERT INTO notas (titulo, contenido) VALUES ($1, $2)",
["Nueva nota", ""]
);
await cargarNotas();
const nuevas = await db.select(
"SELECT * FROM notas WHERE id = $1",
[result.lastInsertId]
);
if (nuevas[0]) setNotaActual(nuevas[0]);
}
async function guardarNota(id, titulo, contenido) {
if (!db) return;
await db.execute(
"UPDATE notas SET titulo = $1, contenido = $2 WHERE id = $3",
[titulo, contenido, id]
);
await cargarNotas();
}
async function eliminarNota(id) {
if (!db) return;
await db.execute("DELETE FROM notas WHERE id = $1", [id]);
if (notaActual?.id === id) setNotaActual(null);
await cargarNotas();
}
if (!db) {
return <p style={{ padding: "2rem" }}>Cargando base de datos...</p>;
}
return (
<div className="app">
<ListaNotas
notas={notas}
notaActual={notaActual}
onSelect={setNotaActual}
onNew={crearNota}
onDelete={eliminarNota}
/>
<EditorNota nota={notaActual} onSave={guardarNota} />
</div>
);
}
export default App;
ListaNotas.jsx – La barra lateral
Crea un nuevo archivo src/ListaNotas.jsx con este contenido:
import { confirm } from "@tauri-apps/plugin-dialog";
function ListaNotas({ notas, notaActual, onSelect, onNew, onDelete }) {
async function handleEliminar(e, nota) {
e.stopPropagation();
const ok = await confirm(
'¿Eliminar la nota "' + nota.titulo + '"?',
{ title: "Confirmar", kind: "warning" }
);
if (ok) onDelete(nota.id);
}
return (
<aside className="sidebar">
<div className="sidebar-header">
<h2>Notas</h2>
<button onClick={onNew} title="Nueva nota">+</button>
</div>
<ul>
{notas.map((nota) => (
<li
key={nota.id}
className={notaActual?.id === nota.id ? "active" : ""}
onClick={() => onSelect(nota)}
>
<div className="nota-info">
<strong>{nota.titulo}</strong>
<small>{new Date(nota.fecha).toLocaleDateString()}</small>
</div>
<button
className="btn-eliminar"
onClick={(e) => handleEliminar(e, nota)}
>
x
</button>
</li>
))}
</ul>
{notas.length === 0 && (
<p className="empty">No hay notas. Crea una con el botón +</p>
)}
</aside>
);
}
export default ListaNotas;
EditorNota.jsx – El editor de notas
Crea un nuevo archivo src/EditorNota.jsx con este contenido:
import { useState, useEffect } from "react";
function EditorNota({ nota, onSave }) {
const [titulo, setTitulo] = useState("");
const [contenido, setContenido] = useState("");
useEffect(() => {
if (nota) {
setTitulo(nota.titulo);
setContenido(nota.contenido || "");
}
}, [nota]);
async function handleGuardar() {
if (nota) {
await onSave(nota.id, titulo, contenido);
}
}
if (!nota) {
return (
<div className="editor vacio">
<p>Selecciona una nota o crea una nueva</p>
</div>
);
}
return (
<div className="editor">
<input
className="editor-titulo"
value={titulo}
onChange={(e) => setTitulo(e.target.value)}
placeholder="Título de la nota"
/>
<textarea
className="editor-contenido"
value={contenido}
onChange={(e) => setContenido(e.target.value)}
placeholder="Escribe tu nota aquí..."
/>
<button className="btn-guardar" onClick={handleGuardar}>
Guardar
</button>
</div>
);
}
export default EditorNota;
App.css – Los estilos
Abre src/App.css y reemplaza todo su contenido con estos estilos:
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: system-ui, sans-serif; background: #1a1a1a; color: #e0e0e0; }
.app { display: flex; height: 100vh; }
/* Barra lateral */
.sidebar {
width: 280px;
background: #242424;
border-right: 1px solid #333;
display: flex;
flex-direction: column;
}
.sidebar-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border-bottom: 1px solid #333;
}
.sidebar-header h2 { font-size: 1.2rem; }
.sidebar-header button {
width: 32px;
height: 32px;
border: none;
border-radius: 6px;
background: #3b82f6;
color: white;
font-size: 1.3rem;
cursor: pointer;
}
.sidebar ul {
list-style: none;
overflow-y: auto;
flex: 1;
}
.sidebar li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.8rem 1rem;
cursor: pointer;
border-bottom: 1px solid #333;
}
.sidebar li:hover { background: #2a2a2a; }
.sidebar li.active { background: #3b82f6; }
.nota-info {
flex: 1;
overflow: hidden;
}
.nota-info strong {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.nota-info small { color: #888; font-size: 0.8rem; }
.sidebar li.active .nota-info small { color: #ccc; }
.btn-eliminar {
opacity: 0;
background: none;
border: none;
color: #888;
font-size: 1.2rem;
cursor: pointer;
padding: 0 0.3rem;
}
.sidebar li:hover .btn-eliminar { opacity: 1; }
.sidebar .empty {
padding: 2rem 1rem;
text-align: center;
color: #666;
font-size: 0.9rem;
}
/* Editor */
.editor {
flex: 1;
display: flex;
flex-direction: column;
padding: 1.5rem;
}
.editor.vacio {
align-items: center;
justify-content: center;
color: #666;
}
.editor-titulo {
font-size: 1.5rem;
font-weight: 600;
border: none;
background: transparent;
color: #e0e0e0;
padding: 0.5rem 0;
outline: none;
border-bottom: 1px solid #333;
margin-bottom: 1rem;
}
.editor-contenido {
flex: 1;
border: none;
background: transparent;
color: #e0e0e0;
font-size: 1rem;
line-height: 1.6;
resize: none;
outline: none;
}
.btn-guardar {
align-self: flex-end;
padding: 0.6rem 1.5rem;
background: #3b82f6;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 1rem;
margin-top: 1rem;
}
.btn-guardar:hover { background: #2563eb; }
Cómo funciona la aplicación
Ahora que tenemos todo el código, veamos cómo encajan las piezas.
App.jsx: el cerebro de la aplicación
App.jsx es el componente principal y se encarga de tres cosas fundamentales:
- Inicializar la base de datos: el
useEffectse ejecuta al montar el componente. Llama aDatabase.load("sqlite:notas.db")para abrir (o crear) la base de datos, y luego ejecutaCREATE TABLE IF NOT EXISTSpara asegurar que la tablanotasexista. Finalmente carga las notas existentes. - Gestionar el estado: mantiene tres variables de estado:
db(la conexión a la base de datos),notas(el array con todas las notas) ynotaActual(la nota seleccionada en la barra lateral). - Proporcionar funciones CRUD: define cuatro funciones (
cargarNotas,crearNota,guardarNotayeliminarNota) que ejecutan sentencias SQL contra la base de datos. Estas funciones se pasan como props a los componentes hijos.
Fíjate en el patrón let cancelado = false dentro del useEffect. Esto es una protección contra el StrictMode de React. En modo desarrollo, React monta y desmonta los componentes dos veces para detectar problemas. Sin esta protección, la base de datos se inicializaría dos veces, lo cual podría causar errores. Con la variable cancelado, si React desmonta el componente antes de que termine la inicialización, simplemente ignoramos el resultado.
ListaNotas.jsx: la barra lateral
ListaNotas.jsx recibe el array de notas y lo renderiza como una lista. Cuando haces clic en una nota, llama a onSelect para que App sepa cuál se seleccionó. El botón de eliminar (la «x») usa e.stopPropagation() para que el clic no seleccione la nota al mismo tiempo. Antes de eliminar, muestra un diálogo nativo de confirmación usando la función confirm del plugin de diálogos que aprendimos en el capítulo 11.
EditorNota.jsx: el editor
EditorNota.jsx muestra el título y el contenido de la nota seleccionada en campos editables. Usa su propio estado local (titulo y contenido) para que puedas escribir sin que cada tecla dispare una escritura en la base de datos. El useEffect sincroniza el estado local cada vez que se selecciona una nota diferente. Cuando pulsas el botón «Guardar», la función handleGuardar llama a onSave que a su vez ejecuta el UPDATE en la base de datos.
Usamos un botón de guardado manual en lugar de autoguardado por una razón pedagógica: el autoguardado con useEffect requiere lógica adicional (temporizadores, comparación de valores anteriores, limpieza) que añade complejidad innecesaria en este punto del tutorial. El guardado manual es más explícito y fácil de entender.
Cómo se comunican los componentes
La comunicación entre componentes sigue el patrón estándar de React:
- Los datos bajan:
Apppasa datos hacia abajo como props.ListaNotasrecibe el arraynotasy lanotaActual.EditorNotarecibe lanotaseleccionada. - Las acciones suben: los componentes hijos avisan al padre cuando algo pasa.
ListaNotasllama aonSelect,onNewyonDelete.EditorNotallama aonSave. Todas estas son funciones definidas enAppque modifican el estado o la base de datos.
Este patrón se resume en una frase: «los datos fluyen hacia abajo, las acciones fluyen hacia arriba». Es la forma más común de organizar componentes en React y la verás en prácticamente cualquier proyecto.
Ejecutar y probar
Inicia la aplicación en modo desarrollo:
npm run tauri dev
La primera compilación tardará un poco porque Rust necesita compilar los plugins de SQL y diálogos. Una vez que abra la ventana, prueba lo siguiente:
- Haz clic en el botón + para crear varias notas.
- Selecciona una nota y edita su título y contenido. Pulsa Guardar.
- Selecciona otra nota y vuelve a la anterior para comprobar que los cambios se guardaron.
- Pasa el ratón sobre una nota en la barra lateral y haz clic en la x para eliminarla. Debería aparecer un diálogo nativo preguntando si estás seguro.
- Cierra la aplicación completamente y vuelve a abrirla con
npm run tauri dev. Todas tus notas deberían seguir ahí, porque SQLite las persiste en disco.

Resumen
En este capítulo hemos construido una aplicación de notas completa que integra lo aprendido en capítulos anteriores. Repasemos lo que incluye:
- Base de datos SQLite para almacenar las notas de forma persistente (capítulo 10).
- Diálogos nativos para confirmar antes de eliminar una nota (capítulo 11).
- Tres componentes React que se comunican mediante props y funciones callback.
- Operaciones CRUD completas: crear, leer, actualizar y eliminar notas.
- Permisos granulares en Tauri:
sql:allow-executeysql:allow-selectademás desql:default. - Diseño con barra lateral y editor, un patrón de interfaz muy común en aplicaciones de productividad.
- Protección contra StrictMode en la inicialización de la base de datos.
Esta es la primera aplicación del tutorial que realmente se siente como una app de escritorio completa. Tiene persistencia de datos, interfaz dividida en paneles y usa diálogos nativos del sistema operativo.
Siguiente paso
En el próximo capítulo construiremos otro proyecto: un gestor de tareas más completo con prioridades, fechas y filtros. Será una app más ambiciosa que nos permitirá explorar consultas SQL más avanzadas y una interfaz con más interactividad.
¿Te está gustando este tutorial?
Este tutorial forma parte del libro Tauri 2.0: Aplicaciones de Escritorio con React y Rust, disponible en Amazon en formato papel y ebook. El libro incluye todos los capítulos, tres proyectos completos paso a paso y contenido exclusivo que no encontrarás en el blog.
Un saludo, y si aún no lo has hecho no olvides suscribirte a mi blog para no perderte los próximos posts :-),También puedes seguirme en Twitter en @revi_apps y no olvides que me ayudas mucho si compartes este post en las redes sociales.
