Skip to content

Instantly share code, notes, and snippets.

@safx
Created October 26, 2023 12:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save safx/9af2efa78e0a34d3144c268eb5a72dac to your computer and use it in GitHub Desktop.
Save safx/9af2efa78e0a34d3144c268eb5a72dac to your computer and use it in GitHub Desktop.
Fine-grained reactive system: https://www.youtube.com/watch?v=GWB3vTWeLd4
use std::any::Any;
use std::cell::{Cell, RefCell};
use std::collections::{HashMap, HashSet};
use std::marker::PhantomData;
use web_sys::{console, window};
use wasm_bindgen::prelude::{Closure, JsCast};
#[derive(Default)]
struct Runtime {
signal_values: RefCell<Vec<Box<RefCell<dyn Any>>>>,
running_effect: Cell<Option<EffectId>>,
signal_subscribers: RefCell<HashMap<SignalId, HashSet<EffectId>>>,
effects: RefCell<Vec<Box<dyn Fn()>>>,
}
impl Runtime {
fn create_signal<T>(&'static self, value: T) -> Signal<T> where T: Clone + 'static {
let len = self.signal_values.borrow().len();
self.signal_values.borrow_mut().push(Box::new(RefCell::new(value)));
Signal { cx: self, id: SignalId(len), ty: PhantomData }
}
fn create_effect(&'static self, f: impl Fn() + 'static) {
let len = self.effects.borrow().len();
self.effects.borrow_mut().push(Box::new(f));
self.run_effect(EffectId(len))
}
fn run_effect(&self, effect_id: EffectId) {
// push effect onto stack
let prev_running_effect = self.running_effect.take();
self.running_effect.set(Some(effect_id));
// run effect
let effect = &self.effects.borrow()[effect_id.0];
effect();
// pop effect off stack
self.running_effect.set(prev_running_effect);
}
}
#[derive(Copy, Clone, Eq, Hash, PartialEq)]
struct SignalId(usize);
#[derive(Copy, Clone, Eq, Hash, PartialEq)]
struct EffectId(usize);
#[derive(Copy, Clone)]
struct Signal<T> where T: Clone + 'static {
cx: &'static Runtime,
id: SignalId,
ty: PhantomData<T>,
}
impl<T> Signal<T> where T: Clone + 'static {
fn get(&self) -> T {
// get value
let value = &self.cx.signal_values.borrow()[self.id.0];
let value = value.borrow();
let value = value.downcast_ref::<T>().unwrap();
// add subscribers
if let Some(running_effect) = self.cx.running_effect.get() {
let mut subs = self.cx.signal_subscribers.borrow_mut();
let subs = subs.entry(self.id).or_default();
subs.insert(running_effect);
}
// return value
value.clone()
}
fn set(&self, new_value: T) {
// set value
{
let value = &self.cx.signal_values.borrow_mut()[self.id.0];
let mut value = value.borrow_mut();
let value: &mut T = value.downcast_mut::<T>().unwrap();
*value = new_value;
}
// notify subscribers
let subs = {
let subs = self.cx.signal_subscribers.borrow();
subs.get(&self.id).cloned()
};
if let Some(subs) = subs {
for sub in subs {
self.cx.run_effect(sub);
}
}
}
}
fn main() {
console_error_panic_hook::set_once();
let window = window().unwrap();
let document = window.document().unwrap();
let body = document.body().unwrap();
let dec = document.create_element("button").unwrap();
let p = document.create_element("p").unwrap();
let p2 = document.create_element("p").unwrap();
let inc = document.create_element("button").unwrap();
dec.set_text_content(Some("-1"));
p.set_text_content(Some("Some value"));
p2.set_text_content(Some("Some value"));
inc.set_text_content(Some("+1"));
let _ = body.append_child(&dec);
let _ = body.append_child(&p);
let _ = body.append_child(&p2);
let _ = body.append_child(&inc);
// set up reactive system
let cx: &'static Runtime = Box::leak(Box::default());
let count = cx.create_signal(0);
cx.create_effect(move || {
p.set_text_content(Some(&count.get().to_string()));
});
let double_count = move || count.get() * 2;
cx.create_effect(move || {
p2.set_text_content(Some(&double_count().to_string()));
});
let closure = Closure::wrap(Box::new(move |_event: web_sys::MouseEvent| {
console::log_1(&"-1".into());
count.set(count.get() - 1);
}) as Box<dyn FnMut(_)>);
dec.add_event_listener_with_callback("click", closure.as_ref().unchecked_ref()).unwrap();
closure.forget();
let closure = Closure::wrap(Box::new(move |_event: web_sys::MouseEvent| {
console::log_1(&"+1".into());
count.set(count.get() + 1);
}) as Box<dyn FnMut(_)>);
inc.add_event_listener_with_callback("click", closure.as_ref().unchecked_ref()).unwrap();
closure.forget();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment