Skip to content

Instantly share code, notes, and snippets.

@craftyc0der
Last active July 30, 2023 03:48
Show Gist options
  • Save craftyc0der/1e71964cc3e85d533f0f70985c22a74a to your computer and use it in GitHub Desktop.
Save craftyc0der/1e71964cc3e85d533f0f70985c22a74a to your computer and use it in GitHub Desktop.
Firebase & Yew

Firebase & Yew

Motivation

I often use Firebase to store data for small projects. I've been using wasm-bindgen to build web tools for a while now (both in production and for personal use). Until now, I have not tried Yew to build a web app. If Yew works well with Firebase, I thought I might give it a shot for the next single page app (SPA) I build.

Goals

  • Create basic Yew page
  • Add Firebase JS SDK
  • Write minimal JS code to interact with Firebase and do all rendering in Rust/Yew via callbacks

Steps

Setup Rust environment for wasm32-unknown-unknown target:

rustup target add wasm32-unknown-unknown

Install Trunk (packaging tool)

cargo install --locked trunk

Create a new Yew project

cargo new yew-app
cd yew-app

Update Cargo.toml to use Yew and wasm-bindgen

...
[dependencies]
yew = { version = "0.20.0", features = ["csr"] }
wasm-bindgen = {version = "^0.2"}

Create index.html

You can find my index.html below in this gist. The critical thing to include is the hint for trunk to render the wasm files it creates.

<head>
...
<link data-trunk rel="rust" />
</head>

Website Premise

The sample website we will build here will query a Firebase database for a list of rows that contain an object that contains a haiku poem. We will then render these data in the DOM using Yew. Because the Firebase SDK provided by Google is written in JavaScript, we will need wasm-bindgen to bridge the gap between Rust and JavaScript.

In order to squeeze this into a gist, I've flattened the directory structure. Normally the .rs files would be in a src directory.

Create main.rs

You can find my main.rs below in this gist.

Things of note:

  • Msg enum is used to define messages that can be sent to the HaikuPage struct which is the main component of the app
    • Msg::HaikuClear is used to send a message to the HaikuPage to clear the haiku data
    • Msg::HaikuRecord(Haiku) is used to send a message to the HaikuPage to add a new haiku to the list
  • Haiku struct stores a single haiku row from Firebase
    • Firebase stores three fields for each haiku
      1. int64 id
      2. int64 featured
      3. string text
  • HaikuPage struct is the main component of the app
    • haikus is a vector of Haiku structs

You should read the Yew Documentation on Lifecycles to understand how the create, update, view, and changed functions work.

For this example, the critical pieces are in create and update. We query Firebase in the create function and process the callbacks from Javascript land within the update function.

Write JavaScript code to interact with Firebase

You can find my index.js below in this gist.

const app = initializeApp(firebaseConfig);
const database = getDatabase(app);

// query Firebase for Haiku
// `fnHaikuClear` and `fnHaikuAdd` are closures defined in Rust
// that emit `Msg` messages to the `HaikuPage` `update` function
export function getHaiku(fnHaikuClear, fnHaikuAdd) {
  const haikuRef = query(ref(database, 'haiku/'), orderByChild("featured"));
  onValue(haikuRef, (snapshot) => {
    // when data is returned, clear the current haiku list by
    // calling a closure passed in from the Rust
    fnHaikuClear();
    // then loop over the returned data
    snapshot.forEach((childSnapshot) => {
        var childKey = childSnapshot.key;
        var childData = childSnapshot.val();
        // and send each haiku to the Rust closure passed in above
        fnHaikuAdd(childKey, childData.featured, childData.text);
    });
  });
  console.log("Get Haiku");
}

Setup wasm-bindgen for the JavaScript code

You can find my caller.rs below in this gist.

use wasm_bindgen::prelude::*;

// wasm-bindgen will automatically take care of including this script
#[wasm_bindgen(module = "/index.js")]
extern "C" {
    #[wasm_bindgen(js_name = "getHaiku")]
    pub fn get_haiku(callbackClear: JsValue, callbackAdd: JsValue);
}

This basically tells wasm-bindgen to include the index.js file in the final wasm file it creates and to expose the getHaiku function to Rust as get_haiku(callbackClear: JsValue, callbackAdd: JsValue).

Bring it all together in main.rs

You can find my main.rs below in this gist.

