Build Desktop Applications with Tauri

Viraj NikamViraj Nikam
6 min read

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.

  1. 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
  1. 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.

  1. 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.

  1. select the unique identifier for the application. (it’s mostly recommended to put you business domain)

     >> Identifier (com.tauri-app.app)
    
  2. 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
    
  3. select the package manager.

     >> Choose your package manager 
     pnpm
     yarn
     npm
     bun
    
  4. select the UI template

     >> Choose your UI template 
     Vanilla
     Vue
     Svelte
     React
     Solid
     Angular
     Preact
    
     >> Choose your UI flavor 
     TypeScript
     JavaScript
    
  5. install the libraries from package.json

     cd /project_name
     npm install
    
  6. 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.

  1. 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");
}
  1. 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.

  1. 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
    
  2. 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:

  1. Install the plugin:

     cargo add tauri-plugin-http
    
  2. Add the plugin in lib.rs :

     .plugin(tauri_plugin_http::init())
    
  3. 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! 📚

0
Subscribe to my newsletter

Read articles from Viraj Nikam directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Viraj Nikam
Viraj Nikam