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 + Synctype, 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.