Calling Backend (Tauri) from the Frontend (YEW)

Hi,

I try to call Backend from my Frontend, specifically a config. My code is compiling but I cannot reach the config.toml
Backend: Tauri 2.0
Frontend: YEW 0.21

Config is located at src-tauri/src/config.toml

I would be very happy if I can get help to spot the issue. Hopefully that is possible from the code I have provided. Otherwise make me aware of common mistakes I should pay attention to?

//snippet from Frontend YEW lib.rs
#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"], js_name = invoke)]
    async fn invoke(cmd: &str, args: JsValue) -> JsValue;
}

#[function_component(DanceOmatic)]
pub fn dance_o_matic() -> Html {
    let config = use_state(|| None);
    let config_clone = config.clone();

    

    use_effect(move || {
        wasm_bindgen_futures::spawn_local(async move {
            // Create an empty JSON object for the arguments
            let args = serde_json::json!({});
            // Convert to JsValue
            let js_args = to_value(&args).unwrap();
            let result = invoke("get_config", js_args).await;
            let config_result: Result<Config, String> = serde_wasm_bindgen::from_value(result).unwrap();
            match config_result {
                Ok(loaded_config) => {
                    log::info!("Config loaded successfully");
                    config_clone.set(Some(Rc::new(loaded_config)));
                },
                Err(err) => log::error!("Failed to parse result: {:?}", err),
            }
        });
        
        || () 
    });


    html! {
        <div>
            <MusicContextProvider>
            <SoundEffectsProvider>
            <BrowserRouter>
                { if let Some(config) = &*config {
                    html! { <Switch<Route> render={switch(config.clone())} /> }
                } else {
                    html! { <p>{ "Loading config..." }</p> } //<-- I only get this output on screen
                }}
            </BrowserRouter>
            </SoundEffectsProvider>
            </MusicContextProvider>
        </div>
    }
}

//snippet end

Backend looks like this:

//src-tauri/src/lib.rs
#[cfg_attr(mobile, tauri::mobile_entry_point)]
use toml;
use serde::Deserialize;
use serde::Serialize;
use std::fmt;

const CONFIG_PATH: &str = "src-tauri/src/config.toml";

pub struct ConfigError(String);

//impl of config error

#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
struct Config {
    pub dancers: Dancers,
    pub demo_videos: DemoVideos,
    pub choreo_videos: ChoreoVideos,
}

#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
struct Dancers {
    pub list: Vec<ConfigDancer>,
}

#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
struct ConfigDancer {
//fields of struct

#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
struct DemoVideos {
    pub list: Vec<DemoVideoConfig>,
}

#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
struct DemoVideoConfig {
//fields of struct
}

#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
struct ChoreoVideos {
    pub list: Vec<ChoreoVideoConfig>,
}

#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
struct ChoreoVideoConfig {
//fields of struct
}

impl Config {
    pub fn from_file(path: &str) -> Result<Self, ConfigError> {
        let config_content = std::fs::read_to_string(path).map_err(|err| err.to_string())?;
        let config: Config = toml::from_str(&config_content).map_err(|err| err.to_string())?;
        Ok(config)
    }
}


#[tauri::command]
fn get_config() -> Result<Config, String> {
    Config::from_file(CONFIG_PATH).map_err(|err| err.to_string())
}

pub fn run() {
  tauri::Builder::default()
    .invoke_handler(tauri::generate_handler![get_config])
    .run(tauri::generate_context!())
    .expect("error while running tauri application");
}

Inspecting element when running cargo tauri dev in terminal gives me:

panicked at src/lib.rs:38:1:
unexpected exception: JsValue("ConfigError: No such file or directory (os error 2)")

Stack:

@http://localhost:8080/yew-app-273cc721a4e71c34.js:614:30
<?>.wasm-function[21578]@[wasm code]* *<?>.wasm-function[13475]@[wasm code]
<?>.wasm-function[3161]@[wasm code]
Output is much longer.

I also have a config.rs in frontend:

//src/components/data/config.rs
// snippet of code

// defining struct Dancers, DemoVideos, ChoreoVideos (again)

#[derive(Debug, Deserialize, Clone, PartialEq)]
pub struct Config {
    pub dancers: Dancers,
    pub demo_videos: DemoVideos,
    pub choreo_videos: ChoreoVideos,
}



impl Config {
    pub fn load_dancers(&self) -> std::collections::HashMap<usize, Vec<Dancer>> {
        let mut choreography_map = std::collections::HashMap::new();

        for dancer_config in &self.dancers.list {
            let dancer = Dancer {
                image: dancer_config.image.clone(),
                name: dancer_config.name.clone(),
                strength: dancer_config.strength,
                flexibility: dancer_config.flexibility,
            };

            for &choreo_number in &dancer_config.in_chroeography_nr {
                choreography_map
                    .entry(choreo_number)
                    .or_insert_with(Vec::new)
                    .push(dancer.clone());
            }
        }

        choreography_map
    }

