Tauriで楽天レシピ取得アプリ作ってみた

## はじめに
今回はTauriを使ってレシピ取得アプリを作成しました。
著者はRustもReact(TypeScript)も初学者のため学習のために作成しました。

完成形のGitHubリポジトリ
https://github.com/taiyou1116/tauri-recipe-search

### 作るモチベ
RustとTypeScriptを一度に学べる!!
Rustの非同期処理について学べる!!

### 使う技術
- Tauri
- Rust
- React (TypeScript)

## 対象者
この記事は下記のような人を対象にしています。

- Tauriでデスクトップアプリを作成してみたい方
- Rust, TypeScript初学者
- 楽天APIを使ってみたい方

## 完成形
![recipe_img_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2965124/89c84340-eb50-8eb0-a8d4-10726a3b6ccd.png)
![recipe_img_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2965124/b89923fd-4835-0733-0783-4dc59d5b45a4.png)

### 追記(2023/10/11)
下記コードでevalメソッドを使用していますが、直接jsコードを実行してしまうので、代わりにemitメソッドを使用しましょう。emitは引数の関数をイベントに登録できます。

## 以前のコード
```rust:commands.rs
// メイン関数で受信と処理
    while let Some(recipes) = rx.recv().await {
        // 重複を見てあげる
        let mut new_recipes = Vec::new();
        for recipe in recipes {
            if seen.insert(recipe.recipe_id.clone()) {
                new_recipes.push(recipe);
            }
        }

        // ここで受信したレシピを処理
        let js_command = format!(
            "window.receiveRecipes({})",
            serde_json::to_string(&new_recipes).unwrap()
        );
        window.eval(&js_command).unwrap();
    }
```
## 次のように変更
```rust:commands.rs
// メイン関数で受信と処理
    while let Some(recipes) = rx.recv().await {
        // 重複を見てあげる
        let mut new_recipes = Vec::new();
        for recipe in recipes {
            if seen.insert(recipe.recipe_id.clone()) {
                new_recipes.push(recipe);
            }
        }
        window.emit("receive_recipes", &new_recipes).unwrap();
    }
```
その他詳細は私のGitHubや、下のコメントしてくれた方を参考にしてください。↓

### コード解説

### 問題点
get_category_dataでレシピデータを取得しますが、楽天APIは人気のレシピデータを4つずつしか取得できません。また、APIへのリクエストは1秒ほど間隔を空けないとエラーが発生してしまいます。(負荷を抑えるため)

### tokio::spawnで解決
tokio::spawnを使用して新しくタスクを作成します。その中でAPIへのリクエストをしレシピを取得します。
取得するたびにwhile let Some(recipes) = rx.recv().awaitの部分で受け取ります。
そして、window.eval(&js_command).unwrap();でフロントのwindow.receiveRecipesを実行します。

```rust:commands.rs
#[tauri::command(async)]
pub async fn get_category_data(category_name: String, window: Window) -> Result<(), String> {
    let (tx, mut rx) = mpsc::channel(32); // 32はバッファサイズ

    tokio::spawn(async move {
        let config = config::Config::new();
        let response: ApiResponseOfCategory = reqwest::get(&config.category_url)
            .await
            .unwrap()
            .json()
            .await
            .unwrap();

        let categories = extract_categories_from_response(&response, &category_name);

        for category in categories {
            let recipe_url = format!("{}{}", config.recipe_url, category.category_id);
            let response: ApiResponseOfRecipe = reqwest::get(&recipe_url)
                .await
                .unwrap()
                .json()
                .await
                .unwrap();

            // チャネルを通じてレシピを送信
            if let Err(_) = tx.send(response.result).await {
                return;
            }

            // 1秒待機
            sleep(Duration::from_secs(1)).await;
        }
    });

    let mut seen = HashSet::new();

    // メイン関数で受信と処理
    while let Some(recipes) = rx.recv().await {
        // 重複を見てあげる
        let mut new_recipes = Vec::new();
        for recipe in recipes {
            if seen.insert(recipe.recipe_id.clone()) {
                new_recipes.push(recipe);
            }
        }

        // ここで受信したレシピを処理
        let js_command = format!(
            "window.receiveRecipes({})",
            serde_json::to_string(&new_recipes).unwrap()
        );
        window.eval(&js_command).unwrap();
    }

    Ok(())
}
```

### window.receiveRecipes
ここではrecipeListにRust側で取得したレシピを格納しています。


```App.tsx
useEffect(() => {
    window.receiveRecipes = function(recipes) {
      useStore.setState((state) => ({
        recipeList: [...state.recipeList, ...recipes],
      }));
      
      onGetDataMatchingMaterial(materialName);
    };

    // クリーンアップ関数：コンポーネントのアンマウント時に実行
    return () => {
      window.receiveRecipes = null;
    };
  }, [materialName]);
```

### window.recipeRecipes
window.recipeRecipesを使うためには、WindowというグローバルインターフェースにreceiveRecipesを追加する必要があります。
```global.d.ts
import { Recipe } from "./Recipe";

declare global {
  interface Window {
    receiveRecipes: ((recipes: Recipe[]) => void) | null;
  }
}
```

## おわりに
Tauriで楽天レシピを取得するアプリについて、レシピ取得部分のみ解説してみました。
コード全体についてはGitHubリポジトリをご覧ください。

## 参考記事
[楽天APIを使って人気レシピを取得してみた](https://qiita.com/anzuuuuu/items/be4e4845c21ac3334695)
[Pythonで楽天レシピAPIからレシピを取得する](https://qiita.com/konitech913/items/7ffa7907a6c03c8909fc)