Bye-Bye Electron: Building a Feature-Rich To-Do Desktop App with Tauri and Next.js
Hey there, fellow developers! If you’re tired of bloated Electron apps that eat up your computer’s memory and resources, you’re in the right place! In this post, we’ll build a slick and lightweight To-Do application using Tauri and Next.js. We’ll implement proper state management, a splash screen, type safety, testing, and a whole lot more! Let’s dive in! =>https://v2.tauri.app/
Why Tauri and Next.js?
Before we start coding, let’s chat about why Tauri is the better choice over Electron for building desktop applications.
- Performance: Tauri apps are lightweight and use less memory, making them much faster than Electron apps. Bye-bye, sluggish load times!
- Rust Backend: Tauri is built on Rust, which offers safe memory management and efficient garbage collection. You get all the speed of Rust without worrying about memory leaks or performance issues.
- Familiarity: If you’re a full-stack developer like me, you’re probably comfortable with React and React Native. Tauri allows you to use those skills to create powerful desktop apps without switching contexts!
So, let’s get started!
Prerequisites
Make sure you have the following:
- Node.js and npm installed.
- A basic understanding of React, Next.js, and Rust.
- Your favorite code editor (I personally love NeoVim).
Setting Up the Project
Step 1: Create a New Next.js Application
Open your terminal and create a new Next.js app:
npx create-next-app@latest tauri-todo-app
Step 2: Install Tauri
Navigate to your newly created project:
cd tauri-todo-app
Install Tauri as a development dependency:
npm install --save-dev tauri
Step 3: Initialize Tauri
Now, let’s initialize Tauri:
npx tauri init
This command creates a tauri
directory in your project with essential files.
Step 4: Configure Tauri
Open tauri/tauri.conf.json
and update it like this:
{
"tauri": {
"build": {
"beforeBuildCommand": "npm run build",
"distDir": "../out",
"devPath": "http://localhost:3000"
},
"tauri": {
"windows": [
{
"title": "Tauri To-Do App",
"width": 800,
"height": 600,
"resizable": true
}
]
}
}
}
his setup ensures Tauri knows where to find your Next.js app.
Step 5: Create the To-Do App UI
In pages/index.
tsx, let’s build a simple UI with state management. Here’s the code:
import { useState, useEffect } from 'react';
import { invoke } from '@tauri-apps/api';
import SplashScreen from '../components/SplashScreen'; // Import the splash screen component
import styles from '../styles/Home.module.css';
export default function Home() {
const [tasks, setTasks] = useState([]);
const [taskInput, setTaskInput] = useState('');
const [loading, setLoading] = useState(true); // Loading state for splash screen
// Load tasks from local storage on mount
useEffect(() => {
const loadTasks = async () => {
setLoading(true); // Show splash screen
try {
const loadedTasks = await invoke('load_tasks_from_file', { file_path: 'tasks.txt' });
setTasks(loadedTasks);
} catch (error) {
console.error('Failed to load tasks:', error);
} finally {
setLoading(false); // Hide splash screen
}
};
loadTasks();
}, []);
const addTask = async () => {
if (!taskInput) return;
const newTasks = [...tasks, taskInput];
setTasks(newTasks);
await invoke('save_tasks_to_file', { tasks: newTasks, file_path: 'tasks.txt' });
setTaskInput('');
};
const deleteTask = async (index) => {
const newTasks = tasks.filter((_, i) => i !== index);
setTasks(newTasks);
await invoke('save_tasks_to_file', { tasks: newTasks, file_path: 'tasks.txt' });
};
if (loading) return <SplashScreen />; // Show splash screen while loading
return (
<div className={styles.container}>
<h1>Tauri To-Do App</h1>
<input
type="text"
value={taskInput}
onChange={(e) => setTaskInput(e.target.value)}
placeholder="Add a new task"
className={styles.input}
/>
<button onClick={addTask} className={styles.button}>Add Task</button>
<ul className={styles.taskList}>
{tasks.map((task, index) => (
<li key={index}>
{task} <button onClick={() => deleteTask(index)}>Delete</button>
</li>
))}
</ul>
</div>
);
}
Explanation of the Code
- State Management: We manage the list of tasks, input, and loading state using React’s
useState
anduseEffect
hooks. - Loading State: We introduce a loading state to show a splash screen while tasks are being loaded.
- Task Management: We load, add, and delete tasks, saving them to a file using Tauri commands.
Step 6: Create the Splash Screen
Create a new file in the components
directory named SplashScreen.tsx
:
import React from 'react';
import styles from '../styles/SplashScreen.module.css'; // Create your CSS styles
const SplashScreen = () => {
return (
<div className={styles.splashContainer}>
<h1>Loading...</h1>
</div>
);
};
export default SplashScreen;
Step 7: Add Styles
Create a CSS module for the splash screen styles/SplashScreen.module.css
:
.splashContainer {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #282c34;
color: white;
font-size: 2rem;
}
Also, add styles for your main app in styles/Home.module.css
:
.container {
padding: 20px;
}
.input {
margin-right: 10px;
padding: 10px;
}
.button {
padding: 10px;
}
.taskList {
list-style: none;
padding: 0;
}
Step 8: Add Tauri Commands
Now, let’s add some commands in src-tauri/src/main.rs
to handle file operations:
use std::fs;
use tauri::command;
#[command]
fn save_tasks_to_file(tasks: Vec<String>, file_path: String) -> Result<(), String> {
let contents = tasks.join("\n");
fs::write(file_path, contents).map_err(|e| e.to_string())?;
Ok(())
}
#[command]
fn load_tasks_from_file(file_path: String) -> Result<Vec<String>, String> {
let content = fs::read_to_string(file_path).map_err(|e| e.to_string())?;
Ok(content.lines().map(String::from).collect())
}
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![save_tasks_to_file, load_tasks_from_file])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Step 9: Run the Application
You’re almost there! Start your Next.js application:
npm run dev
And in another terminal, run the Tauri application:
npx tauri dev
Your To-Do app should pop up, complete with a splash screen!
Testing Your Application
Step 10: Add Unit Tests
To ensure our app is rock-solid, let’s add some tests! Create a new file named tasks.test.ts
in the tests
directory:
import { render, screen, fireEvent } from '@testing-library/react';
import Home from '../pages/index';
test('renders add task input', () => {
render(<Home />);
const inputElement = screen.getByPlaceholderText(/Add a new task/i);
expect(inputElement).toBeInTheDocument();
});
test('adds a new task', () => {
render(<Home />);
const inputElement = screen.getByPlaceholderText(/Add a new task/i);
const buttonElement = screen.getByText(/Add Task/i);
fireEvent.change(inputElement, { target: { value: 'New Task' } });
fireEvent.click(buttonElement);
expect(screen.getByText(/New Task/i)).toBeInTheDocument();
});
Step 11: Run Your Tests
Run your tests to ensure everything works as expected:
npm run test
Conclusion
And there you have it! We’ve built a robust To-Do application using Tauri and Next.js, complete with state management, a splash screen, type safety, and tests. This project showcases how Tauri leverages the power of Rust for performance and memory safety while allowing you to build with the tools you already love.
Feel free to expand this application with additional features like task prioritization, due dates, or even notifications. The possibilities are endless!
So, say goodbye to heavy Electron apps and embrace the lightweight, efficient world of Tauri! Happy coding!
If you found this guide helpful, share it with your developer community, and let’s spread the word about the awesome potential of Tauri!