    pub fn get_demo_videos(&self) -> Vec<VideoType> {
        self.demo_videos.list.iter().map(|video_config| {
            VideoType::Demo(DemoVideo {
                video: Video {
                    id: video_config.id,
                    url: video_config.url.clone(),
                    loop_video: video_config.loop_video,
                },
                title: video_config.title.clone(),
                duration: video_config.duration.clone(),
            })
        }).collect()
    }

    pub fn load_choreo_videos(&self) -> Vec<VideoType> {
        self.choreo_videos.list.iter().map(|video_config| {
            VideoType::Regular(Video {
                id: video_config.id,
                url: video_config.url.clone(),
                loop_video: video_config.loop_video,
            })
        }).collect()
    }


}

I'm aware there has been a little mistake in the path, solving the initiated problem:

//src-tauri/src/lib.rs
const CONFIG_PATH: &str = "src-tauri/src/config.toml";

//changed to

const CONFIG_PATH: &str = "src/config.toml";

Inspecting element when running cargo tauri dev in terminal now gives me:

panicked at library/std/src/sys/pal/wasm/../unsupported/time.rs:31:9:
time not implemented on this platform

I have tried various attempts to adjust time in my cargo.toml files, still having the same output upon inspecting element. I assume the issue lies elsewhere... :thinking:

:waving_hand:

I assume by this you mean you enabled the wasm-bindgen feature?

Hey. Thanks for the help.
Yes. In both Cargo.toml i do have

wasm-bindgen = "0.2.100"
wasm-bindgen-futures = "0.4.40"

It is integrated like so

//lib.rs
//code snippet
#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"], js_name = invoke)]
    async fn invoke(cmd: &str, args: JsValue) -> JsValue;
}
//code snippet end.

No. What I mean is the time crate has a wasm-bindgen feature flag which needs to be enabled in order for it to be compiled to wasm.

Do you have a public repo with your code?

Yes. Right here: https://github.com/martinschultzkristensen/yew-web-app/tree/feature/config_v3

Thanks for checking it out! :collision: :face_with_monocle:

Add features = ["wasm-bindgen"] to time = ">=0.3.35". I suspect you will get more errors, but start from there.

Hey.
Just want to says I solved the issue.
I think it was "as simple as" going into taur.conf.json and set

 "app": {
    "withGlobalTauri": true,

Now I have:

//src-taur/src/lib.rs
const CONFIG_PATH: &str = "src/config.toml";

#[tauri::command]
fn get_config() -> Result<Config, String> {
    Config::from_file(CONFIG_PATH).map_err(|err| err.to_string())
}

And in frontend:

//lib.rs

#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"], js_name = invoke)]
    async fn invoke(cmd: &str, args: JsValue) -> JsValue;
}



fn is_tauri() -> bool {
    web_sys::window()
        .unwrap()
        .get("__TAURI__")
        .is_some()
}

#[function_component(DanceOmatic)]
pub fn dance_o_matic() -> Html {
    let config = use_state(|| None::<Rc<Config>>);
        let config_fetched = use_state(|| false);
        let config_clone = config.clone();
        let config_fetched_clone = config_fetched.clone();


        use_effect(move || {
            // Only fetch if we haven't yet
            if is_tauri() && !*config_fetched_clone {
                spawn_local(async move {
                    let args = serde_json::json!({});
                    let js_args = to_value(&args).unwrap();
                    let result = invoke("get_config", js_args).await;
                    log::info!("Raw result from invoke: {:?}", result);
    
                    match serde_wasm_bindgen::from_value::<Config>(result) {
                        Ok(loaded_config) => {
                            let new_config = Rc::new(loaded_config);
                            if Some(new_config.clone()) != *config_clone {
                                log::info!("🔄 Config changed, updating state.");
                                config_clone.set(Some(new_config));
                            } else {
                                log::info!("✅ Config unchanged, skipping update.");
                            }
                            config_fetched_clone.set(true);
                        }
                        Err(err) => log::error!("Failed to deserialize config: {:?}", err),
                    }
                    
                });
            }
    
            || ()
        });

This is just snippets to give an idea. But it's working.
Thanks for assisting me.