State Management
How to use a Tauri “State” and what it is exactly isn’t covered very well in the current documentation, even though it’s one of the most powerful tools you have in your Tauri arsenal.
A State is any data structure you create in Rust with the goal of storing data for the duration of your applications runtime. Using a State you can get very easy access to the value both in commands as well as anywhere you have access to an AppHandle.
Before we get into the more nitty gritty details on how Tauri handles your State and how to use them, lets get a bit better understanding first for related topics that will be important for later on when we decide to use State’s in practice.
The name “serde” comes from the words “serialize” and “deserialize”, and it’s what the library helps you implement for your data structures.
Serialization is when you take a value from its original form and turn it into something else, usually a string. In Tauri’s case this is what we use for turning your Rust value into a Javascript value. If you need to pass data from the backend to the frontend it has to be serializeable.
Deserialization is when you take a serialized value, usually a string, and turn it back into its original format, which in Rusts case is turning it back into a struct. In Tauri’s case this is how we turn inputs to commands from Javascript back into their Rust format. If you need to pass data from the frontend to the backend it has to be deserializeable.
When you create a State you can choose to derive from Serialize, Deserialize or both, depending on which directions your struct needs to support.
Something I use a lot for my structs is the macro #[serde(skip_serializing)]
. Lets say I have a struct called AuthState
. In that state I’m keeping an authentication token. I however am building a very security critical application, so I don’t want to pass the token to the frontend. However, keeping that token in a separate struct would be very annoying to do. So what I do is simply instruct serde that certain specific properties in the struct should be excluded from serialization, thus ensuring that I can return the AuthState
safely to the frontend and serde will ensure that any security critical data is never passed back to the frontend.
Here you can see me create an AuthState
where the token is an Option
, since we might not yet have a token to use, and in the event I want to communicate the current state of authentication with the frontend I have skipped serializing that property, meaning if we passed that struct over to the frontend then Javascript would receive an object like so { logged_in: false }
.
Mutex is short for “mutually exclusive”. When you’re passing a value between threads in Rust you will need to ensure that two threads aren’t accessing the same value at the same time. To do this we wrap values in a Mutex so that we can lock that value to a single thread, and then unlock the value once the value is dropped.
If we try to manipulate the same value in multiple threads there will be issues.
If we instead use a Mutex we ensure that only one thread can mutate the state at the same time. Note that this code wouldn’t work either because we need an Arc, but I’m going to talk about that right after this.
Important note on locking: Make sure you don’t end up locking your thread forever.
Contrary to popular belief, it is ok and often preferred to use the ordinary Mutex from the standard library in asynchronous code … The primary use case for the async mutex is to provide shared mutable access to IO resources such as a database connection
In short, the most important differences between std::sync::Mutex
and tauri::async_runtime::Mutex
(which a re-export of tokio::sync::Mutex
) are that if you use the async variant you’ll 1. need to use it within an async function, and 2. you’ll call .await
in order for it to perform the locking instead of ?
. Using the async variant is more costly performance wise and generally speaking isn’t necessary to do what you want.
Additionally, when you do want shared access to an IO resource, it is often better to spawn a task to manage the IO resource, and to use message passing to communicate with that task.
In other words, if you need to use the async variant, you most likely should be switching how you do it instead.
Note that in contrast to std::sync::Mutex, this implementation does not poison the mutex when a thread holding the MutexGuard panics. In such a case, the mutex will be unlocked. If the panic is caught, this might leave the data protected by the mutex in an inconsistent state.
Which basically means that there are situations, albeit few, when the data the Mutex protects might be in a bad state if you use the async variant. When you use the sync variant it’ll raise a PoisonError.
For these reasons I would recommend you always use std::sync::Mutex
instead, it performs better, is safer to use, and if you need the async Mutex you’re probably doing something wrong anyway.
An Arc is a reference counter. Normally in Rust values only exist in one place at one time. What languages will normally do is keep a reference counter on a variable in order to know when all references to that value are gone so it knows that it’s safe to drop. You can essentially see it as that in Rust that reference counter is by default just 1, you have a single reference to a value, once it’s gone it’s gone forever.
Using an Arc you can create a reference counter for your variable, allowing you to keep creating more references to the same value so that the original references value doesn’t get cleaned up.
So if we look at the example from before with Mutex and add an Arc.
The Arc allows us to get a reference to a value across threads, and a Mutex gives us the ability to access that value in a thread safe manner.
However, the State itself gives us something very close to what Arc gives us!
So why did we bother going over what an Arc is? Well, because if you create a State that needs to be accessed and manipulated anywhere that Tauri can’t provide it to you easily, then you would create the Arc yourself.
So in most cases, if you don’t have an advanced use-case, you may very well get away with just creating a Mutex<MyState>>
An example of a case where you would need an Arc is if you are spawning a separate thread for your State and need to handle it in a non-Tauri manner. For example, we’ll be creating a simple SystemState later for checking the current power level of your computer where we want to update the value in the state entirely separate from any commands that Tauri is running.
I won’t go too in-depth on error handling for commands here, that deserves its own article along with tracing. This is the error I’m going to use for now. The most noteworthy things here are that for you to return an error from Rust to Javascript you’ll need to implement serialization for that as well just like with your struct, and I’m implementing PoisonError
for this error, which is an error that may be raised when you lock a Mutex
.
Tauri uses a crate called state
to create a Container
, which is “global type-based state” where “A container can store at most one instance of given type as well as n thread-local instances of a given type.”.
What Tauri does is little more than give you a higher level interface with the functionality provided by that Container, and the way it works in simple terms is that it has a global HashMap where values you push to it get stored, and you can later on access them by simply specifying the type T
that you want to fetch from it.
So now that we have a better understanding of what a State is and some related technologies, lets look at something a bit more pratical!
We’re going to focus on my example of an AuthState since it’s most likely the most commonly needed State for any project.
And that’s it! You have now registered a new Mutex guarded State with Tauri and can access it in your commands.
One seldom used way of managing a state is using an AppHandle. It’s a perfectly valid way to let Tauri handle more States that you create later on.
Remember, any value you tell Tauri to “manage” is essentially just a high level access to a globally stored HashMap. There’s not much magic to it, you’re pushing a value to a HashMap that can be accessed later by specifying the type you want to fetch from it. The main reason it’s a good idea to register all the ones you want to use at the start of your application is because they are all type unique and should all be known and accessible during setup, but that doesn’t mean you can’t push them later on in the run of your app, just that if you don’t .manage()
the State before you try to access it you’ll end up with a panic.
Important note: If you tell Tauri to manage multiple states of the same type T
, only the first one registered will actually be used and retrieved. So for example, it’s perfectly fine to tell it to manage AuthState
and SystemState
, but if I tried to push multiple versions of AuthState
then only the first one would be what actually gets registered. If you feel a need to use multiple of the same State type you should reconsider how you do it, such as creating a Vec within a State. Treat each State as if their type T
is unique (because it is). Most likely if you think you need multiple states, what you actually want is a single state that has a dynamically sized property inside it.
If you are developing a Tauri command you would access the state in the followng manner. Note the `_ lifetime we specify for the Mutex. The reason we have to add that is because we’re using an async fn
, in a regular fn
that part can be skipped. But we are good sensible developers here, so we always use async fn
for commands!
Note that you don’t have to do anything special before this. Tauri takes care of fetching the corresponding State you have specified as long as you’ve managed it beforehand.
The AppHandle is the most useful thing you can imagine for your app and really serves as a hub for your entire application. Here you can see use fetching the state manually from the AppHandle instead of getting it magically as an argument to your command. Note that the AppHandle itself can be fetched easily in commands using the same kind of magic as a state.
If the syntax is new to you let me explain briefly what’s going on: AppHandle has a function called .state()
. That function is capable of fetching a State. But in order to fetch a state, it needs to know which state it should fetch. It knows this by checking for a State that has the type T
, where type T
is Rust lingo for a generic type. So, for .state()
to be capable of fetching something of type T
, that type has to be specified, and the Rust syntax for specifying type T for functions is using ::<T>
, which in this case is ::<Mutex<AuthState>>
. Clear? Ok, let’s see what it looks like in a Command!
There are two options for you to access a State somewhere that isn’t within a command. You can either use an Arc
like discussed previously, or you can pass around an AppHandle
. Which approach you choose is entirely up to you and they both have their own merits.
The most notable difference is that if you pass around an AppHandle
not only can you access States without having to use an Arc, but also you can add new structs to be managed later on down the line. For simplicity sake I tend to pass around AppHandle’s.
Now I can’t stress enough how this isn’t meant to be an example of a “perfect” application. This is a starting point and there’s more that you can do with it. For example, I’m not setting up ensuring that the setup function is only ran once per window, so currently we’ll be spawning a new thread every time we invoke('setup')
. There’s multiple ways you can go about ensuring a function is only ran once per window. We also don’t set up tracing here and have no actual login logic. It’s just a short example to illustrate State management.
Now in your frontend (in this case Nuxt) you might do something along these lines in order to synchronize the SystemState.
- Tauri State: A storage of variables that can be accessed based on type where each type stored is unique on a first-come first-serve basis. We can either access it in commands using magic, or fetch it manually from an AppHandle.
- Generic type
T
: In Rust we don’t have polymorphism. But we do have generic types. Generic types can have any letter(s) you want, it’s just common practise to useT
. - Mutex: Mutually Exclusive variables that allow variables to be accessed in a thread safe manner.
- Async Mutex: If you need to use it you’re doing something wrong.
- Arc: A reference counter mechanism in Rust that allows variables to continue living even when they go out of scope.
- Serialization: Turning a variable from on state into another, usually a struct into a string.
- Deserialization: Turning a variable back from some other state into a Rust variable, usually from a string to a struct.
Ask yourself the following:
- Is the information unique to the entire app and not just the window?
- Does the information have to be accessed from multiple commands or parts of the program?
Ask yourself the following:
- Do you need to access the value somewhere not managed by Tauri.?
Ask yourself the following:
- Do you need the full power of an AppHandle elsewhere?
- Do you need to initialize the State struct before you have access to an AppHandle?