Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend ReactiveScope into scopes that are siblings #280

Merged
merged 8 commits into from
Oct 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,5 @@ jobs:
run: |
cd packages/sycamore
cargo miri test
cd ../sycamore-reactive
cargo miri test
8 changes: 6 additions & 2 deletions docs/next/advanced/routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,15 +242,19 @@ When data fetching (e.g. from a REST API) is required to load a page, it is reco
the data. This will cause the router to wait until the data is loaded before rendering the page,
removing the need for some "Loading..." indicator.

`spawn_local_in_scope` is a simple wrapper around `wasm_bindgen_futures::spawn_local` that extends
the current scope into inside the `async` block. Make sure you enable the `"futures"` feature on
`sycamore`.

```rust
use wasm_bindgen_futures::spawn_local;
use sycamore::futures::spawn_local_in_scope;

template! {
Router(RouterProps::new(HistoryIntegration::new(), |route: StateHandle<AppRoutes>| {
let template = Signal::new(Template::empty());
create_effect(cloned!((template) => move || {
let route = route.get();
spawn_local(cloned!((template) => async move {
spawn_local_in_scope(cloned!((template) => async move {
let t = match route.as_ref() {
AppRoutes::Index => template! {
"This is the index page"
Expand Down
2 changes: 1 addition & 1 deletion packages/sycamore-macro/src/template/element.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ impl ToTokens for Element {
attributes,
children,
} = self;

let tag = tag_name.to_string();
let mut quoted = quote! {
let __el = ::sycamore::generic_node::GenericNode::element(#tag);
Expand Down
137 changes: 120 additions & 17 deletions packages/sycamore-reactive/src/effect.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::cell::RefCell;
use std::future::Future;
use std::hash::{Hash, Hasher};
use std::rc::{Rc, Weak};
use std::{mem, ptr};
Expand Down Expand Up @@ -99,26 +100,48 @@ impl ReactiveScope {
pub(crate) fn downgrade(&self) -> ReactiveScopeWeak {
ReactiveScopeWeak(Rc::downgrade(&self.0))
}

/// Runs the passed callback in the reactive scope pointed to by this handle.
pub fn extend<U>(&self, f: impl FnOnce() -> U) -> U {
SCOPES.with(|scopes| {
scopes.borrow_mut().push(ReactiveScope(self.0.clone())); // We now have 2 references to the scope.
let u = f();
scopes.borrow_mut().pop().unwrap(); // Rationale: pop the scope we pushed above.
// Since we have 2 references to the scope, this will not drop the scope.
u
})
}

/// Runs the passed future in the reactive scope pointed to by this handle.
pub async fn extend_future<U>(&self, f: impl Future<Output = U>) -> U {
SCOPES.with(|scopes| {
scopes.borrow_mut().push(ReactiveScope(self.0.clone())); // We now have 2 references to the scope.
});
let u = f.await;
SCOPES.with(|scopes| {
scopes.borrow_mut().pop().unwrap(); // Rationale: pop the scope we pushed above.
// Since we have 2 references to the scope, this will not drop the scope.
});
u
}
}

impl Drop for ReactiveScope {
fn drop(&mut self) {
debug_assert_eq!(
Rc::strong_count(&self.0),
1,
"should only have 1 strong link to ReactiveScopeInner"
);

for effect in &self.0.borrow().effects {
effect
.borrow_mut()
.as_mut()
.unwrap_throw()
.clear_dependencies();
}
if Rc::strong_count(&self.0) == 1 {
// This is the last reference to the scope. Drop all effects and call the cleanup
// callbacks.
for effect in &self.0.borrow().effects {
effect
.borrow_mut()
.as_mut()
.unwrap_throw()
.clear_dependencies();
}

for cleanup in mem::take(&mut self.0.borrow_mut().cleanup) {
untrack(cleanup);
for cleanup in mem::take(&mut self.0.borrow_mut().cleanup) {
untrack(cleanup);
}
}
}
}
Expand All @@ -127,9 +150,53 @@ impl Drop for ReactiveScope {
/// [`ReactiveScope::downgrade`].
///
/// There can only ever be one strong reference (it is impossible to clone a [`ReactiveScope`]).
/// However, there can be multiple weak references to the same [`ReactiveScope`].
/// However, there can be multiple weak references to the same [`ReactiveScope`]. As such, it is
/// impossible to obtain a [`ReactiveScope`] from a [`ReactiveScopeWeak`] because that would allow
/// creating multiple [`ReactiveScope`]s.
#[derive(Default)]
pub(crate) struct ReactiveScopeWeak(pub Weak<RefCell<ReactiveScopeInner>>);
pub struct ReactiveScopeWeak(pub(crate) Weak<RefCell<ReactiveScopeInner>>);

impl ReactiveScopeWeak {
/// Runs the passed callback in the reactive scope pointed to by this handle.
///
/// If the scope has already been destroyed, the callback is not run and `None` is returned.
pub fn extend<U>(&self, f: impl FnOnce() -> U) -> Option<U> {
// We only upgrade this temporarily for the duration of this
// function call.
if let Some(this) = self.0.upgrade() {
SCOPES.with(|scopes| {
scopes.borrow_mut().push(ReactiveScope(this)); // We now have 2 references to the scope.
let u = f();
scopes.borrow_mut().pop().unwrap(); // Rationale: pop the scope we pushed above.
// Since we have 2 references to the scope, this will not drop the scope.
Some(u)
})
} else {
None
}
}

/// Runs the passed future in the reactive scope pointed to by this handle.
///
/// If the scope has already been destroyed, the callback is not run and `None` is returned.
pub async fn extend_future<U>(&self, f: impl Future<Output = U>) -> Option<U> {
// We only upgrade this temporarily for the duration of this
// function call.
if let Some(this) = self.0.upgrade() {
SCOPES.with(|scopes| {
scopes.borrow_mut().push(ReactiveScope(this)); // We now have 2 references to the scope.
});
let u = f.await;
SCOPES.with(|scopes| {
scopes.borrow_mut().pop().unwrap(); // Rationale: pop the scope we pushed above.
// Since we have 2 references to the scope, this will not drop the scope.
Some(u)
})
} else {
None
}
}
}

pub(super) type CallbackPtr = *const RefCell<dyn FnMut()>;

Expand Down Expand Up @@ -540,6 +607,17 @@ pub fn dependency_count() -> Option<usize> {
})
}

/// Returns a [`ReactiveScopeWeak`] handle to the current reactive scope or `None` if outside of a
/// reactive scope.
pub fn current_scope() -> Option<ReactiveScopeWeak> {
SCOPES.with(|scope| {
scope
.borrow()
.last()
.map(|last_context| last_context.downgrade())
})
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -959,4 +1037,29 @@ mod tests {
trigger.set(());
assert_eq!(*counter.get(), 1);
}

#[test]
fn cleanup_in_extended_scope() {
let counter = Signal::new(0);

let root = create_root(cloned!((counter) => move || {
on_cleanup(cloned!((counter) => move || {
counter.set(*counter.get_untracked() + 1);
}));
}));

assert_eq!(*counter.get(), 0);

// Extend the root and add a new on_cleanup callback that increments counter.
root.extend(cloned!((counter) => move || {
on_cleanup(cloned!((counter) => move || {
counter.set(*counter.get_untracked() + 1);
}));
}));

assert_eq!(*counter.get(), 0);

drop(root);
assert_eq!(*counter.get(), 2);
}
}
2 changes: 2 additions & 0 deletions packages/sycamore/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ smallvec = "1.6.1"
sycamore-macro = { path = "../sycamore-macro", version = "=0.6.3" }
sycamore-reactive = { path = "../sycamore-reactive", version = "=0.6.3" }
wasm-bindgen = { version = "0.2.78", features = ["enable-interning"] }
wasm-bindgen-futures = { version = "0.4.28", optional = true }

[dependencies.lexical]
version = "6.0.0"
Expand Down Expand Up @@ -53,6 +54,7 @@ wasm-bindgen-test = "0.3.28"
[features]
default = ["dom"]
dom = []
futures = ["wasm-bindgen-futures"]
ssr = ["html-escape", "once_cell"]
serde = ["sycamore-reactive/serde"]

Expand Down
41 changes: 41 additions & 0 deletions packages/sycamore/src/futures.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use std::future::Future;

use sycamore_reactive::current_scope;
use wasm_bindgen_futures::spawn_local;

/// A wrapper around [`wasm_bindgen_futures::spawn_local`] that extends the current reactive scope
/// that it is called in.
///
/// If the scope is dropped by the time the future is spawned, the callback will not be called.
///
/// If not on `wasm32` target arch, this function is a no-op.
///
/// # Panics
/// This function panics if called outside of a reactive scope.
///
/// # Example
/// ```
/// use sycamore::futures::spawn_local_in_scope;
/// use sycamore::prelude::*;
///
/// create_root(|| {
/// // Inside reactive scope.
/// spawn_local_in_scope(async {
/// // Still inside reactive scope.
/// });
/// });
/// ```
pub fn spawn_local_in_scope<F>(future: F)
where
F: Future<Output = ()> + 'static,
{
if cfg!(target_arch = "wasm32") {
if let Some(scope) = current_scope() {
spawn_local(async move {
scope.extend_future(future).await;
});
} else {
panic!("spawn_local_in_scope called outside of reactive scope");
}
}
}
5 changes: 5 additions & 0 deletions packages/sycamore/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
//! ## Features
//! - `dom` (_default_) - Enables rendering templates to DOM nodes. Only useful on
//! `wasm32-unknown-unknown` target.
//! - `futures` - Enables wrappers around `wasm-bindgen-futures` to make it easier to extend a
//! reactive scope into an `async` function.
//! - `ssr` - Enables rendering templates to static strings (useful for Server Side Rendering /
//! Pre-rendering).
//! - `serde` - Enables serializing and deserializing `Signal`s and other wrapper types using
Expand All @@ -34,6 +36,9 @@ pub mod portal;
pub mod template;
pub mod utils;

