willhart.io ALL POSTS ALL TAGS

Tauri and Create React App Part 2 - Commands

Posted by Will Hart on 2021-08-27
See also:RANDOMPROJECTS
ᐊ BACK TO ALL POSTS

Recap

In part 1 of this tutorial series we set up a Tauri and create-react-app app and added a basic non-functional counter. We worked out how to run the app with one command and use the built-in hot reloading to reload the app.

In this part, we will write a basic rust command and invoke it from the client side to update our counter in the app.

What are commands

Commands are the way that Tauri supports exchanging data and actions between the web and rust parts of the code base. Tauri provides a JavaScript API for "invoking" commands in the rust code.

Commands are just rust functions that are registered when the Tauri app is built.

Writing a command

Lets start by writing a simple rust command. Open up src-tauri/src/main.rs, and above the main() function add the following:

use std::sync::atomic::{AtomicI32, Ordering};
use tauri::State;

#[tauri::command]
fn increment_counter(state: State<AtomicI32>, delta: i32) -> Result<i32, String> {
  println!("Incrementing counter by {}", delta);
  Ok(state.fetch_add(delta, Ordering::SeqCst) + delta)
}

We've imported a few things to store a thread-safe number, and we've created a function that takes a tauri::State which contains an AtomicI32 and a second argument is a delta which we use to increment our counter. The function is a one liner which uses fetch_add to update our atomic integer. We then return the previous value (returned by fetch_add plus the delta).

We then need to update our tauri main() function to insert our state and register the commands. Update the main function to look like this:

  tauri::Builder::default()
    .manage(AtomicI32::from(0))
    .invoke_handler(tauri::generate_handler![increment_counter])
    .run(tauri::generate_context!())
    .expect("error while running tauri application");

The two new lines are the manage and invoke_handler lines. The first line tells Tauri to "manage" our state, in this case our atomic integer. The second line invoke_handler registers our handlers. If we want multiple handlers we can do:

tauri::generate_handler![my_command, another_command, a_third_command]

If you look back at your terminal now you should see some activity as the Tauri app reloads in response to the changes. This is all we need to do to set up the commands in the backend.

Invoking the command on the front end

Now we can return to the App.tsx to use (or invoke) our command. We want to invoke the command when the app first loads to get the current value of the counter. We can do this with a useEffect before the return in App.tsx. It should look something like this:

import { useEffect, useState } from "react";
import { invoke } from '@tauri-apps/api'

const App = () => {
  const [counter, setCounter] = useState(-1)

  useEffect(() => {
    invoke('increment_counter', { delta: 0 }).then((result) => setCounter(result as number))
  }, [setCounter])

  return (
    <div>
      <button>Increment</button> {counter}
    </div>
  )
}

When the app reloads you should see a message in the terminal:

Incrementing counter by 0

The counter next to the button should also say 0, which is the value we set in the server. We can verify that this number does come from the rust code by changing line 18 in main.rs from .manage(AtomicI32::from(0)) to .manage(AtomicI32::from(5)). When the app reloads the counter should start on 5.

We now want the counter to update when we hit the "increment" button. We can create a callback for this:

const increment = useCallback(async () => {
  const result = await invoke('increment_counter', { delta: 1 }) as number
  setCounter(result)
}, [setCounter])

We can then update the button to:

<button onClick={increment}>increment</button>

After reloading click the button. The console should print:

Incrementing counter by 1

and the counter should increment by 1 for each click!

Handling errors

If the rust function returns an Err(some_string) then this is passed through to the invoke function on the web side. As invoke is just a promise, this means we could do something like:

invoke(...).then(result => ...).catch(console.error)

We now have commands invoked from the web side executing rust code and handling the result. The code for this tutorial can be found here on github. Part 1 of the tutorial can be found here and part 3 of the tutorial can be found here.