diff --git a/examples/todomvc-ssr/todomvc-ssr-client/Cargo.toml b/examples/todomvc-ssr/todomvc-ssr-client/Cargo.toml index c7987a525..6b4c31596 100644 --- a/examples/todomvc-ssr/todomvc-ssr-client/Cargo.toml +++ b/examples/todomvc-ssr/todomvc-ssr-client/Cargo.toml @@ -3,6 +3,18 @@ name = "todomvc-ssr-client" version = "0.1.0" edition = "2021" +[lib] +crate-type = ["cdylib", "rlib"] + [dependencies] -todomvc = { path = "../../todomvc", features = ["hydrate"] } +console_log = "0.2" 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' \ No newline at end of file diff --git a/examples/todomvc-ssr/todomvc-ssr-client/src/lib.rs b/examples/todomvc-ssr/todomvc-ssr-client/src/lib.rs new file mode 100644 index 000000000..7ae2e4074 --- /dev/null +++ b/examples/todomvc-ssr/todomvc-ssr-client/src/lib.rs @@ -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! { } + }); +} diff --git a/examples/todomvc-ssr/todomvc-ssr-client/src/main.rs b/examples/todomvc-ssr/todomvc-ssr-client/src/main.rs deleted file mode 100644 index e7a11a969..000000000 --- a/examples/todomvc-ssr/todomvc-ssr-client/src/main.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - println!("Hello, world!"); -} diff --git a/examples/todomvc-ssr/todomvc-ssr-server/Cargo.toml b/examples/todomvc-ssr/todomvc-ssr-server/Cargo.toml index 7a03f1ec5..e6e74432b 100644 --- a/examples/todomvc-ssr/todomvc-ssr-server/Cargo.toml +++ b/examples/todomvc-ssr/todomvc-ssr-server/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] +actix-files = "0.6" actix-web = "4" leptos = { path = "../../../leptos", features = ["ssr"] } -todomvc = { path = "../../todomvc", features = ["ssr"] } +todomvc = { path = "../../todomvc", default-features = false, features = ["ssr"] } diff --git a/examples/todomvc-ssr/todomvc-ssr-server/src/main.rs b/examples/todomvc-ssr/todomvc-ssr-server/src/main.rs index 3db86003f..7e5f6b5a5 100644 --- a/examples/todomvc-ssr/todomvc-ssr-server/src/main.rs +++ b/examples/todomvc-ssr/todomvc-ssr-server/src/main.rs @@ -1,20 +1,43 @@ +use std::{cell::RefCell, rc::Rc}; + +use actix_files::{Directory, Files, NamedFile}; use actix_web::*; use leptos::*; use todomvc::*; +// TODO +// - WASM/JS routes + #[get("/")] async fn render_todomvc() -> impl Responder { - let mut buffer: String; - _ = create_scope(|cx| { - 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()) - ]); + HttpResponse::Ok().content_type("text/html").body(format!( + "{}", + run_scope({ + |cx| { + 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()), + ]); - buffer = view! {
}; - }); - buffer + view! { + + + + + + + "Leptos • TodoMVC" + + + + + + + } + } + }) + )) } #[actix_web::main] @@ -22,8 +45,10 @@ async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new() .service(render_todomvc) + .service(Files::new("/static", "../../todomvc/node_modules")) + .service(Files::new("/pkg", "../todomvc-ssr-client/pkg")) }) .bind(("127.0.0.1", 8080))? .run() .await -} \ No newline at end of file +} diff --git a/examples/todomvc/Cargo.toml b/examples/todomvc/Cargo.toml index 2ed075901..9178b82f6 100644 --- a/examples/todomvc/Cargo.toml +++ b/examples/todomvc/Cargo.toml @@ -8,7 +8,7 @@ leptos = { path = "../../leptos" } wee_alloc = "0.4" miniserde = "0.1" log = "0.4" -console_log = "0.1" +console_log = "0.2" [dev-dependencies] wasm-bindgen-test = "0.3.0" @@ -19,6 +19,7 @@ lto = true opt-level = 'z' [features] +default = ["csr"] csr = ["leptos/csr"] hydrate = ["leptos/hydrate"] ssr = ["leptos/ssr"] diff --git a/examples/todomvc/src/lib.rs b/examples/todomvc/src/lib.rs index 8d30dfc6c..5bda9d57e 100644 --- a/examples/todomvc/src/lib.rs +++ b/examples/todomvc/src/lib.rs @@ -13,23 +13,21 @@ impl Todos { pub fn new(cx: Scope) -> Self { let starting_todos = if is_server!() { Vec::new() + } else if let Ok(Some(storage)) = window().local_storage() { + storage + .get_item(STORAGE_KEY) + .ok() + .flatten() + .and_then(|value| json::from_str::>(&value).ok()) + .map(|values| { + values + .into_iter() + .map(|stored| stored.into_todo(cx)) + .collect() + }) + .unwrap_or_default() } else { - if let Ok(Some(storage)) = window().local_storage() { - storage - .get_item(STORAGE_KEY) - .ok() - .flatten() - .and_then(|value| json::from_str::>(&value).ok()) - .map(|values| { - values - .into_iter() - .map(|stored| stored.into_todo(cx)) - .collect() - }) - .unwrap_or_default() - } else { - Vec::new() - } + Vec::new() }; Self(starting_todos) } @@ -39,7 +37,7 @@ impl Todos { } pub fn add(&mut self, todo: Todo) { - self.0.push(todo); + self.0.push(todo); } pub fn remove(&mut self, id: usize) { @@ -47,17 +45,11 @@ impl Todos { } pub fn remaining(&self) -> usize { - self.0 - .iter() - .filter(|todo| !(todo.completed)()) - .count() + self.0.iter().filter(|todo| !(todo.completed)()).count() } pub fn completed(&self) -> usize { - self.0 - .iter() - .filter(|todo| (todo.completed)()) - .count() + self.0.iter().filter(|todo| (todo.completed)()).count() } pub fn toggle_all(&self) { @@ -67,13 +59,13 @@ impl Todos { if todo.completed.get() { (todo.set_completed)(|completed| *completed = false); } - }; + } } // otherwise, mark them all complete else { - for todo in &self.0 { + for todo in &self.0 { (todo.set_completed)(|completed| *completed = true); - }; + } } } @@ -118,6 +110,14 @@ const ENTER_KEY: u32 = 13; #[component] pub fn TodoMVC(cx: Scope, todos: Todos) -> Vec { + 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); provide_context(cx, set_todos); @@ -127,7 +127,6 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> Vec { set_mode(|mode| *mode = new_mode); }); - let mut next_id = 0; let add_todo = move |ev: web_sys::Event| { let target = event_target::(&ev); ev.stop_propagation(); @@ -144,20 +143,20 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> Vec { }; let filtered_todos = create_memo::>(cx, move |_| { - todos.with(|todos| { - match mode.get() { - Mode::All => todos.0.to_vec(), - Mode::Active => todos.0 - .iter() - .filter(|todo| !todo.completed.get()) - .cloned() - .collect(), - Mode::Completed => todos.0 - .iter() - .filter(|todo| todo.completed.get()) - .cloned() - .collect(), - } + todos.with(|todos| match mode.get() { + Mode::All => todos.0.to_vec(), + Mode::Active => todos + .0 + .iter() + .filter(|todo| !todo.completed.get()) + .cloned() + .collect(), + Mode::Completed => todos + .0 + .iter() + .filter(|todo| todo.completed.get()) + .cloned() + .collect(), }) }); @@ -165,7 +164,9 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> Vec { // this does reactive reads, so it will automatically serialize on any relevant change create_effect(cx, move |_| { if let Ok(Some(storage)) = window().local_storage() { - let objs = todos.get().0 + let objs = todos + .get() + .0 .iter() .map(TodoSerialized::from) .collect::>(); @@ -230,11 +231,9 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> Vec { #[component] 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 set_todos = use_context::>(cx).unwrap(); - let input: web_sys::Element; + let input: Element; let save = move |value: &str| { let value = value.trim(); @@ -288,6 +287,7 @@ pub fn Todo(cx: Scope, todo: Todo) -> Element { }; + #[cfg(not(feature = "ssr"))] create_effect(cx, move |_| { if editing() { _ = input.unchecked_ref::().focus(); diff --git a/leptos_core/src/lib.rs b/leptos_core/src/lib.rs index e805d8d14..9aec25ee4 100644 --- a/leptos_core/src/lib.rs +++ b/leptos_core/src/lib.rs @@ -1,8 +1,12 @@ +#[cfg(any(feature = "csr", feature = "hydrate", feature = "ssr"))] mod for_component; mod map; +#[cfg(any(feature = "csr", feature = "hydrate", feature = "ssr"))] mod suspense; +#[cfg(any(feature = "csr", feature = "hydrate", feature = "ssr"))] pub use for_component::*; +#[cfg(any(feature = "csr", feature = "hydrate", feature = "ssr"))] pub use suspense::*; pub trait Prop { diff --git a/leptos_dom/src/child.rs b/leptos_dom/src/child.rs index 95a50609b..2d40e64ac 100644 --- a/leptos_dom/src/child.rs +++ b/leptos_dom/src/child.rs @@ -1,6 +1,8 @@ -use std::rc::Rc; +use std::{cell::RefCell, rc::Rc}; use leptos_reactive::Scope; + +#[cfg(any(feature = "csr", feature = "hydrate"))] use wasm_bindgen::JsCast; use crate::Node; @@ -9,7 +11,7 @@ use crate::Node; pub enum Child { Null, Text(String), - Fn(Rc Child>), + Fn(Rc Child>>), Node(Node), Nodes(Vec), } @@ -21,9 +23,9 @@ impl Child { Child::Null => String::new(), Child::Text(text) => text.to_string(), Child::Fn(f) => { - let mut value = f(); + let mut value = (f.borrow_mut())(); while let Child::Fn(f) = value { - value = f(); + value = (f.borrow_mut())(); } value.as_child_string() } @@ -106,8 +108,7 @@ where } } -#[cfg(any(feature = "csr", feature = "hydrate"))] -impl IntoChild for Vec { +impl IntoChild for Vec { fn into_child(self, _cx: Scope) -> Child { Child::Nodes(self) } @@ -126,11 +127,11 @@ impl IntoChild for Vec { impl IntoChild for T where - T: Fn() -> U + 'static, + T: FnMut() -> U + 'static, U: IntoChild, { - fn into_child(self, cx: Scope) -> Child { - let modified_fn = Rc::new(move || (self)().into_child(cx)); + fn into_child(mut self, cx: Scope) -> Child { + let modified_fn = Rc::new(RefCell::new(move || (self)().into_child(cx))); Child::Fn(modified_fn) } } diff --git a/leptos_dom/src/lib.rs b/leptos_dom/src/lib.rs index 2d142773d..12e4850fa 100644 --- a/leptos_dom/src/lib.rs +++ b/leptos_dom/src/lib.rs @@ -1,10 +1,9 @@ mod attribute; +#[cfg(any(feature = "csr", feature = "hydrate", feature = "ssr"))] mod child; mod class; -#[cfg(any(feature = "csr", feature = "hydrate"))] mod event_delegation; pub mod logging; -#[cfg(any(feature = "csr", feature = "hydrate"))] mod operations; mod property; #[cfg(any(feature = "csr", feature = "hydrate"))] @@ -13,10 +12,10 @@ mod reconcile; mod render; pub use attribute::*; +#[cfg(any(feature = "csr", feature = "hydrate", feature = "ssr"))] pub use child::*; pub use class::*; pub use logging::*; -#[cfg(any(feature = "csr", feature = "hydrate"))] pub use operations::*; pub use property::*; #[cfg(any(feature = "csr", feature = "hydrate"))] @@ -82,7 +81,7 @@ where }); } -#[cfg(any(feature = "csr", feature = "hydrate"))] +#[cfg(feature = "hydrate")] pub fn hydrate(parent: web_sys::HtmlElement, f: F) where F: Fn(Scope) -> T + 'static, @@ -99,7 +98,7 @@ where pub fn create_component(cx: Scope, f: F) -> T where - F: Fn() -> T, + F: FnOnce() -> T, { // TODO hydration logic here cx.untrack(f) @@ -108,7 +107,7 @@ where #[macro_export] macro_rules! is_server { () => { - cfg!(feature = "server") + cfg!(feature = "ssr") }; } diff --git a/leptos_dom/src/operations.rs b/leptos_dom/src/operations.rs index bbd36d073..a66e78c27 100644 --- a/leptos_dom/src/operations.rs +++ b/leptos_dom/src/operations.rs @@ -239,15 +239,15 @@ pub fn add_event_listener( .unwrap_throw(); */ } -pub fn window_event_listener(event_name: &str, cb: impl Fn(web_sys::Event)) { - let boxed: Box = Box::new(cb); - // Safety: see add_event_listener above - let handler: Box = unsafe { std::mem::transmute(boxed) }; +pub fn window_event_listener(event_name: &str, cb: impl Fn(web_sys::Event) + 'static) { + if !is_server!() { + let handler = Box::new(cb) as Box; - let cb = Closure::wrap(handler).into_js_value(); - window() - .add_event_listener_with_callback(event_name, cb.unchecked_ref()) - .unwrap_throw(); + let cb = Closure::wrap(handler).into_js_value(); + window() + .add_event_listener_with_callback(event_name, cb.unchecked_ref()) + .unwrap_throw(); + } } // Hydration operations to find text and comment nodes diff --git a/leptos_dom/src/render.rs b/leptos_dom/src/render.rs index 9e890dd77..5beb3291b 100644 --- a/leptos_dom/src/render.rs +++ b/leptos_dom/src/render.rs @@ -128,11 +128,11 @@ pub fn insert( .unwrap_or_else(|| initial.clone()) .unwrap_or(Child::Null); - let mut value = f(); + let mut value = (f.borrow_mut())(); if current != value { while let Child::Fn(f) = value { - value = f(); + value = (f.borrow_mut())(); } 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 Child::Fn(f) => { - let mut value = f(); + let mut value = (f.borrow_mut())(); while let Child::Fn(f) = value { - value = f(); + value = (f.borrow_mut())(); } value } diff --git a/leptos_macro/src/view.rs b/leptos_macro/src/view.rs index d5082ff72..037d637a1 100644 --- a/leptos_macro/src/view.rs +++ b/leptos_macro/src/view.rs @@ -83,12 +83,14 @@ fn root_element_to_tokens(template_uid: &Ident, node: &Node, mode: Mode) -> Toke match mode { Mode::Ssr => { - quote! { + quote! {{ + #(#navigations);*; + format!( #template, #(#expressions),* ) - } + }} } _ => { // create the root element from which navigations and expressions will begin @@ -165,7 +167,14 @@ fn element_to_tokens( // 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 @@ -187,7 +196,9 @@ fn element_to_tokens( let #this_el_ident = #parent.first_child().unwrap_throw(); } }; - navigations.push(this_nav); + if mode != Mode::Ssr { + navigations.push(this_nav); + } // self-closing tags // https://developer.mozilla.org/en-US/docs/Glossary/Empty_element @@ -261,6 +272,7 @@ fn attr_to_tokens( el_id: &Ident, template: &mut String, expressions: &mut Vec, + navigations: &mut Vec, mode: Mode, ) { let name = node @@ -283,8 +295,26 @@ fn attr_to_tokens( // refs if name == "ref" { - // refs mean nothing in SSR - if mode != Mode::Ssr { + let ident = match &node.value { + 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 { Some(expr) => { if let Some(ident) = expr_to_ident(expr) { @@ -329,7 +359,7 @@ fn attr_to_tokens( // Classes else if name.starts_with("class:") { if mode == Mode::Ssr { - todo!() + // TODO class: in SSR } else { let name = name.replacen("class:", "", 1); 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 { - navigations.push(location); + if mode != Mode::Ssr { + navigations.push(location); + } template.push_str(&v); 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 match mode { // in CSR, simply insert a comment node: it will be picked up and replaced with the value diff --git a/leptos_reactive/src/effect.rs b/leptos_reactive/src/effect.rs index b41447850..3fb6fb724 100644 --- a/leptos_reactive/src/effect.rs +++ b/leptos_reactive/src/effect.rs @@ -2,14 +2,14 @@ use crate::{Runtime, Scope, ScopeId, Source, Subscriber}; use serde::{Deserialize, Serialize}; use std::{any::type_name, cell::RefCell, collections::HashSet, fmt::Debug, marker::PhantomData}; -pub fn create_render_effect(cx: Scope, f: impl FnMut(Option) -> T + 'static) -> Effect +pub fn create_render_effect(cx: Scope, f: impl FnMut(Option) -> T + 'static) where T: Debug + 'static, { cx.create_eff(true, f) } -pub fn create_effect(cx: Scope, f: impl FnMut(Option) -> T + 'static) -> Effect +pub fn create_effect(cx: Scope, f: impl FnMut(Option) -> T + 'static) where T: Debug + 'static, { @@ -17,11 +17,8 @@ where } impl Scope { - pub(crate) fn create_eff( - self, - render_effect: bool, - f: impl FnMut(Option) -> T + 'static, - ) -> Effect + #[cfg(not(feature = "ssr"))] + pub(crate) fn create_eff(self, render_effect: bool, f: impl FnMut(Option) -> T + 'static) where T: Debug + 'static, { @@ -29,16 +26,19 @@ impl Scope { let id = self.push_effect(state); - let eff = Effect { - scope: self.id, - id, - ty: PhantomData, - }; - self.runtime .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( + self, + _render_effect: bool, + _f: impl FnMut(Option) -> T + 'static, + ) where + T: Debug + 'static, + { } } diff --git a/leptos_reactive/src/hydration.rs b/leptos_reactive/src/hydration.rs index 47fe71601..16d639d2d 100644 --- a/leptos_reactive/src/hydration.rs +++ b/leptos_reactive/src/hydration.rs @@ -2,13 +2,16 @@ use std::collections::HashMap; #[derive(Debug, PartialEq, Default)] pub struct SharedContext { + #[cfg(any(feature = "csr", feature = "hydrate"))] pub completed: Vec, pub events: Vec<()>, pub id: Option, + #[cfg(any(feature = "csr", feature = "hydrate"))] pub registry: HashMap, } impl SharedContext { + #[cfg(any(feature = "csr", feature = "hydrate"))] pub fn new_with_registry(registry: HashMap) -> Self { Self { completed: Default::default(), @@ -28,8 +31,4 @@ impl SharedContext { 0 } } - - pub fn gather(&self) { - todo!() - } } diff --git a/leptos_reactive/src/lib.rs b/leptos_reactive/src/lib.rs index 9cdfb997f..5700147cb 100644 --- a/leptos_reactive/src/lib.rs +++ b/leptos_reactive/src/lib.rs @@ -5,7 +5,6 @@ mod context; mod effect; -#[cfg(any(feature = "csr", feature = "hydrate"))] mod hydration; mod memo; mod resource; diff --git a/leptos_reactive/src/memo.rs b/leptos_reactive/src/memo.rs index 7eceb53d9..053360c60 100644 --- a/leptos_reactive/src/memo.rs +++ b/leptos_reactive/src/memo.rs @@ -6,6 +6,7 @@ pub struct Memo(ReadSignal>) where T: 'static; +#[cfg(not(feature = "ssr"))] pub fn create_memo(cx: Scope, mut f: impl FnMut(Option) -> T + 'static) -> Memo where T: PartialEq + Clone + Debug + 'static, @@ -23,6 +24,18 @@ where 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(cx: Scope, mut f: impl FnMut(Option) -> T + 'static) -> Memo +where + T: PartialEq + Clone + Debug + 'static, +{ + let (read, _) = create_signal(cx, Some(f(None))); + + Memo(read) +} + impl Clone for Memo where T: 'static, diff --git a/leptos_reactive/src/resource.rs b/leptos_reactive/src/resource.rs index 20b2062ca..a6c0100cd 100644 --- a/leptos_reactive/src/resource.rs +++ b/leptos_reactive/src/resource.rs @@ -65,6 +65,7 @@ where }); // initial load fires immediately + // TODO SSR — this won't run on server create_effect(cx, { let r = Rc::clone(&r); move |_| r.load(false) @@ -179,6 +180,8 @@ where let suspense_contexts = self.suspense_contexts.clone(); let has_value = v.is_some(); + + // TODO SSR check -- this won't run on server create_effect(self.scope, move |_| { if let Some(s) = &suspense_cx { let mut contexts = suspense_contexts.borrow_mut(); diff --git a/leptos_reactive/src/runtime.rs b/leptos_reactive/src/runtime.rs index 3b9b60561..06ac1f6b7 100644 --- a/leptos_reactive/src/runtime.rs +++ b/leptos_reactive/src/runtime.rs @@ -3,16 +3,14 @@ use crate::{ ScopeState, SignalId, SignalState, Subscriber, TransitionState, }; use slotmap::SlotMap; -use std::cell::{Cell, RefCell}; +use std::cell::RefCell; use std::fmt::Debug; use std::rc::Rc; -#[cfg(any(feature = "csr", feature = "hydrate"))] use crate::hydration::SharedContext; #[derive(Default, Debug)] pub(crate) struct Runtime { - #[cfg(any(feature = "csr", feature = "hydrate"))] pub(crate) shared_context: RefCell>, pub(crate) stack: RefCell>, pub(crate) scopes: RefCell>>, @@ -133,6 +131,20 @@ impl Runtime { ScopeDisposer(Box::new(move || scope.dispose())) } + pub fn run_scope(&'static self, f: impl FnOnce(Scope) -> T, parent: Option) -> 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) { self.stack.borrow_mut().push(id); } diff --git a/leptos_reactive/src/scope.rs b/leptos_reactive/src/scope.rs index 82d52bd2e..bee4ef658 100644 --- a/leptos_reactive/src/scope.rs +++ b/leptos_reactive/src/scope.rs @@ -1,6 +1,6 @@ use crate::{ - AnyEffect, AnySignal, EffectId, EffectState, ReadSignal, ResourceId, ResourceState, Runtime, - SignalId, SignalState, WriteSignal, + hydration::SharedContext, AnyEffect, AnySignal, EffectId, EffectState, ReadSignal, ResourceId, + ResourceState, Runtime, SignalId, SignalState, WriteSignal, }; use elsa::FrozenVec; use std::{ @@ -12,11 +12,24 @@ use std::{ }; #[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 { let runtime = Box::leak(Box::new(Runtime::new())); 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(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)] pub struct Scope { pub(crate) runtime: &'static Runtime, @@ -152,13 +165,13 @@ impl Scope { let v = curr.node_value(); if v == Some("#".to_string()) { count += 1; - } else if v == Some("/".to_string()) { - count -= 1; + } else if v == Some("/".to_string()) { + count -= 1; if count == 0 { current.push(curr.clone()); return (curr, current); } - + log::debug!(">>> count is now {count}"); } } @@ -168,10 +181,28 @@ impl Scope { } log::debug!("end = {end:?}"); - log::debug!("current = {:?}", current.iter().map(|n| (n.node_name(), n.node_value())).collect::>()); + log::debug!( + "current = {:?}", + current + .iter() + .map(|n| (n.node_name(), n.node_value())) + .collect::>() + ); (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);