🪟 Windows TippsID.3 GTI: VW stellt den stärksten Serien-GTI aller Zeiten vor(16.09.2026 um 10:00 Uhr)
🪟 Windows TippsDoppelte Power: HMX 6 mit 2x RTX 5090 von ZOTAC GAMING(16.09.2026 um 10:25 Uhr)
🪟 Windows TippsAnthbot N8 im Test: Mähroboter mit Fangkorb für Gras und Laub(16.09.2026 um 10:30 Uhr)
🪟 Windows TippsGenesung, Erholung, Entspannung – Aufgaben der Beleuchtung(16.09.2026 um 10:30 Uhr)
🪟 Windows TippsID.3 GTI: VW stellt den stärksten Serien-GTI aller Zeiten vor(16.09.2026 um 10:00 Uhr)
🪟 Windows TippsDoppelte Power: HMX 6 mit 2x RTX 5090 von ZOTAC GAMING(16.09.2026 um 10:25 Uhr)
🪟 Windows TippsAnthbot N8 im Test: Mähroboter mit Fangkorb für Gras und Laub(16.09.2026 um 10:30 Uhr)
🪟 Windows TippsGenesung, Erholung, Entspannung – Aufgaben der Beleuchtung(16.09.2026 um 10:30 Uhr)

🔧 Programmierung 🕛 vor 2 Jahren 15 Min Lesezeit
0

Leptos + Tauri Tutorial

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht

Photo by , cropped




Summer for me is over, and I realized it has been a long time since I posted my last blog. Lately, I've accepted a job which involves the programming on a very specific, proprietary and rather backward piece of software (innuendo intended), it's also a long time I have no fun programming.



Bad, very bad: one should never forget that coding is fun. Let's remedy that.




This is the or is quite "terse," but still enough.



What I want to do here is to give you a walkthough, a



Now, let's add the target wasm32 with rustup, if not already done:




CODE
rustup target add wasm32-unknown-unknown






Now let's create the Tauri app:




CODE
cargo create-tauri-app --rc -m cargo -t leptos -y leptos-tauri && cd leptos-tauri






We will use Rust nightly, at least for this project:




CODE
rustup toolchain install nightly
rustup override set nightly