The create function is responsible for querying Firebase and setting up the callbacks to process the data returned from Firebase. Note we do not manage the closures in the Rust compiler. We are cheating a little here and just using the .into_js_value() method to convert the closures to a JsValue which calls drop() on the closure so that we can call it multiple times in JavaScript land. This is not ideal, but it works for this example.

    fn create(ctx: &Context<Self>) -> Self {
        // define callback for clearing the haiku list
        let callback_clear = ctx.link().callback(|_| Msg::HaikuClear);
        // define closure for clearing the haiku list
        let closure_clear = Closure::wrap(Box::new(move || {
            // call the clear callback
            // we want to clear the data array before we update all the data
            callback_clear.emit(());
        }) as Box<dyn FnMut()>);
        // define callback for adding a haiku to the list
        let callback_add = ctx.link().callback(Msg::HaikuRecord);
        // define closure for adding a haiku to the list
        let closure_add = Closure::wrap(Box::new(move |key: JsValue, featured: JsValue, text: JsValue| {
            let key_str = key.as_string().unwrap();
            let featured_int = featured.as_f64().unwrap() as i64;
            let key_int = key_str.parse::<i64>().unwrap();
            let haiku = Haiku {
                id: key_int,
                featured: featured_int,
                content: text.as_string().unwrap(),
            };
            callback_add.emit(haiku);
        }) as Box<dyn FnMut(JsValue, JsValue, JsValue)>);
        // call the get_haiku function in index.js
        // we use `into_js_value()` to convert the closures into javacript managed closures
        // they are no longer managed by rust
        // an unfortunate side effect of needing to use these more than once in JS land
        caller::get_haiku(closure_clear.into_js_value(), closure_add.into_js_value());
        Self {
            haikus: Vec::new(),
        }
    }

The update function is responsible for processing the messages sent from the JavaScript land via the closures defined above.

    fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
        match msg {
            Msg::HaikuClear => {
                // clear the haiku list
                self.haikus.clear();
                true
            }
            Msg::HaikuRecord(haiku) => {
                // add a haiku to the list
                self.haikus.push(haiku);
                true
            }
        }
    }

The rest of the code is just for rendering the data returned from Firebase.

Haiku App

Run this gist

trunk serve

Future Work

It would be a much better experience if we used wasm-bindgen to control the Firebase SDK directly rather than using the JavaScript SDK. This would allow us to use Rust types and memory management rather than having to convert everything to JsValue and back again. We could take that a step further and build idiomatic Rust wrappers around the Firebase SDK to make it more intuitive to use.

Conclusion

Yew is an interesting framework. I am more accustomed to using Lit and Angular for web development. I would be happier if Yew had first class debugging support. I think the solution is to build a lib around all the business logic so that you can debug that independently from the wasm code. But, that is a topic for another day.

