SSR/hydration example!

This commit is contained in:
Greg Johnston 2022-09-03 17:53:18 -04:00
parent 22b4c78804
commit 812c3a2045
20 changed files with 283 additions and 128 deletions

View file

@ -3,6 +3,18 @@ name = "todomvc-ssr-client"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies] [dependencies]
todomvc = { path = "../../todomvc", features = ["hydrate"] } console_log = "0.2"
leptos = { path = "../../../leptos", features = ["hydrate"] } leptos = { path = "../../../leptos", features = ["hydrate"] }
todomvc = { path = "../../todomvc", features = ["hydrate"] }
log = "0.4"
wasm-bindgen = "0.2"
wee_alloc = "0.4"
[profile.release]
codegen-units = 1
lto = true
opt-level = 'z'

View file

@ -0,0 +1,24 @@
use leptos::*;
use todomvc::*;
use wasm_bindgen::prelude::wasm_bindgen;
extern crate wee_alloc;
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
#[wasm_bindgen]
pub fn main() {
console_log::init_with_level(log::Level::Debug);
log::debug!("initialized logging");
leptos::hydrate(body().unwrap(), |cx| {
// initial state — identical to server
let todos = Todos(vec![
Todo::new(cx, 0, "Buy milk".to_string()),
Todo::new(cx, 1, "???".to_string()),
Todo::new(cx, 2, "Profit!".to_string()),
]);
view! { <TodoMVC todos=todos/> }
});
}

View file

@ -1,3 +0,0 @@
fn main() {
println!("Hello, world!");
}

View file

@ -4,6 +4,7 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
actix-files = "0.6"
actix-web = "4" actix-web = "4"
leptos = { path = "../../../leptos", features = ["ssr"] } leptos = { path = "../../../leptos", features = ["ssr"] }
todomvc = { path = "../../todomvc", features = ["ssr"] } todomvc = { path = "../../todomvc", default-features = false, features = ["ssr"] }

View file

@ -1,20 +1,43 @@
use std::{cell::RefCell, rc::Rc};
use actix_files::{Directory, Files, NamedFile};
use actix_web::*; use actix_web::*;
use leptos::*; use leptos::*;
use todomvc::*; use todomvc::*;
// TODO
// - WASM/JS routes
#[get("/")] #[get("/")]
async fn render_todomvc() -> impl Responder { async fn render_todomvc() -> impl Responder {
let mut buffer: String; HttpResponse::Ok().content_type("text/html").body(format!(
_ = create_scope(|cx| { "<!DOCTYPE html>{}",
let todos = Todos(vec![ run_scope({
Todo::new(cx, 0, "Buy milk".to_string()), |cx| {
Todo::new(cx, 1, "???".to_string()), let todos = Todos(vec![
Todo::new(cx, 2, "Profit!".to_string()) Todo::new(cx, 0, "Buy milk".to_string()),
]); Todo::new(cx, 1, "???".to_string()),
Todo::new(cx, 2, "Profit!".to_string()),
]);
buffer = view! { <div id="root"><TodoMVC todos=todos/></div> }; view! {
}); <html lang="en">
buffer <head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="stylesheet" href="/static/todomvc-common/base.css"/>
<link rel="stylesheet" href="/static/todomvc-app-css/index.css"/>
<title>"Leptos • TodoMVC"</title>
</head>
<body>
<TodoMVC todos=todos/>
</body>
<script type="module">r#"import init, {{ main }} from './pkg/todomvc_ssr_client.js'; init().then(main);"#</script>
</html>
}
}
})
))
} }
#[actix_web::main] #[actix_web::main]
@ -22,8 +45,10 @@ async fn main() -> std::io::Result<()> {
HttpServer::new(|| { HttpServer::new(|| {
App::new() App::new()
.service(render_todomvc) .service(render_todomvc)
.service(Files::new("/static", "../../todomvc/node_modules"))
.service(Files::new("/pkg", "../todomvc-ssr-client/pkg"))
}) })
.bind(("127.0.0.1", 8080))? .bind(("127.0.0.1", 8080))?
.run() .run()
.await .await
} }

View file