The last command must be done inside the project root to set it project wide, and leave stable Rust for all your other projects (to set it system-wide: rustup default nightly, but I wouldn't recommend doing that).



Now we can add our Leptos dependencies:




CODE
cargo add leptos --features=csr,nightly
cargo add console_error_panic_hook
cargo add console_log
cargo add log






The first adds leptos with csr and nighlty features, the second adds console_error_panic_hook which is useful in order to use the browser inspector and get some sensible Rust-like error messages (at least the lines that caused the error), instead of the default link to the wasm bundle and unhelpful messages.



console_log is needed in order to log to the browser console.



Now let's create all the additional files we need to make this work.



The following is for the formatters. Inside the project root, let's add a rustfmt.toml file, to set the correct edition:




CODE
# rustfmt.toml
edition = "2021"






Let's add also a rust-analyzer.toml that will help us to override the default rustfmt and use leptosfmt instead:




CODE
# rust-analyzer.toml
[rustfmt]
overrideCommand = ["leptosfmt", "--stdin", "--rustfmt"]






FInally, let's configure leptosfmt with the leptosfmt.toml file:




CODE
# leptosfmt.toml
max_width = 120 # Maximum line width
tab_spaces = 4 # Number of spaces per tab
indentation_style = "Auto" # "Tabs", "Spaces" or "Auto"
newline_style = "Auto" # "Unix", "Windows" or "Auto"
attr_value_brace_style = "WhenRequired" # "Always", "AlwaysUnlessLit", "WhenRequired" or "Preserve"
macro_names = [ "leptos::view", "view" ] # Macro to be formatted by leptosfmt
closing_tag_style = "Preserve" # "Preserve", "SelfClosing" or "NonSelfClosing"

# Attribute values with custom formatters
[attr_values]
class = "Tailwind" # "Tailwind" is the only available formatter for now






Noise. Enough with configuration files, let's do something in src/main.rs. Tauri already created main.rs and app.rs for us, so we need to add few lines to src/main.rs:




CODE
// src/min.rs
mod app;

use app::*;
use leptos::*;

fn main() {
console_error_panic_hook::set_once();
_ = console_log::init_with_level(log::Level::Debug);
mount_to_body(|| {
view! {
<App/>
}
})
}






Let's explain the code a little bit, for those who are new with Leptos:




  • We initialize console_error_panic_hook with the set_once() function.


  • We also use console_log::init_with_level() to correctly init the logging on the web browser console of any error


  • mount_to_body is a leptos function to mount a IntoView type to the body of the page. In this case we pass it a function that returns a IntoView type through the view! macro. This macro accepts HTML-like syntax, and in this case we pass to it a custom component that we need to create in the module mod app.




Now we need to take care of said App component inside src/app.rs (already created by Tauri's template for us); replace all the content of the file with the following:




CODE
// src/app.rs
use leptos::*;

#[component]
pub fn App() -> impl IntoView {
let (count, set_count) = create_signal(0);

view! {
<div>
<button on:click=move |_| {
set_count.update(|n| *n += 1);
}>"+1"</button>
<button on:click=move |_| {
set_count.update(|n| *n -= 1);
}>"-1"</button>
<p class:red=move || count() < 0>"Counter: "{count}</p>
</div>
}
}







  • At the heart, a component can be just a function that returns an IntoView type, marked by #[component]. The name of the function will be the name of the component, that is why we wrote it with a capitalized first letter (yes, it's a React-like style).


  • At the beginning of our function, we set a signal, which is a reactive type, meaning that it can change during the WASM app lifecycle, and in turn make the component change because of it. We use create_signal() which initializes a type given to it as parameter, and returns a getter and a setter for the reactive type thus created, in this case count and set_count respectively.


  • We then use the view! macro which returns an IntoView, and accets a HTML-like syntax mixed with Rust. We create a <div> containing two <button> and a <p>. We can pass a move closure to the on:click parameter of the button component, where we update the value in the signal we created earlier. We update the signal's value using the update() function on the signal's setter. The update() function accepts a closure where the parameter is the current value of the signal. With this method we creatd a button that increases the counter and a button to decrease it


  • Inside the view! macro we also use the signal getter (simply {count} for the rendering in view!, but count() if needed inside a closure). The getter will visualize the current value of the signal. Notice also that we conditionally assign a class to the <p> only if the value of the signal is less than 0, with the syntax class:<value>=<move closure>




To render the view! inside the fn main() we decared that it will be mounted to the <body> of the page, but which page? Tauri already created for us a index.html file for it, so we dont need to worry; moreoer the template created also a styles.css imported in the index. In this way, we just need to add the following style to the already created styles.css:




CODE
/* Append this at the end of styles.css */
.red {
color: red;
}






This .red CSS class will be condiionally assigned to the <p> inside our <App> component as we saw earlier.



Now if all went well, running trunk should show our Leptos app inside our favorite browser:




CODE
trunk serve --open






Notice that if the --open flag does not work, it is sufficient to point the browser to localhost:1420






Adding Back Tauri Integration



We deleted everything in src/app.rs, but in those lines there was also an example of Tauri integration. Now we can bring back our own integration code and understand things a little better, step by step.



Presently the first thing we will do is to launch the tauri app proper with:




CODE
cargo tauri dev






With this we launched th app inside a desktop Tauri app, instead of the browser. Notice that with <ctrl>+<shift>+i we have an inspector even in the tauri window... sweet!



Now, let's take care of creating some Tauri integration, as said.




To follow this part of the tutorial you can checkout this tag: git checkout v1.2




When we used cargo create-tauri-app the leptos template we used added a src-tauri/ folder we ignored so far. Inside this foder, we have a src-tauri/main.rs and a src-tauri/lib.rs.



The main.rs is minimal and it is used to basically call the leptos_tauri_lib::run().



The src-tauri/lib.rs instead is where we can put some functions to be called by the leptos front-end.



What we have already is the following:




CODE
// src-tauri/lib.rs
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}






We have a custom fn greet() that is a function annotated as a #[tauri:command]. This function is passed to the tauri::Builder in fn run() by generating a invoke_handler. Let's change this file to have an increase and decrease command to use for the counter.



This is the new src-tauri/lib.rs:




CODE
// src-tauri/lib.rs
#[tauri::command]
fn increase(count: i32) -> i32 {
count + 1
}

#[tauri::command]
fn decrease(count: i32) -> i32 {
count - 1
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.invoke_handler(tauri::generate_handler![increase, decrease])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}






As you can see, we created two tauri::command increase and decrease. Then, in the tauri:Builder inside the run() function (which is the main entry-point for the tauri app), we pass them to the invoke_handler(); the rest is all boiler-plate.



Back to our src/app.rs, we need to let our App component use these two functions (tauri commands).



The first thing we need is to add the needed use statements:




CODE
// src/app.rs
use leptos::*;
use leptos::leptos_dom::ev::MouseEvent;
use serde::{Deserialize, Serialize};
use serde_wasm_bindgen::to_value;
use wasm_bindgen::prelude::*;






We added the MouseEvent from leptos::leptos_dom::ev::MouseEvent, which will let us handle the creation of a custom closure for the on:click in our buttons.



We imported Deserialize and Serialize from serde, and to_value from the serde_wasm_bindgen, and of course the wasm_bindgen itself, because we need to pass arguments from the Rust frontend to the Tauri backend, passing through JavaScript (Remeber the rant I told you to skip? Just forget it already).



Now, with an external call to the wasm_bindgen we bind to a generic JavaScript function invoke that is meant to call the Tauri commands:




CODE
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"])]
async fn invoke(cmd: &str, args: JsValue) -> JsValue;
}






