Bye-Bye Electron: Building a Feature-Rich To-Do Desktop App with Tauri and Next.js

Amit Chakraborty
5 min readOct 20, 2024

--

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 and useEffecthooks.
  • 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!

--

--

Amit Chakraborty
Amit Chakraborty

Written by Amit Chakraborty

dApp Mobile Solutions Architect | Lead Mobile & Full Stack Developer | 7+ Years in React, React Native, Fullstack | Blockchain & Web3 Innovator

No responses yet