#[cfg(feature = "futures")]
pub mod futures;

/// Alias self to sycamore for proc-macros.
extern crate self as sycamore;

Expand Down
7 changes: 3 additions & 4 deletions website/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,12 @@ pulldown-cmark = "0.8.0"
reqwasm = "0.2.1"
serde-lite = { version = "0.2.0", features = ["derive"] }
serde_json = "1.0.68"
sycamore = {path = "../packages/sycamore"}
sycamore-router = {path = "../packages/sycamore-router"}
sycamore = { path = "../packages/sycamore", features = ["futures"] }
sycamore-router = { path = "../packages/sycamore-router" }
wasm-bindgen = "0.2.78"
wasm-bindgen-futures = "0.4.28"

[dev-dependencies]
docs = {path = "../docs"}
docs = { path = "../docs" }

[dependencies.web-sys]
features = ["MediaQueryList", "Storage", "Window"]
Expand Down
4 changes: 2 additions & 2 deletions website/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ use reqwasm::http::Request;
use serde_lite::Deserialize;
use sidebar::SidebarData;
use sycamore::context::{use_context, ContextProvider, ContextProviderProps};
use sycamore::futures::spawn_local_in_scope;
use sycamore::prelude::*;
use sycamore_router::{HistoryIntegration, Route, Router, RouterProps};
use wasm_bindgen_futures::spawn_local;

const LATEST_MAJOR_VERSION: &str = "v0.6";
const NEXT_VERSION: &str = "next";
Expand Down Expand Up @@ -68,7 +68,7 @@ fn switch<G: GenericNode>(route: StateHandle<Routes>) -> Template<G> {
let cached_sidebar_data: Signal<Option<(Option<String>, SidebarData)>> = Signal::new(None);
create_effect(cloned!((template) => move || {
let route = route.get();
spawn_local(cloned!((template, cached_sidebar_data) => async move {
spawn_local_in_scope(cloned!((template, cached_sidebar_data) => async move {
let t = match route.as_ref() {
Routes::Index => template! {
div(class="container mx-auto") {
Expand Down