@ -8,7 +8,7 @@ leptos = { path = "../../leptos" }
wee_alloc = "0.4" wee_alloc = "0.4"
miniserde = "0.1" miniserde = "0.1"
log = "0.4" log = "0.4"
console_log = "0.1" console_log = "0.2"
[dev-dependencies] [dev-dependencies]
wasm-bindgen-test = "0.3.0" wasm-bindgen-test = "0.3.0"
@ -19,6 +19,7 @@ lto = true
opt-level = 'z' opt-level = 'z'
[features] [features]
default = ["csr"]
csr = ["leptos/csr"] csr = ["leptos/csr"]
hydrate = ["leptos/hydrate"] hydrate = ["leptos/hydrate"]
ssr = ["leptos/ssr"] ssr = ["leptos/ssr"]

View file

@ -13,23 +13,21 @@ impl Todos {
pub fn new(cx: Scope) -> Self { pub fn new(cx: Scope) -> Self {
let starting_todos = if is_server!() { let starting_todos = if is_server!() {
Vec::new() Vec::new()
} else if let Ok(Some(storage)) = window().local_storage() {
storage
.get_item(STORAGE_KEY)
.ok()
.flatten()
.and_then(|value| json::from_str::<Vec<TodoSerialized>>(&value).ok())
.map(|values| {
values
.into_iter()
.map(|stored| stored.into_todo(cx))
.collect()
})
.unwrap_or_default()
} else { } else {
if let Ok(Some(storage)) = window().local_storage() { Vec::new()
storage
.get_item(STORAGE_KEY)
.ok()
.flatten()
.and_then(|value| json::from_str::<Vec<TodoSerialized>>(&value).ok())
.map(|values| {
values
.into_iter()
.map(|stored| stored.into_todo(cx))
.collect()
})
.unwrap_or_default()
} else {
Vec::new()
}
}; };
Self(starting_todos) Self(starting_todos)
} }
@ -39,7 +37,7 @@ impl Todos {
} }
pub fn add(&mut self, todo: Todo) { pub fn add(&mut self, todo: Todo) {
self.0.push(todo); self.0.push(todo);
} }
pub fn remove(&mut self, id: usize) { pub fn remove(&mut self, id: usize) {
@ -47,17 +45,11 @@ impl Todos {
} }
pub fn remaining(&self) -> usize { pub fn remaining(&self) -> usize {
self.0 self.0.iter().filter(|todo| !(todo.completed)()).count()
.iter()
.filter(|todo| !(todo.completed)())
.count()
} }
pub fn completed(&self) -> usize { pub fn completed(&self) -> usize {
self.0 self.0.iter().filter(|todo| (todo.completed)()).count()
.iter()
.filter(|todo| (todo.completed)())
.count()
} }
pub fn toggle_all(&self) { pub fn toggle_all(&self) {
@ -67,13 +59,13 @@ impl Todos {
if todo.completed.get() { if todo.completed.get() {
(todo.set_completed)(|completed| *completed = false); (todo.set_completed)(|completed| *completed = false);
} }
}; }
} }
// otherwise, mark them all complete // otherwise, mark them all complete
else { else {
for todo in &self.0 { for todo in &self.0 {
(todo.set_completed)(|completed| *completed = true); (todo.set_completed)(|completed| *completed = true);
}; }
} }
} }
@ -118,6 +110,14 @@ const ENTER_KEY: u32 = 13;
#[component] #[component]
pub fn TodoMVC(cx: Scope, todos: Todos) -> Vec<Element> { pub fn TodoMVC(cx: Scope, todos: Todos) -> Vec<Element> {
let mut next_id = todos
.0
.iter()
.map(|todo| todo.id)
.max()
.map(|last| last + 1)
.unwrap_or(0);
let (todos, set_todos) = create_signal(cx, todos); let (todos, set_todos) = create_signal(cx, todos);
provide_context(cx, set_todos); provide_context(cx, set_todos);
@ -127,7 +127,6 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> Vec<Element> {
set_mode(|mode| *mode = new_mode); set_mode(|mode| *mode = new_mode);
}); });
let mut next_id = 0;
let add_todo = move |ev: web_sys::Event| { let add_todo = move |ev: web_sys::Event| {
let target = event_target::<HtmlInputElement>(&ev); let target = event_target::<HtmlInputElement>(&ev);
ev.stop_propagation(); ev.stop_propagation();
@ -144,20 +143,20 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> Vec<Element> {
}; };
let filtered_todos = create_memo::<Vec<Todo>>(cx, move |_| { let filtered_todos = create_memo::<Vec<Todo>>(cx, move |_| {
todos.with(|todos| { todos.with(|todos| match mode.get() {
match mode.get() { Mode::All => todos.0.to_vec(),
Mode::All => todos.0.to_vec(), Mode::Active => todos
Mode::Active => todos.0 .0
.iter() .iter()
.filter(|todo| !todo.completed.get()) .filter(|todo| !todo.completed.get())
.cloned() .cloned()
.collect(), .collect(),
Mode::Completed => todos.0 Mode::Completed => todos
.iter() .0
.filter(|todo| todo.completed.get()) .iter()
.cloned() .filter(|todo| todo.completed.get())
.collect(), .cloned()
} .collect(),
}) })
}); });
@ -165,7 +164,9 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> Vec<Element> {
// this does reactive reads, so it will automatically serialize on any relevant change // this does reactive reads, so it will automatically serialize on any relevant change
create_effect(cx, move |_| { create_effect(cx, move |_| {
if let Ok(Some(storage)) = window().local_storage() { if let Ok(Some(storage)) = window().local_storage() {
let objs = todos.get().0 let objs = todos
.get()
.0
.iter() .iter()
.map(TodoSerialized::from) .map(TodoSerialized::from)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@ -230,11 +231,9 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> Vec<Element> {
#[component] #[component]
pub fn Todo(cx: Scope, todo: Todo) -> Element { pub fn Todo(cx: Scope, todo: Todo) -> Element {
// creates a scope-bound reference to the Todo
// this allows us to move the reference into closures below without cloning it
let (editing, set_editing) = create_signal(cx, false); let (editing, set_editing) = create_signal(cx, false);
let set_todos = use_context::<WriteSignal<Todos>>(cx).unwrap(); let set_todos = use_context::<WriteSignal<Todos>>(cx).unwrap();
let input: web_sys::Element; let input: Element;
let save = move |value: &str| { let save = move |value: &str| {
let value = value.trim(); let value = value.trim();
@ -288,6 +287,7 @@ pub fn Todo(cx: Scope, todo: Todo) -> Element {
</li> </li>
}; };
#[cfg(not(feature = "ssr"))]
create_effect(cx, move |_| { create_effect(cx, move |_| {
if editing() { if editing() {
_ = input.unchecked_ref::<HtmlInputElement>().focus(); _ = input.unchecked_ref::<HtmlInputElement>().focus();

View file

@ -1,8 +1,12 @@
#[cfg(any(feature = "csr", feature = "hydrate", feature = "ssr"))]
mod for_component; mod for_component;
mod map; mod map;
#[cfg(any(feature = "csr", feature = "hydrate", feature = "ssr"))]
mod suspense; mod suspense;
#[cfg(any(feature = "csr", feature = "hydrate", feature = "ssr"))]
pub use for_component::*; pub use for_component::*;
#[cfg(any(feature = "csr", feature = "hydrate", feature = "ssr"))]
pub use suspense::*; pub use suspense::*;
pub trait Prop { pub trait Prop {

View file

@ -1,6 +1,8 @@
use std::rc::Rc; use std::{cell::RefCell, rc::Rc};
use leptos_reactive::Scope; use leptos_reactive::Scope;
#[cfg(any(feature = "csr", feature = "hydrate"))]
use wasm_bindgen::JsCast; use wasm_bindgen::JsCast;
use crate::Node; use crate::Node;
@ -9,7 +11,7 @@ use crate::Node;
pub enum Child { pub enum Child {
Null, Null,
Text(String), Text(String),
Fn(Rc<dyn Fn() -> Child>), Fn(Rc<RefCell<dyn FnMut() -> Child>>),
Node(Node), Node(Node),
Nodes(Vec<Node>), Nodes(Vec<Node>),
} }
@ -21,9 +23,9 @@ impl Child {
Child::Null => String::new(), Child::Null => String::new(),
Child::Text(text) => text.to_string(), Child::Text(text) => text.to_string(),
Child::Fn(f) => { Child::Fn(f) => {
let mut value = f(); let mut value = (f.borrow_mut())();
while let Child::Fn(f) = value { while let Child::Fn(f) = value {
value = f(); value = (f.borrow_mut())();
} }
value.as_child_string() value.as_child_string()
} }
@ -106,8 +108,7 @@ where
} }
} }
#[cfg(any(feature = "csr", feature = "hydrate"))] impl IntoChild for Vec<Node> {
impl IntoChild for Vec<web_sys::Node> {
fn into_child(self, _cx: Scope) -> Child { fn into_child(self, _cx: Scope) -> Child {
Child::Nodes(self) Child::Nodes(self)
} }
@ -126,11 +127,11 @@ impl IntoChild for Vec<web_sys::Element> {
impl<T, U> IntoChild for T impl<T, U> IntoChild for T
where where
T: Fn() -> U + 'static, T: FnMut() -> U + 'static,
U: IntoChild, U: IntoChild,
{ {
fn into_child(self, cx: Scope) -> Child { fn into_child(mut self, cx: Scope) -> Child {
let modified_fn = Rc::new(move || (self)().into_child(cx)); let modified_fn = Rc::new(RefCell::new(move || (self)().into_child(cx)));
Child::Fn(modified_fn) Child::Fn(modified_fn)
} }
} }

View file

@ -1,10 +1,9 @@
mod attribute; mod attribute;
#[cfg(any(feature = "csr", feature = "hydrate", feature = "ssr"))]
mod child; mod child;
mod class; mod class;
#[cfg(any(feature = "csr", feature = "hydrate"))]
mod event_delegation; mod event_delegation;
pub mod logging; pub mod logging;
#[cfg(any(feature = "csr", feature = "hydrate"))]
mod operations; mod operations;
mod property; mod property;
#[cfg(any(feature = "csr", feature = "hydrate"))] #[cfg(any(feature = "csr", feature = "hydrate"))]
@ -13,10 +12,10 @@ mod reconcile;
mod render; mod render;
pub use attribute::*; pub use attribute::*;
#[cfg(any(feature = "csr", feature = "hydrate", feature = "ssr"))]
pub use child::*; pub use child::*;
pub use class::*; pub use class::*;
pub use logging::*; pub use logging::*;
#[cfg(any(feature = "csr", feature = "hydrate"))]
pub use operations::*; pub use operations::*;
pub use property::*; pub use property::*;
#[cfg(any(feature = "csr", feature = "hydrate"))] #[cfg(any(feature = "csr", feature = "hydrate"))]
@ -82,7 +81,7 @@ where
}); });
} }
#[cfg(any(feature = "csr", feature = "hydrate"))] #[cfg(feature = "hydrate")]
pub fn hydrate<T, F>(parent: web_sys::HtmlElement, f: F) pub fn hydrate<T, F>(parent: web_sys::HtmlElement, f: F)
where where
F: Fn(Scope) -> T + 'static, F: Fn(Scope) -> T + 'static,
@ -99,7 +98,7 @@ where
pub fn create_component<F, T>(cx: Scope, f: F) -> T pub fn create_component<F, T>(cx: Scope, f: F) -> T
where where
F: Fn() -> T, F: FnOnce() -> T,
{ {
// TODO hydration logic here // TODO hydration logic here
cx.untrack(f) cx.untrack(f)
@ -108,7 +107,7 @@ where
#[macro_export] #[macro_export]
macro_rules! is_server { macro_rules! is_server {
() => { () => {
cfg!(feature = "server") cfg!(feature = "ssr")
}; };
} }

View file

@ -239,15 +239,15 @@ pub fn add_event_listener(
.unwrap_throw(); */ .unwrap_throw(); */
} }
pub fn window_event_listener(event_name: &str, cb: impl Fn(web_sys::Event)) { pub fn window_event_listener(event_name: &str, cb: impl Fn(web_sys::Event) + 'static) {
let boxed: Box<dyn FnMut(web_sys::Event)> = Box::new(cb); if !is_server!() {
// Safety: see add_event_listener above let handler = Box::new(cb) as Box<dyn FnMut(web_sys::Event)>;
let handler: Box<dyn FnMut(web_sys::Event) + 'static> = unsafe { std::mem::transmute(boxed) };
let cb = Closure::wrap(handler).into_js_value(); let cb = Closure::wrap(handler).into_js_value();
window() window()
.add_event_listener_with_callback(event_name, cb.unchecked_ref()) .add_event_listener_with_callback(event_name, cb.unchecked_ref())
.unwrap_throw(); .unwrap_throw();
}
} }
// Hydration operations to find text and comment nodes // Hydration operations to find text and comment nodes

View file

@ -128,11 +128,11 @@ pub fn insert(
.unwrap_or_else(|| initial.clone()) .unwrap_or_else(|| initial.clone())
.unwrap_or(Child::Null); .unwrap_or(Child::Null);
let mut value = f(); let mut value = (f.borrow_mut())();
if current != value { if current != value {
while let Child::Fn(f) = value { while let Child::Fn(f) = value {
value = f(); value = (f.borrow_mut())();
} }
Some(insert_expression( Some(insert_expression(
@ -243,9 +243,9 @@ pub fn insert_expression(
// Nested Signals here simply won't do anything; they should be flattened so it's a single Signal // Nested Signals here simply won't do anything; they should be flattened so it's a single Signal
Child::Fn(f) => { Child::Fn(f) => {
let mut value = f(); let mut value = (f.borrow_mut())();
while let Child::Fn(f) = value { while let Child::Fn(f) = value {
value = f(); value = (f.borrow_mut())();
} }
value value
} }

View file

@ -83,12 +83,14 @@ fn root_element_to_tokens(template_uid: &Ident, node: &Node, mode: Mode) -> Toke
match mode { match mode {
Mode::Ssr => { Mode::Ssr => {
quote! { quote! {{
#(#navigations);*;
format!( format!(
#template, #template,
#(#expressions),* #(#expressions),*
) )
} }}
} }
_ => { _ => {
// create the root element from which navigations and expressions will begin // create the root element from which navigations and expressions will begin
@ -165,7 +167,14 @@ fn element_to_tokens(
// attributes // attributes
for attr in &node.attributes { for attr in &node.attributes {
attr_to_tokens(attr, &this_el_ident, template, expressions, mode); attr_to_tokens(
attr,
&this_el_ident,
template,
expressions,
navigations,
mode,
);
} }
// navigation for this el // navigation for this el
@ -187,7 +196,9 @@ fn element_to_tokens(
let #this_el_ident = #parent.first_child().unwrap_throw(); let #this_el_ident = #parent.first_child().unwrap_throw();
} }
}; };
navigations.push(this_nav); if mode != Mode::Ssr {
navigations.push(this_nav);
}
// self-closing tags // self-closing tags
// https://developer.mozilla.org/en-US/docs/Glossary/Empty_element // https://developer.mozilla.org/en-US/docs/Glossary/Empty_element
@ -261,6 +272,7 @@ fn attr_to_tokens(
el_id: &Ident, el_id: &Ident,
template: &mut String, template: &mut String,
expressions: &mut Vec<TokenStream>, expressions: &mut Vec<TokenStream>,
navigations: &mut Vec<TokenStream>,
mode: Mode, mode: Mode,
) { ) {
let name = node let name = node
@ -283,8 +295,26 @@ fn attr_to_tokens(
// refs // refs
if name == "ref" { if name == "ref" {
// refs mean nothing in SSR let ident = match &node.value {
if mode != Mode::Ssr { Some(expr) => {
if let Some(ident) = expr_to_ident(expr) {
quote_spanned! { span => #ident }
} else {
quote_spanned! { span => compile_error!("'ref' needs to be passed a variable name") }
}
}
None => {
quote_spanned! { span => compile_error!("'ref' needs to be passed a variable name") }
}
};
if mode == Mode::Ssr {
// fake the initialization; should only be used in effects or event handlers, which will never run on the server
// but if we don't initialize it, the compiler will complain
navigations.push(quote_spanned! {
span => #ident = String::new();
});
} else {
expressions.push(match &node.value { expressions.push(match &node.value {
Some(expr) => { Some(expr) => {
if let Some(ident) = expr_to_ident(expr) { if let Some(ident) = expr_to_ident(expr) {
@ -329,7 +359,7 @@ fn attr_to_tokens(
// Classes // Classes
else if name.starts_with("class:") { else if name.starts_with("class:") {
if mode == Mode::Ssr { if mode == Mode::Ssr {
todo!() // TODO class: in SSR
} else { } else {
let name = name.replacen("class:", "", 1); let name = name.replacen("class:", "", 1);
let value = node.value.as_ref().expect("class: attributes need values"); let value = node.value.as_ref().expect("class: attributes need values");
@ -468,11 +498,15 @@ fn child_to_tokens(
}; };
if let Some(v) = str_value { if let Some(v) = str_value {
navigations.push(location); if mode != Mode::Ssr {
navigations.push(location);
}
template.push_str(&v); template.push_str(&v);
PrevSibChange::Sib(name) PrevSibChange::Sib(name)
} else /* if next_sib.is_some() */ { } else
/* if next_sib.is_some() */
{
// these markers are one of the primary templating differences across modes // these markers are one of the primary templating differences across modes
match mode { match mode {
// in CSR, simply insert a comment node: it will be picked up and replaced with the value // in CSR, simply insert a comment node: it will be picked up and replaced with the value

View file

@ -2,14 +2,14 @@ use crate::{Runtime, Scope, ScopeId, Source, Subscriber};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{any::type_name, cell::RefCell, collections::HashSet, fmt::Debug, marker::PhantomData}; use std::{any::type_name, cell::RefCell, collections::HashSet, fmt::Debug, marker::PhantomData};
pub fn create_render_effect<T>(cx: Scope, f: impl FnMut(Option<T>) -> T + 'static) -> Effect<T> pub fn create_render_effect<T>(cx: Scope, f: impl FnMut(Option<T>) -> T + 'static)
where where
T: Debug + 'static, T: Debug + 'static,
{ {
cx.create_eff(true, f) cx.create_eff(true, f)
} }
pub fn create_effect<T>(cx: Scope, f: impl FnMut(Option<T>) -> T + 'static) -> Effect<T> pub fn create_effect<T>(cx: Scope, f: impl FnMut(Option<T>) -> T + 'static)
where where
T: Debug + 'static, T: Debug + 'static,
{ {
@ -17,11 +17,8 @@ where
} }
impl Scope { impl Scope {
pub(crate) fn create_eff<T>( #[cfg(not(feature = "ssr"))]
self, pub(crate) fn create_eff<T>(self, render_effect: bool, f: impl FnMut(Option<T>) -> T + 'static)
render_effect: bool,
f: impl FnMut(Option<T>) -> T + 'static,
) -> Effect<T>
where where
T: Debug + 'static, T: Debug + 'static,
{ {
@ -29,16 +26,19 @@ impl Scope {
let id = self.push_effect(state); let id = self.push_effect(state);
let eff = Effect {
scope: self.id,
id,
ty: PhantomData,
};
self.runtime self.runtime
.any_effect((self.id, id), |effect| effect.run((self.id, id))); .any_effect((self.id, id), |effect| effect.run((self.id, id)));
}
eff // Simply don't run effects on the server at all
#[cfg(feature = "ssr")]
pub(crate) fn create_eff<T>(
self,
_render_effect: bool,
_f: impl FnMut(Option<T>) -> T + 'static,
) where
T: Debug + 'static,
{
} }
} }

View file

@ -2,13 +2,16 @@ use std::collections::HashMap;
#[derive(Debug, PartialEq, Default)] #[derive(Debug, PartialEq, Default)]
pub struct SharedContext { pub struct SharedContext {
#[cfg(any(feature = "csr", feature = "hydrate"))]
pub completed: Vec<web_sys::Element>, pub completed: Vec<web_sys::Element>,
pub events: Vec<()>, pub events: Vec<()>,
pub id: Option<usize>, pub id: Option<usize>,
#[cfg(any(feature = "csr", feature = "hydrate"))]
pub registry: HashMap<String, web_sys::Element>, pub registry: HashMap<String, web_sys::Element>,
} }
impl SharedContext { impl SharedContext {
#[cfg(any(feature = "csr", feature = "hydrate"))]
pub fn new_with_registry(registry: HashMap<String, web_sys::Element>) -> Self { pub fn new_with_registry(registry: HashMap<String, web_sys::Element>) -> Self {
Self { Self {
completed: Default::default(), completed: Default::default(),
@ -28,8 +31,4 @@ impl SharedContext {
0 0
} }
} }
pub fn gather(&self) {
todo!()
}
} }

View file

@ -5,7 +5,6 @@
mod context; mod context;
mod effect; mod effect;
#[cfg(any(feature = "csr", feature = "hydrate"))]
mod hydration; mod hydration;
mod memo; mod memo;
mod resource; mod resource;

View file

@ -6,6 +6,7 @@ pub struct Memo<T>(ReadSignal<Option<T>>)
where where
T: 'static; T: 'static;
#[cfg(not(feature = "ssr"))]
pub fn create_memo<T>(cx: Scope, mut f: impl FnMut(Option<T>) -> T + 'static) -> Memo<T> pub fn create_memo<T>(cx: Scope, mut f: impl FnMut(Option<T>) -> T + 'static) -> Memo<T>
where where
T: PartialEq + Clone + Debug + 'static, T: PartialEq + Clone + Debug + 'static,
@ -23,6 +24,18 @@ where
Memo(read) Memo(read)
} }
// On the server, Memo just carries its original value
// If we didn't provide this alternate version, it would panic because its inner effect wouldn't run
#[cfg(feature = "ssr")]
pub fn create_memo<T>(cx: Scope, mut f: impl FnMut(Option<T>) -> T + 'static) -> Memo<T>
where
T: PartialEq + Clone + Debug + 'static,
{
let (read, _) = create_signal(cx, Some(f(None)));
Memo(read)
}
impl<T> Clone for Memo<T> impl<T> Clone for Memo<T>
where where
T: 'static, T: 'static,

View file

@ -65,6 +65,7 @@ where
}); });
// initial load fires immediately // initial load fires immediately
// TODO SSR — this won't run on server
create_effect(cx, { create_effect(cx, {
let r = Rc::clone(&r); let r = Rc::clone(&r);
move |_| r.load(false) move |_| r.load(false)
@ -179,6 +180,8 @@ where
let suspense_contexts = self.suspense_contexts.clone(); let suspense_contexts = self.suspense_contexts.clone();
let has_value = v.is_some(); let has_value = v.is_some();
// TODO SSR check -- this won't run on server
create_effect(self.scope, move |_| { create_effect(self.scope, move |_| {
if let Some(s) = &suspense_cx { if let Some(s) = &suspense_cx {
let mut contexts = suspense_contexts.borrow_mut(); let mut contexts = suspense_contexts.borrow_mut();

View file

@ -3,16 +3,14 @@ use crate::{
ScopeState, SignalId, SignalState, Subscriber, TransitionState, ScopeState, SignalId, SignalState, Subscriber, TransitionState,
}; };
use slotmap::SlotMap; use slotmap::SlotMap;
use std::cell::{Cell, RefCell}; use std::cell::RefCell;
use std::fmt::Debug; use std::fmt::Debug;
use std::rc::Rc; use std::rc::Rc;
#[cfg(any(feature = "csr", feature = "hydrate"))]
use crate::hydration::SharedContext; use crate::hydration::SharedContext;
#[derive(Default, Debug)] #[derive(Default, Debug)]
pub(crate) struct Runtime { pub(crate) struct Runtime {
#[cfg(any(feature = "csr", feature = "hydrate"))]
pub(crate) shared_context: RefCell<Option<SharedContext>>, pub(crate) shared_context: RefCell<Option<SharedContext>>,
pub(crate) stack: RefCell<Vec<Subscriber>>, pub(crate) stack: RefCell<Vec<Subscriber>>,
pub(crate) scopes: RefCell<SlotMap<ScopeId, Rc<ScopeState>>>, pub(crate) scopes: RefCell<SlotMap<ScopeId, Rc<ScopeState>>>,
@ -133,6 +131,20 @@ impl Runtime {
ScopeDisposer(Box::new(move || scope.dispose())) ScopeDisposer(Box::new(move || scope.dispose()))
} }
pub fn run_scope<T>(&'static self, f: impl FnOnce(Scope) -> T, parent: Option<Scope>) -> T {
let id = {
self.scopes
.borrow_mut()
.insert(Rc::new(ScopeState::new(parent)))
};
let scope = Scope { runtime: self, id };
let ret = f(scope);
scope.dispose();
ret
}
pub fn push_stack(&self, id: Subscriber) { pub fn push_stack(&self, id: Subscriber) {
self.stack.borrow_mut().push(id); self.stack.borrow_mut().push(id);
} }

View file

@ -1,6 +1,6 @@
use crate::{ use crate::{
AnyEffect, AnySignal, EffectId, EffectState, ReadSignal, ResourceId, ResourceState, Runtime, hydration::SharedContext, AnyEffect, AnySignal, EffectId, EffectState, ReadSignal, ResourceId,
SignalId, SignalState, WriteSignal, ResourceState, Runtime, SignalId, SignalState, WriteSignal,
}; };
use elsa::FrozenVec; use elsa::FrozenVec;
use std::{ use std::{
@ -12,11 +12,24 @@ use std::{
}; };
#[must_use = "Scope will leak memory if the disposer function is never called"] #[must_use = "Scope will leak memory if the disposer function is never called"]
/// Creates a child reactive scope and runs the function within it. This is useful for applications
/// like a list or a router, which may want to create child scopes and dispose of them when
/// they are no longer needed (e.g., a list item has been destroyed or the user has navigated away
/// from the route.)
pub fn create_scope(f: impl FnOnce(Scope) + 'static) -> ScopeDisposer { pub fn create_scope(f: impl FnOnce(Scope) + 'static) -> ScopeDisposer {
let runtime = Box::leak(Box::new(Runtime::new())); let runtime = Box::leak(Box::new(Runtime::new()));
runtime.create_scope(f, None) runtime.create_scope(f, None)
} }
/// Creates a temporary scope, runs the given function, disposes of the scope,
/// and returns the value returned from the function. This is very useful for short-lived
/// applications like SSR, where actual reactivity is not required beyond the end
/// of the synchronous operation.
pub fn run_scope<T>(f: impl FnOnce(Scope) -> T + 'static) -> T {
let runtime = Box::leak(Box::new(Runtime::new()));
runtime.run_scope(f, None)
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct Scope { pub struct Scope {
pub(crate) runtime: &'static Runtime, pub(crate) runtime: &'static Runtime,
@ -152,13 +165,13 @@ impl Scope {
let v = curr.node_value(); let v = curr.node_value();
if v == Some("#".to_string()) { if v == Some("#".to_string()) {
count += 1; count += 1;
} else if v == Some("/".to_string()) { } else if v == Some("/".to_string()) {
count -= 1; count -= 1;
if count == 0 { if count == 0 {
current.push(curr.clone()); current.push(curr.clone());
return (curr, current); return (curr, current);
} }
log::debug!(">>> count is now {count}"); log::debug!(">>> count is now {count}");
} }
} }
@ -168,10 +181,28 @@ impl Scope {
} }
log::debug!("end = {end:?}"); log::debug!("end = {end:?}");
log::debug!("current = {:?}", current.iter().map(|n| (n.node_name(), n.node_value())).collect::<Vec<_>>()); log::debug!(
"current = {:?}",
current
.iter()
.map(|n| (n.node_name(), n.node_value()))
.collect::<Vec<_>>()
);
(start, current) (start, current)
} }
pub fn next_hydration_key(&self) -> usize {
let mut sc = self.runtime.shared_context.borrow_mut();
if let Some(ref mut sc) = *sc {
sc.next_hydration_key()
} else {
let mut new_sc = SharedContext::default();
let id = new_sc.next_hydration_key();
*sc = Some(new_sc);
id
}
}
} }
pub struct ScopeDisposer(pub(crate) Box<dyn FnOnce()>); pub struct ScopeDisposer(pub(crate) Box<dyn FnOnce()>);