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. In part 2 we created and invoked a command for incrementing our counter. In part 3 we created and invoked a command for incrementing our counter.
In this part, we will update our command and hooks to support multiple different counters indexed by ID.
Basic concept
If you recall from part 3 of this
series, the useSWR
uses
the first argument as a key
to cache queries. The key can be an array, and the
key is passed to the fetcher
function as arguments. We're going to use the key
to store a counterId
variable that we can use to maintain separate counters.
We'll also need to update our commands in the rust code to support an ID.
Update our hook to support counter Ids
We need to update our useInvoke
hook in useInvoke.tsx
to support this new
requirement. We can start by updating the fetcher to take an id
as an
argument:
export const invokeFetcher = async <TArgs extends Record<string, any>, TResult>(
command: string,
id: number,
args: TArgs
): Promise<TResult> => invoke<TResult>(command, { id, ...args })
The main change here is that we are now taking an id
as a second argument, and
spreading it into the args sent to the invoke command. We then need to update
our useInvoke
hook to:
export const useInvoke = <TResult>(
id: number,
getCommand: string,
setCommand: string
) => {
// run the invoke command to get by ID
const { data, error, mutate } = useSWR<TResult>(
[getCommand, id, null],
invokeFetcher
)
// create an update function
const update = useCallback(
async (newData: TResult) => {
mutate(await invoke(
setCommand,
{ id, ...newData }
), false)
},
[mutate, id, setCommand]
)
// unchanged
}
We now pass the id
to the hook, which is used as part of the key
in
useSWR
. In our update
function we add the id
into the data payload sent to
invoke
. Other than that, not a lot has changed.
Updating our front end
It would be nice at this point to factor out the Counter
into a new component.
This lets us pass the counterId
as a prop. Create a new file, Counter.tsx
and add the following:
import { useInvoke } from "./useInvoke";
const defaultArgs = { delta: 1 }
const Counter = ({ counterId }: { counterId: number }) => {
const { data: counter, update } = useInvoke(
counterId,
'get_counter',
'increment_counter'
)
return (
<div>
<button onClick={() => update(defaultArgs)}>increment</button>
Counter {counterId}: {counter}
</div>
)
}
export default Counter
This is basically copied over from our previous implementation inside App.tsx
.
Speaking of which, we can now use our Counter
component inside App.tsx
:
import Counter from './Counter'
const App = () => {
return (
<div>
<Counter counterId={1} />
<Counter counterId={1} />
<Counter counterId={2} />
</div>
)
}
export default App;
We're using three counters here, but two of them point to counterId == 1
. If
we run the app now it kind of works, the counters with id == 1
increment
together and the counter with id == 2
increments separately. However you can
see that the two counters are linked, i.e. they're modifying the same underlying
counter, but only the counters with the same id
visually update when the
increment action is invoked.
Updating the rust command
To fix this, we need to extend our commands in src-tauri/src/main.rs
. Here is
the code:
use tauri::{async_runtime::RwLock, State};
type InnerState = RwLock<HashMap<i32, i32>>;
#[tauri::command]
async fn increment_counter(
state: State<'_, InnerState>,
id: i32,
delta: i32,
) -> Result<i32, String> {
println!("Incrementing counter {} by {}", id, delta);
let mut hashmap = state.write().await;
let next_value = *hashmap.get(&id).unwrap_or(&0) + delta;
hashmap.insert(id, next_value);
Ok(next_value)
}
#[tauri::command]
async fn get_counter(state: State<'_, InnerState>, id: i32) -> Result<i32, String> {
println!("Getting counter value for counter {}", id);
let hashmap = state.read().await;
Ok(*hashmap.get(&id).unwrap_or(&0))
}
We're doing quite a bit here. First of all we've removed the AtomicI32
and
replaced it with a RwLock<HashMap<i32, i32>>
. The main condition here is that
our State
can be managed across threads. Here we're using a read-write lock to
make sure that there can be multiple reads but only one write at a time. We also
added a bit more logging so we can see which counterId
is being get or set in
the logs.
Note that we've used a
RwLock<HashMap>
here as our state, but in reality could use anySend + Sync
type, i.e. one that supports threading. This might be a database, or a file store or something like that in a more complex app. In addition, the inner state type (currentlyi32
) could be anything that supportsserde::Serialize
.
We also need to update the way our state is created in the main()
function.
Change the line with manage
to:
// tauri::Builder
.manage(RwLock::new(HashMap::<i32, i32>::new()))
At this point we can also remove a bunch of unused imports in the main.rs
file. If we run the app we can see that the counters behave as we'd expect, each
incrementing separately and the counters using the same ID updating at the same
time.
Building the app
Now that we are done developing the app, lets build it and see how large the binary is and how much memory it uses. To build the app,
yarn tauri build
The build can take a while as the CRA is built and the rust parts are compiled
in release mode. Once it is built we can look in src-tauri/target/release
. In
the bundle
folder there is an msi
installer we can use, but there should be
a counter-app.exe
directly in the release
folder. Mine is about 7MB.
If I run the application I can check the memory footprint. (After first clicking the increment buttons a bunch of times to make sure everything is working!). Its a fairly slim application, but with basically no CPU and about 50MB of RAM its perfectly acceptible out of the box.
That's it, our counter tutorial app is complete! In this part we extended our command here to support counters with different IDs. The code for this tutorial can be found here on github. Part 3 of the tutorial can be found here.