Build Desktop Applications with Tauri


1. Story behind.
"JavaScript developers can’t build anything except web applications." Every time I hear this, my reaction is — Really? 🤔
Many people think JavaScript is limited to just web development, but that’s far from the truth. Trust me, JavaScript is almost everywhere.
In this article, we’ll explore how JavaScript can be used to build desktop applications using Tauri.
For context, Tauri uses Rust under the hood for its backend.
2.Setup tauri in existing React Project.
- step one install tauri cli
npm install -g @tauri-apps/cli
// or if you prefer using it locally then
npm install --save-dev @tauri-apps/cli
- Install tauri in your project
npm install @tauri-apps/api // This lets your React frontend talk to the Tauri backend (eg. for filesystem, dialogs, etc.)
npx tauri init
This will add src-tauri folder and configure everything.
- To run the tauri app
npm run tauri dev
3. New Tauri App Setup.
If you are starting with the tauri i would recommend you to start it from scratch. this would be easy to understand the setup.
npm create tauri-app@latest
follow the the instructions to setup the ui library, language preference and other necessary things.
select the unique identifier for the application. (it’s mostly recommended to put you business domain)
>> Identifier (com.tauri-app.app)
select the language for building frontend (UI layer), I recommend to use Typescript/Javascript
>> Choose which language to use for your frontend Rust (Cargo) Typescript / Javascript (npm, yarn, pnpm, bun) .NET
select the package manager.
>> Choose your package manager pnpm yarn npm bun
select the UI template
>> Choose your UI template Vanilla Vue Svelte React Solid Angular Preact >> Choose your UI flavor TypeScript JavaScript
install the libraries from package.json
cd /project_name npm install
Run development server.
npm run tauri dev
You’ll now see a new window open with your app running.
Congratulations! We did it 🤝
4. Communication between UI layer and backend (Rust)
To build the interactive application we need to keep the backend and the UI layer in sync, so for that we get the invoke api from the tauri, where we will create the function with all the logic at rust side and will call it with the help of invoke function from the UI layer using @tauri-apps/api.
- Create the function at rust side.
use tauri::{App, Manager};
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
fn main() {
tauri::Builder::default()
.setup(|app: &mut App| {
// Optional: do something with app
let win = app.get_window("main").unwrap(); //This requires Manager because get_window is from the Manager trait.
win.set_title("New Title").unwrap();
Ok(())
})
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
- Now we can call the same function from react component
import { useState } from 'react'
import { invoke } from '@tauri-apps/api'
function App() {
const [name, setName] = useState('')
const [greeting, setGreeting] = useState('')
async function greetUser() {
const msg = await invoke('greet', { name })
setGreeting(msg)
}
return (
<div>
<input value={name} onChange={(e) => setName(e.target.value)} />
<button onClick={greetUser}>Greet</button>
<p>{greeting}</p>
</div>
)
}
export default App
This was all to make the communication between frontend and backend.
5. Understanding .Setup( | app | { … })
Understanding .Setup( | app | { … }) block in Tauri is very powerful. It's called right before your Tauri app launches, and it gives you access to the whole native backend (App, windows, plugins etc.).
It’s a lifecycle hook in tauri that runs: after the tauri initializes, but before the app window is shown. This is where you can configure or access window, Register Global State, Communicate with frontend or Use Plugins.
Access and Control Windows
.setup(|app| { let win = app.get_window("main").unwrap(); win.set_title("My App Title").unwrap(); win.set_always_on_top(true).unwrap(); }) //You can use all the methods provided by tauri::Window
Emit Events to Frontend
// Rust side (Backend) let win = app.get_window("main").unwrap(); win.emit("backend-ready", "Tauri backend initialized").unwrap();
//React side (Frontend) //Listen this at react side import { listen } from '@tauri-apps/api/event'; listen('backend-ready', (event) => { console.log(event.payload); });
6. Know More about WebView
We can actually control Webview over here inside setup. we can control closeing of application waking application on start on the machine and lot more. Below is the example of how we can restrict user from closing the window and auto launching application on start of machine.
.setup(|app| {
let app_handle = app.handle();
// Show main window on startup
if let Some(window) = app.get_webview_window("main") {
if let Err(e) = window.show() {
eprintln!("Failed to show window: {}", e);
}
if let Err(e) = window.set_focus() {
eprintln!("Failed to focus window: {}", e);
}
// Prevent window from closing
window.on_window_event(move |event| {
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
api.prevent_close();
println!("Close prevented - application will continue running");
}
});
}
#[cfg(desktop)]
{
// Enable autostart if not already enabled
let autostart_manager = app.autolaunch();
if let Ok(false) = autostart_manager.is_enabled() {
match autostart_manager.enable() {
Ok(_) => println!("Autostart enabled successfully."),
Err(e) => eprintln!("Failed to enable autostart: {}", e),
}
}
}
Ok(())
})
7. Understanding Event Emmiter.
We will emmit the event at defined interval, and at frontend side we will receive that event and perform the specific task.
//Awaake_interval.rs
use tauri::{AppHandle, Emitter};
use std::time::Duration;
#[tauri::command]
pub async fn trigger_sync() -> Result<String, String> {
Ok("sync_triggered".to_string())
}
pub async fn simple_background_timer(app_handle: AppHandle) {
let mut interval = tokio::time::interval(Duration::from_secs(30));
loop {
interval.tick().await;
if let Err(e) = app_handle.emit("background_sync_trigger", ()) {
eprintln!("Failed to emit background_sync_trigger: {}", e);
}
}
}
mod awake_interval
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![awake_interval::trigger_sync])
.setup(|app| {
let app_handle = app.handle();
tauri::async_runtime::spawn(awake_interval::simple_background_timer(app_handle.clone()));
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
// App.jsx
import { useCallback, useEffect, useState } from "react";
import { listen } from "@tauri-apps/api/event";
const useBackgroundSync = () => {
const [count, setCount] = useState(0);
const performSync = async (syncFunction) => {
try {
if (syncFunction) {
await syncFunction();
}
} catch (error) {
console.error("Sync error:", error);
}
};
const setupBackgroundSync = useCallback((syncFunction) => {
let unlisten;
const setupListener = async () => {
unlisten = await listen("background_sync_trigger", () => {
console.log("Background sync trigger received");
setCount((prevCount) => prevCount + 1);
performSync(syncFunction);
});
};
setupListener();
return () => {
if (unlisten) {
unlisten();
}
};
}, [performSync]);
return {
count,
performSync,
setupBackgroundSync,
};
};
export default useBackgroundSync;
8. Network Calls.
We cannot use native fetch or any library just like axios to make the network calls. tauri restricts this to prevent unauthorized or malicious requests. For this tauri gives as http plugin.
This plugin provides a safe bridge for network access in a desktop environment where direct access could pose security risks. Tauri enforces strict security by requiring you to whitelist domains in the tauri.config.json file to prevent unauthorized or malicious requests. Without this, the app will block outgoing requests for safety.
Setup Steps:
Install the plugin:
cargo add tauri-plugin-http
Add the plugin in lib.rs :
.plugin(tauri_plugin_http::init())
Whitelist URLs in default.json: capablities>default.json
{ "$schema": "../gen/schemas/desktop-schema.json", "identifier": "default", "description": "Capability for the main window", "windows": ["main"], "remote": { "urls": [ "http://localhost:9000", "https://base-url/api/v1/**/**/**" ] }, "permissions": [ "core:default", { "identifier": "http:default", "allow": [ { "url": "http://localhost:9000" }, { "url": "https://base-url/api/v1/**/**/**" } ] } ] }
npm install @tauri-apps/plugin-http
Now we can use the fetch method from @tauri-apps/plugin-http to make the network call
//action.js
import { fetch } from "@tauri-apps/plugin-http";
export const userLogin = async (formData) => {
try {
const BASE_URL = import.meta.env.VITE_BASE_URL;
const response = await fetch(`${BASE_URL}/auth/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email: formData.email,
password: formData.password,
}),
credentials: "include",
});
const data = await response.json();
return data;
} catch (error) {
console.log(error);
throw error;
}
};
Thank You! 📚
Subscribe to my newsletter
Read articles from Viraj Nikam directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
