Create a desktop app in Rust using Tauri and Yew
Tags: rust, webassembly, tauri, yew
Published: 2022-01-16 05:31:38

I recently created a complete desktop app in Rust using Tauri and Yew. A few people expressed interest in how I did this, so here is a tutorial to show how to get started.

If you're into Rust and want to build desktop apps, Tauri is a great choice. It's still very early days for web front-ends in Rust but Yew is already quite usable as you will see.

So let's get started.

First let's create a directory for our new project. We'll create a monorepo containing separate directories for the front-end and the back-end.

```
mkdir tauri-yew-demo
cd tauri-yew-demo
git init
```

If you like, you can create a new github repository for it and add the git origin following the instructions [here](https://docs.github.com/en/get-started/getting-started-with-git/managing-remote-repositories).

All of the code in this tutorial is available on my github if you want to just download it and follow along:
https://github.com/stevepryde/tauri-yew-demo

## Front-end

Next we'll create the front-end directory. We do this first because later the `tauri` setup will ask where this directory is.

```
cargo new --bin frontend
cd frontend
mkdir public
```

(The `public` directory is where our CSS will go)

Since this will be the `Yew` part of the project, let's follow the setup instructions here: https://yew.rs/docs/getting-started/introduction

```
rustup target add wasm32-unknown-unknown
cargo install trunk
cargo install wasm-bindgen-cli
```

Next create `index.html` in the base of the `frontend` directory:

frontend/index.html
```html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Tauri Yew Demo App</title>

    <link data-trunk rel="css" href="/public/main.css"/>
  </head>
  <body></body>
</html>
```

We'll also create `main.css` in the `public/` directory we created earlier:

frontend/public/main.css
```css
body {
    margin: 20px;
    background: #2d2d2d;
    color: white;
}

.heading {
    text-align: center;
}
```

You can now build your frontend app and view it in the browser! Let's test it out:

```
trunk build
trunk serve
```

Now open your browser and go to `http::/localhost:8080`. You should see a blank page with a grey background.

> You may be wondering why we are viewing this in a browser when we are supposed to be creating a desktop app. We will get to that soon. Tauri essentially bundles your web app inside a desktop app using the OS-provided browser rendering engine to display your front-end app. So the process of creating your front-end part is very similar to creating a regular web front-end, except that instead of making HTTP requests to an external web server, we will make function calls to the tauri back-end via some Javascript glue code (there's very little Javascript required, I promise!).

### Adding Yew

We haven't yet added `yew` to the project, so let's do that now.

To install rust packages to your project, I highly recommend the `cargo-edit` tool, which you can install via `cargo install cargo-edit`.

With `cargo-edit` installed you can do:

```
cargo add yew
```

If you prefer to add dependencies manually, just add the following lines to the `dependencies` section of `Cargo.toml`:

```
yew = "0.19.3"
```

Now open `src/main.rs` and replace its contents with the following:

frontend/src/main.rs
```rust
use yew::prelude::*;

fn main() {
    yew::start_app::<App>();
}

#[function_component(App)]
pub fn app() -> Html {
    html! {
        <div>
            <h2 class={"heading"}>{"Hello, World!"}</h2>
        </div>
    }
}
```

Now you should be able to re-run `trunk serve` and see the updated page in your browser. This is a real Rust front-end, compiled to WASM, running in your browser. Yes, it is that easy!

> By the way, if you ever wish to make a regular `Yew` web front-end, you can follow the exact same process and just continue on with this front-end as a standalone project.

## Adding Tauri

For this step you'll need to be back in the base `tauri-yew-demo` directory. Hit `Ctrl+C` if you're still running `trunk serve`.

If you're still in the `frontend` directory, go up one directory:
```
cd ..
```

Tauri has its own CLI that we'll use to manage the app.

There are various installation options but since we're using Rust, we'll install it via `cargo`.

The Tauri CLI installation instructions can be found here: https://tauri.studio/docs/getting-started/beginning-tutorial#alternatively-install-tauri-cli-as-a-cargo-subcommand

```
cargo install tauri-cli --locked --version ^1.0.0-rc
```

> EDIT: Many readers have had trouble installing the Tauri CLI due to various bugs in previous versions or compatibility issues with newer Rust versions. I recommend checking the [official docs](https://tauri.studio/docs/getting-started/beginning-tutorial#alternatively-install-tauri-cli-as-a-cargo-subcommand) and following the instructions there, especially as the Tauri CLI version gets updated in the future.

Then run `cargo tauri init` which will ask you some questions about the app:

```
$ cargo tauri init
What is your app name?: tauri-yew-demo
What should the window title be?: Tauri Yew Demo
Where are your web assets (HTML/CSS/JS) located, relative to the "<current dir>/src-tauri/tauri.conf.json" file that will be created?: ../frontend/dist
What is the url of your dev server?: http://localhost:8080
```

This will create a `src-tauri` directory, containing all of the back-end code for your app.

You can run `cargo tauri info` to check all of the installation details.

Before we can run the app, we need to tell `tauri` how to actually start the front-end server.

Edit the `tauri.conf.json` file that is in the `src-tauri` directory. This file contains all of the tauri config that tells tauri how to build your app. You will want to bookmark https://tauri.studio/en/docs/api/config/ because it contains a lot of very useful info about all of the options in this file. For now we will just update the build settings.

Replace the `build` section with the following:

src-tauri/tauri.conf.json
```
"build": {
    "distDir": "../frontend/dist",
    "devPath": "http://localhost:8080",
    "beforeDevCommand": "cd frontend && trunk serve",
    "beforeBuildCommand": "cd frontend && trunk build",
    "withGlobalTauri": true
  },
```

This means we won't need to manually run `trunk serve` in another tab while developing. We can develop in the front-end and back-end and both will hot-reload automatically when changes are made.

Ok, we're now ready to start the tauri dev server that you'll use while developing your app:

```
cargo tauri dev
```

And there's your desktop app, running with `tauri`!

## Adding tauri commands

Tauri itself is written in Rust and your back-end application will be a Rust application that provides "commands" through `tauri` in much the same way that a Rust web-server provides routes. You can even have managed state, for things like `sqlite` databases!

Open `src/main.rs` in the `src-tauri` project and add the following below the `main()` function:

src-tauri/src/main.rs
```rust
#[tauri::command]
fn hello(name: &str) -> Result<String, String> {
  // This is a very simplistic example but it shows how to return a Result
  // and use it in the front-end.
  if name.contains(' ') {
    Err("Name should not contain spaces".to_string())
  } else {
    Ok(format!("Hello, {}", name))
  }
}
```

You will also need to modify the `main()` function slightly to tell `tauri` about your new command:

src-tauri/src/main.rs
```rust
fn main() {
  tauri::Builder::default()
    .invoke_handler(tauri::generate_handler![hello])
    .run(tauri::generate_context!())
    .expect("error while running tauri application");
}
```

That's all for the back-end.

> For more details on what you can do with `tauri` commands, including async commands, error handling and accessing managed state, I highly recommend reading through all of the sections in the `tauri` docs here: https://tauri.studio/en/docs/guides/command

## Accessing tauri commands from Rust in the front-end

In order to access tauri commands on the front-end in Rust/WASM we need to add some glue code in Javascript.

There is a `tauri` npm package but since we want to keep any Javascript usage to a minimum and prefer to use `cargo` and `trunk` to manage our front-end, we will use a different approach.

Tauri also exports its functions via the `window` object in Javascript.

In our `front-end` project, create a new file called `glue.js` inside the `public/` directory.

frontend/public/glue.js
```javascript
const invoke = window.__TAURI__.invoke

export async function invokeHello(name) {
    return await invoke("hello", {name: name});
}
```

That is all the Javascript we will add. I told you it would be minimal!

Notice that the Javascript function is async. That is because the tauri `invoke()` command returns a Javascript promise. And through WASM, Javascript promises can be converted into Rust futures. Just let that awesomeness sink in for a while :)

How do we call that Javascript function from Rust? We add more glue code, this time in Rust. Add the following in your frontend's `main.rs`:

frontend/src/main.rs
```rust
#[wasm_bindgen(module = "/public/glue.js")]
extern "C" {
    #[wasm_bindgen(js_name = invokeHello, catch)]
    pub async fn hello(name: String) -> Result<JsValue, JsValue>;
}
```

Here we tell wasm_bindgen about our javascript code in `/public/glue.js`. We give it the javascript function name, and the `catch` parameter tells wasm_bindgen that we want to add a `catch()` handler to the javascript promise and turn it into a `Result` in Rust.

This new code won't work yet, because we haven't added `wasm-bindgen` to our front-end dependencies. Let's do that now. We'll also add `wasm-bindgen-futures` which we'll use to call this later, `web-sys` which provides access to the browser `window` object, and `js-sys` which gives us the `JsValue` types we referenced above.

```
cargo add wasm-bindgen
cargo add wasm-bindgen-futures
cargo add web-sys
cargo add js-sys
```

This will add the following dependencies to `Cargo.toml` in the front-end:

```
wasm-bindgen = "0.2.78"
wasm-bindgen-futures = "0.4.28"
web-sys = "0.3.55"
js-sys = "0.3.55"
```

We also need to add some imports to the top of main.rs.

frontend/src/main.rs
```rust
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::spawn_local;
use web_sys::window;
```

So now we've added the glue code in Javascript and the Rust code that calls that method. What we need to do is wire that up to the front-end code and actually use it.

## Calling Tauri commands in Yew code

In version 0.19 Yew added function components, which operate much like React hooks if you're familiar with the React framework in Javascript. If you're not familiar with that, I will attempt to give a brief explanation of the concepts used in this tutorial, but a fuller explanation of hooks is out of scope. The yew docs themselves go into some more detail. 

Replace the function component in your front-end `main.rs` with the following:

frontend/src/main.rs
```rust
#[function_component(App)]
pub fn app() -> Html {
    let welcome = use_state_eq(|| "".to_string());
    let name = use_state_eq(|| "World".to_string());

    // Execute tauri command via effects.
    // The effect will run every time `name` changes.
    {
        let welcome = welcome.clone();
        use_effect_with_deps(
            move |name| {
                update_welcome_message(welcome, name.clone());
                || ()
            },
            (*name).clone(),
        );
    }

    let message = (*welcome).clone();

    html! {
        <div>
            <h2 class={"heading"}>{message}</h2>
        </div>
    }
}

fn update_welcome_message(welcome: UseStateHandle<String>, name: String) {
    spawn_local(async move {
        // This will call our glue code all the way through to the tauri
        // back-end command and return the `Result<String, String>` as
        // `Result<JsValue, JsValue>`.
        match hello(name).await {
            Ok(message) => {
                welcome.set(message.as_string().unwrap());
            }
            Err(e) => {
                let window = window().unwrap();
                window
                    .alert_with_message(&format!("Error: {:?}", e))
                    .unwrap();
            }
        }
    });
}
```

And this should compile and run.

Navigate up one directory (to the `tauri-yew-demo` directory), and run:

```
cargo tauri dev
```

You should see the application window showing `Hello, World!`.

![App screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q9l7wl9f5njmldbt9zml.png)

Ok, that was a lot of code. Let's unpack it.

First we add some state to the component through the `use_state_eq` hook.

```rust
let welcome = use_state_eq(|| "".to_string());
let name = use_state_eq(|| "World".to_string());
```

Component state has an initial value, and a method for updating that value. Normally, updating a state value will cause the component to be re-rendered with the new value.

The `use_state_eq` hook gives us a state handle that will only cause the component to be re-rendered if the value has changed. It uses Rust's `PartialEq` trait for this.

You can dereference a `UseStateHandle` to get the underlying state value.

```rust
    {
        let welcome = welcome.clone();
        use_effect_with_deps(
            move |name| {
                update_welcome_message(welcome, name.clone());
                || ()
            },
            (*name).clone(),
        );
    }
```

We need to clone the `welcome` handle so that we can move it into the closure.

The `use_effect_with_deps()` hook is a variant of the `use_effect` hook where this one only runs the hook when some dependency changes. 

It takes two arguments. The first is the closure we want to run when the dependency value changes, and the second is the dependency value itself.

In this example the dependency is specified as `(*name).clone()`, which means we are dereferencing the handle to get the underlying `String` and then cloning the `String` because we cannot move it.

That value is then passed as the argument to the closure.

The `use_effect*()` hook is generally used to do some longer operation or some async operation where the component will update with the result of that operation. This is why we pass in the `welcome` handle, because this allows the closure to set the value of that handle, which will cause the component to re-render, but only if the value has changed. Calls to tauri commands will generally go inside a `use_effect` or `use_effect_with_deps` hook.

The return value of the closure is always another closure that will run when the component is unloaded from the virtual DOM. You can use this closure to perform any cleanup relating to this effect hook. In this case there is nothing to clean up so we return an empty closure.

Let's look at the `update_welcome_message()` function.

```rust
fn update_welcome_message(welcome: UseStateHandle<String>, name: String) {
    spawn_local(async move {
        match hello(name).await {
            Ok(message) => {
                welcome.set(message.as_string().unwrap())
            }
            Err(e) => {
                ...
            }
        }
    });
}
```

The `spawn_local()` function is from `wasm_bindgen_futures` and it allows us to run async functions in the current thread. In this case we need it in order to call our `hello()` function, because it is async.

Next we call the `hello()` function, which returns `Result<JsValue, JsValue>`. We know the `JsValue` will be a string since the original tauri command we implemented returned `Result<String, String>`, so we can just unwrap() here.

Finally if the result was `Ok(..)` then we set the new value of the `welcome` state variable. Otherwise we display an alert. 

> Alerts are not a great way to handle errors like this, but as a simple example it shows how the `Result` type is passed through to the front-end. In a proper application you would probably want to show a toast popup and optionally log to the browser console (in debug builds).
> See the [`wasm-logger`](https://docs.rs/wasm-logger/0.2.0/wasm_logger/) crate for an easy way to send logs to the browser console using the `log` crate macros.

## Wrapping up

Hopefully this tutorial has given you a taste for how to create a desktop app almost entirely in Rust, using `Tauri` for the back-end and `Yew` for the front-end.

You can find all of the code for this tutorial on my github:
https://github.com/stevepryde/tauri-yew-demo

Feel free to use it and modify it as a base for your own projects.

I highly recommend reading through the documentation for both Tauri and Yew.

Tauri: https://tauri.studio/en/docs/get-started/intro
Yew: https://yew.rs/docs/getting-started/introduction

If there's anything I missed or anything that is still unclear from this tutorial, please let me know in the comments and I'll aim to update it.

Thanks for reading!