At the end of the day, it's just boiler-plate to setup at the beginning of our components; then we can use this to bridge from the front-end to the back-end.



Next, we create a struct to pass arguments to our Tauri commands:




CODE
#[derive(Serialize, Deserialize)]
struct CounterArgs {
count: i32,
}






The struct will be converted to a JsValue by serde and it's ready to be passed to the invoke function as argument.



Finall there's our component. After the signal we need to create two closures to communicate with the Tauri commands. We will discuss just the increase_me, but the considerations are valid for both closures:




CODE
let increase_me = move |ev: MouseEvent| {
ev.prevent_default();
spawn_local(async move {
let count = count.get_untracked();
let args = to_value(&CounterArgs { count }).unwrap();
let new_value = invoke("increase", args).await.as_f64().unwrap();
set_count.set(new_value as i32);
});
};






In the closure we need to specify that the parameter ev is a MouseEvent type, so we can bind the closure to the on:cick of our button.



With ev.prevent_default(); we prevent the usual event behaviour, since we will provide our own behaviour. This is the same as the Event: preventDefault() method - Web APIs | MDN in the Web API. It is particularly useful for closures that have to handle <input> fields.



We then spawn a thread-local Future with Leptos' spawn_local(), providing an async move closure. This is needed because when we use the invoke bridge, it will return a Future, that is a promise in JavaScript and async in Rust. This is because the Tauri commands are syncronous.



Inside the closure we get the count signal's value with count.get_untracked(). We need the get_untracked() because in this way we prevent the reactive binding that usually is produced with Leptos signals, so that even if the value in the count changes, Leptos will not try to update the closure.



We then create the argument to be passed to the invoke. We use the CounterArgs structure.



We finally invoke the increase command, and retrieve the new value for the counter, which we use to set the signal with its set() provided method. Since the wasm_bindgen::JsValue that gets returned by the invoke bridge does not convert to integers, we need to convert it to f64 first, and then coherce it to a i32 value.




FYI these are the conversions available for a wasm_bindgen::JsValue:





  • as_bool which returns an Option<bool>


  • as_f64 which returns an Option<f64>


  • as_string which returns an Option<String>




Here is the whole src/app.rs code:




CODE
// src/app.rs
use leptos::leptos_dom::ev::MouseEvent;
use leptos::*;
use serde::{Deserialize, Serialize};
use serde_wasm_bindgen::to_value;
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"])]
async fn invoke(cmd: &str, args: JsValue) -> JsValue;
}

#[derive(Serialize, Deserialize)]
struct CounterArgs {
count: i32,
}

#[component]
pub fn App() -> impl IntoView {
let (count, set_count) = create_signal(0);
let increase_me = move |ev: MouseEvent| {
ev.prevent_default();
spawn_local(async move {
let count = count.get_untracked();
let args = to_value(&CounterArgs { count }).unwrap();
let new_value = invoke("increase", args).await.as_f64().unwrap();
set_count.set(new_value as i32);
});
};
let decrease_me = move |ev: MouseEvent| {
ev.prevent_default();
spawn_local(async move {
let count = count.get_untracked();
let args = to_value(&CounterArgs { count }).unwrap();
let new_value = invoke("decrease", args).await.as_f64().unwrap();
set_count.set(new_value as i32);
});
};
view! {
<div>
<button on:click=increase_me>"+1"</button>
<button on:click=decrease_me>"-1"</button>
<p class:red=move || count() < 0>"Counter: "{count}</p>
</div>
}
}









Conclusion



Now with a




CODE
cargo tauri dev






We can appreciate our counter being increased or decrased as it was before, but using code from the back-end.



Granted, it feels the same (there should not even be any appreciable decrease in speed), however this example can be adapted to do a whole deal more useful stuff in the back-end and present it on the front-end as needed.



If you are still here, that means you have successfully glossed over all my rants and (maybe) finished the tutorial. Hope this has been instructive, feel free to comment below.

Vollständiger Original-Artikel
Den kompletten Beitrag mit allen Details direkt auf dev.to lesen.
↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 0%
🟡 In Evaluierung 0%
🟢 Keine Auswirkung 0%
Spannende Innovation 0%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
1 Quelle
ID.3 GTI: VW stellt den stärksten Serien-GTI aller Zeiten vor
1 Quelle
32 Kerne, 128 GB RAM, 80 TB: Das war unsere Profi-Höllenmaschine HMX Pro
1 Quelle
Doppelte Power: HMX 6 mit 2x RTX 5090 von ZOTAC GAMING
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Leptos + Tauri Tutorial

Thematisch verwandte Begriffe: Leptos, Tauri, Tutorial · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...