use wasm_bindgen::prelude::*;
// wasm-bindgen will automatically take care of including this script
#[wasm_bindgen(module = "/index.js")]
extern "C" {
#[wasm_bindgen(js_name = "getHaiku")]
pub fn get_haiku(callbackClear: JsValue, callbackAdd: JsValue);
}
[package]
name = "gist-yew-firebase"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[[bin]]
name = "gist-yew-firebase"
path = "main.rs"
[dependencies]
yew = { version = "0.20.0", features = ["csr"] }
wasm-bindgen = {version = "^0.2"}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Firebase & Yew</title>
<base data-trunk-public-url />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/[email protected]/css/bulma.min.css"
/>
<link data-trunk rel="sass" href="index.scss" />
<link data-trunk rel="rust" />
</head>
<body>
</body>
</html>
// import Firebase 9.14.0 SDK
import { initializeApp } from "https://www.gstatic.com/firebasejs/9.14.0/firebase-app.js";
import { getDatabase, query, onValue, orderByChild, ref } from "https://www.gstatic.com/firebasejs/9.14.0/firebase-database.js";
// Initialize Firebase
const firebaseConfig = {
apiKey: "AIzaSyDM0TICdsfwD0Mj3j__oULR5EQg__hFTl0",
authDomain: "craftycoder-e95a2.firebaseapp.com",
databaseURL: "https://craftycoder-e95a2.firebaseio.com",
projectId: "craftycoder-e95a2",
storageBucket: "craftycoder-e95a2.appspot.com",
messagingSenderId: "605879826235",
appId: "1:605879826235:web:59773e7c77b01c6b6b691e",
measurementId: "G-JS38X3LLBK"
};
const app = initializeApp(firebaseConfig);
const database = getDatabase(app);
// query Firebase for Haiku
// `fnHaikuClear` and `fnHaikuAdd` are closures defined in Rust
// that emit `Msg` messages to the `HaikuPage` `update` function
export function getHaiku(fnHaikuClear, fnHaikuAdd) {
const haikuRef = query(ref(database, 'haiku/'), orderByChild("featured"));
onValue(haikuRef, (snapshot) => {
// when data is returned, clear the current haiku list by
// calling a closure passed in from the Rust
fnHaikuClear();
// then loop over the returned data
snapshot.forEach((childSnapshot) => {
var childKey = childSnapshot.key;
var childData = childSnapshot.val();
// and send each haiku to the Rust closure passed in above
fnHaikuAdd(childKey, childData.featured, childData.text);
});
});
console.log("Get Haiku");
}
.hero {
&.has-background {
position: relative;
overflow: hidden;
}
&-background {
position: absolute;
object-fit: cover;
object-position: bottom;
width: 100%;
height: 100%;
&.is-transparent {
opacity: 0.3;
}
}
}
use yew::prelude::*;
use yew::{Component, Context, Html};
use wasm_bindgen::{JsValue, closure::Closure};
mod caller;
pub enum Msg {
HaikuClear,
HaikuRecord(Haiku),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Haiku {
pub id: i64,
pub featured: i64,
pub content: String,
}
pub struct HaikuPage {
haikus: Vec<Haiku>,
}
impl Component for HaikuPage {
type Message = Msg;
type Properties = ();
fn create(ctx: &Context<Self>) -> Self {
// define callback for clearing the haiku list
let callback_clear = ctx.link().callback(|_| Msg::HaikuClear);
// define closure for clearing the haiku list
let closure_clear = Closure::wrap(Box::new(move || {
// call the clear callback
// we want to clear the data array before we update all the data
callback_clear.emit(());
}) as Box<dyn FnMut()>);
// define callback for adding a haiku to the list
let callback_add = ctx.link().callback(Msg::HaikuRecord);
// define closure for adding a haiku to the list
let closure_add = Closure::wrap(Box::new(move |key: JsValue, featured: JsValue, text: JsValue| {
let key_str = key.as_string().unwrap();
let featured_int = featured.as_f64().unwrap() as i64;
let key_int = key_str.parse::<i64>().unwrap();
let haiku = Haiku {
id: key_int,
featured: featured_int,
content: text.as_string().unwrap(),
};
callback_add.emit(haiku);
}) as Box<dyn FnMut(JsValue, JsValue, JsValue)>);
// call the get_haiku function in index.js
// we use `into_js_value()` to convert the closures into javacript managed closures
// they are no longer managed by rust
// an unfortunate side effect of needing to use these more than once in JS land
caller::get_haiku(closure_clear.into_js_value(), closure_add.into_js_value());
Self {
haikus: Vec::new(),
}
}
fn changed(&mut self, _ctx: &Context<Self>, _old_props: &Self::Properties) -> bool {
true
}
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::HaikuClear => {
// clear the haiku list
// log::info!("clear");
self.haikus.clear();
true
}
Msg::HaikuRecord(haiku) => {
// add a haiku to the list
// log::info!("value: {:?}", value);
self.haikus.push(haiku);
true
}
}
}
fn view(&self, _ctx: &Context<Self>) -> Html {
html! {
<>
<section class="hero is-small is-light has-background">
<div class="hero-body">
<div class="container">
<h1 class="title">
{ "Haiku" }
</h1>
</div>
</div>
</section>
<div class="section container">
{ self.view_content() }
</div>
</>
}
}
}
impl HaikuPage {
fn view_content(&self) -> Html {
html! {
<div class="columns is-multiline is-mobile">
// loop through the haiku list
{ for self.haikus.iter().map(|haiku| {
//split haiku into array on <br>
let haiku_array: Vec<&str> = haiku.content.split("<br>").collect();
html! {
<div class="column is-half">
<div class="card">
<div class="card-content">
<div class="content">
<h5 style="text-align: center;">
{
for haiku_array.iter().map(|line| {
html! {
<div>
{ line }
</div>
}
})
}
</h5>
</div>
</div>
</div>
</div>
}})
}
</div>
}
}
}
fn main() {
yew::Renderer::<HaikuPage>::new().render();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment