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.
- 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
rustup target add wasm32-unknown-unknown
cargo install --locked trunk
cargo new yew-app
cd yew-app
...
[dependencies]
yew = { version = "0.20.0", features = ["csr"] }
wasm-bindgen = {version = "^0.2"}
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>
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.
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 theHaikuPage
struct which is the main component of the appMsg::HaikuClear
is used to send a message to theHaikuPage
to clear the haiku dataMsg::HaikuRecord(Haiku)
is used to send a message to theHaikuPage
to add a new haiku to the list
Haiku
struct stores a single haiku row from Firebase- Firebase stores three fields for each haiku
int64
idint64
featuredstring
text
- Firebase stores three fields for each haiku
HaikuPage
struct is the main component of the apphaikus
is a vector ofHaiku
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.
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");
}
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)
.
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.
trunk serve
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.
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.