It looks like:
I have years of experience using React.js and ClojureScript. And Respo was the virtual DOM library I created in ClojureScript http://cljs.respo-mvc.org/ . Now I try to make Rust version of Respo and finally finished a prototype of Respo.rs ,
- Repository GitHub - Respo/respo.rs: tiny toy virtual DOM based framework for Rust
- TodoMVC GitHub - Respo/todomvc-respo-rs: TodoMVC in Respo.rs
- Doc of crate respo - Rust
A quick overview of features:
- virtual DOM(however simplified in list diffing)
- components declaration with functions
- globals states with Store and Actions dispatching
- cursor-based states tree(inherited from Respo.cljs , might be awkward without hot reloading)
- CSS in Rust macros
- basic component effects of
Mounted, WillUpdate, Updated, WillUnmount
- macros to memoize component functions(quite clumsy code)
features that not implemented in Respo.rs :
- macros for JSX syntax. Respo expects smooth type checking and autocompletion, which might be broken in JSX macros.(it's also hard to implement)
- updating component states in lifecycles. Respo enforces “unidirectional data flow”, you can only update DOM inside lifecycle closures
- React-like hooks. Respo uses functions without tricky side-effects, which restricted the usages of hooks.
- Hot reloading. It does not work in WebAssembly so far
I hold an opinion that hot reloading is an essential feature for virtual DOM based web frameworks. Unfortunately Rust/WASM does not support it. So Respo.rs is far from satisfaction comparing to it's previous versions in compile-to-js languages.
The shinny part of Respo.rs is the power of Rust type systems that abstracts store/states and dispatchers. That's why I still feel glad to finish this library.
A very simple Respo.rs component looks like:
pub fn comp_demo() -> Result<RespoNode<ActionOp>, String> {
Ok(div().class("parent").children([
span().class(style_of_text()).inner_text("Demo component").to_owned()
]).to_owned())
}
to define some static styles(attached to <style />
) with a macro:
static_styles!(
style_of_text,
(
"$0".to_owned(),
RespoStyle::default()
.margin(4.)
.color(CssColor::Hsl(0, 90, 90)),
),
("$0:hover".to_owned(), RespoStyle::default().color(CssColor::Hsl(0, 90, 80))),
);
or use inline styles:
span().inner_text("demo")
.style(RespoStyle::default().color(Css::Color::Blue))
.to_owned()
or use conditional class names:
span()
.toggle_class("selected", status == Status::On)
.maybe_class(if tab == Tab::A { Some("highlighted") } else { None })
.to_owned()
The syntax is quite different from Yew, which uses JSX macros. Respo.rs relies heavily on methods to provide sugars and autocompletion everywhere. I'm not exactly certain if that's best choice for Respo or whether I need to change it in future versions.
Defining global store and actions is easy, just some data abstractions plus traits:
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Store {
pub todos: Vec<Task>,
pub states: StatesTree,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ActionOp {
StatesChange(Vec<String>, MaybeState),
AddTodo(String, String),
ToggleAll,
Toggle(String),
// more...
}
impl RespoAction for ActionOp {
fn wrap_states_action(cursor: &[String], a: MaybeState) -> Self {
Self::StatesChange(cursor.to_vec(), a)
}
}
impl RespoStore for Store {
type Action = ActionOp;
fn get_states(&self) -> StatesTree {
self.states.to_owned()
}
fn update(&mut self, op: Self::Action) -> Result<(), String> {
match op {
ActionOp::StatesChange(path, new_state) => {
self.states.set_in_mut(&path, new_state);
}
ActionOp::AddTodo(id, content) => self.todos.push(Task {
id,
title: content,
completed: false,
}),
ActionOp::ToggleAll => {
let completed = self.todos.iter().all(|t| t.completed);
for t in &mut self.todos {
t.completed = !completed;
}
}
// more...
}
Ok(())
}
}
However, defining component states looks a bit boring in Respo.rs since it does not have "real local mutable states". In Respo it's using a cursor-based design that maintains states in a global tree, and each components picks its own branch with states.pick("name")
level by level and updates it with a cursor.
A rough example split from TodoMVC looks like:
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
struct TaskState {
edit_text: String,
}
fn comp_demo(states: &StatesTree) -> Result<_, String> -> {
// load from states tree
let cursor = states.path();
let state: TaskState = states.data.cast_or_default()?;
// partial code from demo, need to share to multiple closures
let cursor2 = cursor.clone();
let state2 = state.clone();
let handle_blur = move |_e, dispatch: DispatchFn<_>| -> Result<(), String> { handle_submit2(dispatch) };
let handle_keydown = move |e, dispatch: DispatchFn<_>| -> Result<(), String> {
if let RespoEvent::Keyboard { key_code, .. } = e {
if key_code == 13 {
handle_submit3(dispatch)?;
} else if key_code == 27 {
on_cancel3(dispatch)?;
}
}
Ok(())
};
let handle_change = move |e, dispatch: DispatchFn<_>| -> Result<(), String> {
if let RespoEvent::Input { value, .. } = e {
dispatch.run_state(&cursor, TaskState { edit_text: value })?;
}
Ok(())
};
Ok(
input()
.class("edit")
.attribute("value", state.edit_text)
.on_input(handle_change)
.on_keydown(handle_keydown)
.on_named_event("blur", handle_blur)
.to_owned()
)
}
and to call it with picked branch of states:
comp_demo(states.pick("demo"))?
I'm afraid the snippets are too fragile spit out from project. Better to browse the demo project if it appeals to you:
and here's how it runs in a browser TodoMVC Respo.rs .
and the doc:
By now it's a toy for experiment. I'm also glad it's providing a different feature set from Yew so that Respo.rs may explore in a different mileage.