remove leptos_reactive (moved into reactive_graph and leptos_server)

This commit is contained in:
Greg Johnston 2024-06-11 14:43:47 -04:00
parent ea76a0f74e
commit 6088da7342
44 changed files with 0 additions and 13152 deletions

View file

@ -18,7 +18,6 @@ members = [
"leptos_config",
"leptos_hot_reload",
"leptos_macro",
"leptos_reactive",
"leptos_server",
"reactive_graph",
"server_fn",
@ -55,7 +54,6 @@ leptos_dom = { path = "./leptos_dom", version = "0.7.0-preview2" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.0-preview2" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.0-preview2" }
leptos_macro = { path = "./leptos_macro", version = "0.7.0-preview2" }
leptos_reactive = { path = "./leptos_reactive", version = "0.7.0-preview2" }
leptos_router = { path = "./router", version = "0.7.0-preview2" }
leptos_server = { path = "./leptos_server", version = "0.7.0-preview2" }
leptos_meta = { path = "./meta", version = "0.7.0-preview2" }

View file

@ -1,126 +0,0 @@
[package]
name = "leptos_reactive"
version = { workspace = true }
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
description = "Reactive system for the Leptos web framework."
rust-version.workspace = true
[dependencies]
oco_ref = { workspace = true }
slotmap = { version = "1", features = ["serde"] }
serde = { version = "1", features = ["derive"] }
serde-lite = { version = "0.5", optional = true }
futures = { version = "0.3" }
js-sys = { version = "0.3", optional = true }
miniserde = { version = "0.1", optional = true }
rkyv = { version = "0.7.39", features = [
"validation",
"uuid",
"strict",
], optional = true }
bytecheck = { version = "0.7", features = [
"uuid",
"simdutf8",
], optional = true }
rustc-hash = "1"
serde-wasm-bindgen = "0.6"
serde_json = "1"
spin-sdk = { version = "3", optional = true }
base64 = "0.22"
thiserror = "1"
tokio = { version = "1", features = [
"rt",
], optional = true, default-features = false }
tracing = "0.1"
wasm-bindgen = { version = "0.2", optional = true }
wasm-bindgen-futures = { version = "0.4", optional = true }
web-sys = { version = "0.3", optional = true, features = [
"DocumentFragment",
"Element",
"HtmlTemplateElement",
"NodeList",
"Window",
] }
cfg-if = "1"
indexmap = "2"
self_cell = "1.0.0"
pin-project = "1"
paste = "1"
[dev-dependencies]
log = "0.4"
tokio-test = "0.4"
leptos = { path = "../leptos" }
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen-futures = { version = "0.4" }
[features]
default = []
csr = [
"dep:js-sys",
"dep:wasm-bindgen",
"dep:wasm-bindgen-futures",
"dep:web-sys",
]
hydrate = [
"dep:js-sys",
"dep:wasm-bindgen",
"dep:wasm-bindgen-futures",
"dep:web-sys",
]
ssr = ["dep:tokio"]
nightly = [] #["rkyv?/copy"] # not working on more recent nightlys
serde = []
serde-lite = ["dep:serde-lite"]
miniserde = ["dep:miniserde"]
rkyv = ["dep:rkyv", "dep:bytecheck"]
experimental-islands = []
spin = ["ssr", "dep:spin-sdk"]
[package.metadata.cargo-all-features]
denylist = ["nightly", "rkyv"]
skip_feature_sets = [
[
"csr",
"ssr",
],
[
"csr",
"hydrate",
],
[
"ssr",
"hydrate",
],
[
"serde",
"serde-lite",
],
[
"serde-lite",
"miniserde",
],
[
"serde",
"miniserde",
],
[
"serde",
"rkyv",
],
[
"miniserde",
"rkyv",
],
[
"serde-lite",
"rkyv",
],
]
[package.metadata.docs.rs]
rustdoc-args = ["--generate-link-to-definition"]

View file

@ -1 +0,0 @@
extend = { path = "../cargo-make/main.toml" }

View file

@ -1,304 +0,0 @@
//! Callbacks define a standard way to store functions and closures. They are useful
//! for component properties, because they can be used to define optional callback functions,
//! which generic props dont support.
//!
//! # Usage
//! Callbacks can be created manually from any function or closure, but the easiest way
//! to create them is to use `#[prop(into)]]` when defining a component.
//! ```
//! # use leptos::*;
//! #[component]
//! fn MyComponent(
//! #[prop(into)] render_number: Callback<i32, String>,
//! ) -> impl IntoView {
//! view! {
//! <div>
//! {render_number.call(1)}
//! // callbacks can be called multiple times
//! {render_number.call(42)}
//! </div>
//! }
//! }
//! // you can pass a closure directly as `render_number`
//! fn test() -> impl IntoView {
//! view! {
//! <MyComponent render_number=|x: i32| x.to_string()/>
//! }
//! }
//! ```
//!
//! *Notes*:
//! - The `render_number` prop can receive any type that implements `Fn(i32) -> String`.
//! - Callbacks are most useful when you want optional generic props.
//! - All callbacks implement the [`Callable`] trait, and can be invoked with `my_callback.call(input)`. On nightly, you can even do `my_callback(input)`
//! - The callback types implement [`Copy`], so they can easily be moved into and out of other closures, just like signals.
//!
//! # Types
//! This modules implements 2 callback types:
//! - [`Callback`]
//! - [`SyncCallback`]
//!
//! Use `SyncCallback` when you want the function to be `Sync` and `Send`.
use crate::{store_value, StoredValue};
use std::{fmt, sync::Arc};
/// A wrapper trait for calling callbacks.
pub trait Callable<In: 'static, Out: 'static = ()> {
/// calls the callback with the specified argument.
fn call(&self, input: In) -> Out;
}
/// Callbacks define a standard way to store functions and closures.
///
/// # Example
/// ```
/// # use leptos::*;
/// # use leptos::{Callable, Callback};
/// #[component]
/// fn MyComponent(
/// #[prop(into)] render_number: Callback<i32, String>,
/// ) -> impl IntoView {
/// view! {
/// <div>
/// {render_number.call(42)}
/// </div>
/// }
/// }
///
/// fn test() -> impl IntoView {
/// view! {
/// <MyComponent render_number=move |x: i32| x.to_string()/>
/// }
/// }
/// ```
pub struct Callback<In: 'static, Out: 'static = ()>(
StoredValue<Box<dyn Fn(In) -> Out>>,
);
impl<In> fmt::Debug for Callback<In> {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
fmt.write_str("Callback")
}
}
impl<In, Out> Clone for Callback<In, Out> {
fn clone(&self) -> Self {
*self
}
}
impl<In, Out> Copy for Callback<In, Out> {}
impl<In, Out> Callback<In, Out> {
/// Creates a new callback from the given function.
pub fn new<F>(f: F) -> Callback<In, Out>
where
F: Fn(In) -> Out + 'static,
{
Self(store_value(Box::new(f)))
}
}
impl<In: 'static, Out: 'static> Callable<In, Out> for Callback<In, Out> {
fn call(&self, input: In) -> Out {
self.0.with_value(|f| f(input))
}
}
macro_rules! impl_from_fn {
($ty:ident) => {
#[cfg(not(feature = "nightly"))]
impl<F, In, T, Out> From<F> for $ty<In, Out>
where
F: Fn(In) -> T + 'static,
T: Into<Out> + 'static,
{
fn from(f: F) -> Self {
Self::new(move |x| f(x).into())
}
}
paste::paste! {
#[cfg(feature = "nightly")]
auto trait [<NotRaw $ty>] {}
#[cfg(feature = "nightly")]
impl<A, B> ![<NotRaw $ty>] for $ty<A, B> {}
#[cfg(feature = "nightly")]
impl<F, In, T, Out> From<F> for $ty<In, Out>
where
F: Fn(In) -> T + [<NotRaw $ty>] + 'static,
T: Into<Out> + 'static,
{
fn from(f: F) -> Self {
Self::new(move |x| f(x).into())
}
}
}
};
}
impl_from_fn!(Callback);
#[cfg(feature = "nightly")]
impl<In, Out> FnOnce<(In,)> for Callback<In, Out> {
type Output = Out;
extern "rust-call" fn call_once(self, args: (In,)) -> Self::Output {
Callable::call(&self, args.0)
}
}
#[cfg(feature = "nightly")]
impl<In, Out> FnMut<(In,)> for Callback<In, Out> {
extern "rust-call" fn call_mut(&mut self, args: (In,)) -> Self::Output {
Callable::call(&*self, args.0)
}
}
#[cfg(feature = "nightly")]
impl<In, Out> Fn<(In,)> for Callback<In, Out> {
extern "rust-call" fn call(&self, args: (In,)) -> Self::Output {
Callable::call(self, args.0)
}
}
/// A callback type that is `Send` and `Sync` if its input type is `Send` and `Sync`.
/// Otherwise, you can use exactly the way you use [`Callback`].
pub struct SyncCallback<In: 'static, Out: 'static = ()>(
StoredValue<Arc<dyn Fn(In) -> Out>>,
);
impl<In> fmt::Debug for SyncCallback<In> {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
fmt.write_str("SyncCallback")
}
}
impl<In, Out> Callable<In, Out> for SyncCallback<In, Out> {
fn call(&self, input: In) -> Out {
self.0.with_value(|f| f(input))
}
}
impl<In, Out> Clone for SyncCallback<In, Out> {
fn clone(&self) -> Self {
Self(self.0)
}
}
impl<In: 'static, Out: 'static> SyncCallback<In, Out> {
/// Creates a new callback from the given function.
pub fn new<F>(fun: F) -> Self
where
F: Fn(In) -> Out + 'static,
{
Self(store_value(Arc::new(fun)))
}
}
impl_from_fn!(SyncCallback);
#[cfg(feature = "nightly")]
impl<In, Out> FnOnce<(In,)> for SyncCallback<In, Out> {
type Output = Out;
extern "rust-call" fn call_once(self, args: (In,)) -> Self::Output {
Callable::call(&self, args.0)
}
}
#[cfg(feature = "nightly")]
impl<In, Out> FnMut<(In,)> for SyncCallback<In, Out> {
extern "rust-call" fn call_mut(&mut self, args: (In,)) -> Self::Output {
Callable::call(&*self, args.0)
}
}
#[cfg(feature = "nightly")]
impl<In, Out> Fn<(In,)> for SyncCallback<In, Out> {
extern "rust-call" fn call(&self, args: (In,)) -> Self::Output {
Callable::call(self, args.0)
}
}
#[cfg(test)]
mod tests {
use crate::{
callback::{Callback, SyncCallback},
create_runtime,
};
struct NoClone {}
#[test]
fn clone_callback() {
let rt = create_runtime();
let callback = Callback::new(move |_no_clone: NoClone| NoClone {});
let _cloned = callback;
rt.dispose();
}
#[test]
fn clone_sync_callback() {
let rt = create_runtime();
let callback = SyncCallback::new(move |_no_clone: NoClone| NoClone {});
let _cloned = callback.clone();
rt.dispose();
}
#[test]
fn callback_from() {
let rt = create_runtime();
let _callback: Callback<(), String> = (|()| "test").into();
rt.dispose();
}
#[test]
fn callback_from_html() {
let rt = create_runtime();
use leptos::{
html::{AnyElement, HtmlElement},
*,
};
let _callback: Callback<String, HtmlElement<AnyElement>> =
(|x: String| {
view! {
<h1>{x}</h1>
}
})
.into();
rt.dispose();
}
#[test]
fn sync_callback_from() {
let rt = create_runtime();
let _callback: SyncCallback<(), String> = (|()| "test").into();
rt.dispose();
}
#[test]
fn sync_callback_from_html() {
use leptos::{
html::{AnyElement, HtmlElement},
*,
};
let rt = create_runtime();
let _callback: SyncCallback<String, HtmlElement<AnyElement>> =
(|x: String| {
view! {
<h1>{x}</h1>
}
})
.into();
rt.dispose();
}
}

View file

@ -1,100 +0,0 @@
use std::sync::Arc;
use tachydom::{
renderer::dom::Dom,
view::{
any_view::{AnyView, IntoAny},
RenderHtml,
},
};
/// The most common type for the `children` property on components,
/// which can only be called once.
pub type Children = Box<dyn FnOnce() -> AnyView<Dom>>;
/// A type for the `children` property on components that can be called
/// more than once.
pub type ChildrenFn = Arc<dyn Fn() -> AnyView<Dom>>;
/// A type for the `children` property on components that can be called
/// more than once, but may mutate the children.
pub type ChildrenFnMut = Box<dyn FnMut() -> AnyView<Dom>>;
// This is to still support components that accept `Box<dyn Fn() -> AnyView>` as a children.
type BoxedChildrenFn = Box<dyn Fn() -> AnyView<Dom>>;
#[doc(hidden)]
pub trait ToChildren<F> {
fn to_children(f: F) -> Self;
}
impl<F, C> ToChildren<F> for Children
where
F: FnOnce() -> C + 'static,
C: RenderHtml<Dom> + 'static,
{
#[inline]
fn to_children(f: F) -> Self {
Box::new(move || f().into_any())
}
}
impl<F, C> ToChildren<F> for ChildrenFn
where
F: Fn() -> C + 'static,
C: RenderHtml<Dom> + 'static,
{
#[inline]
fn to_children(f: F) -> Self {
Arc::new(move || f().into_any())
}
}
impl<F, C> ToChildren<F> for ChildrenFnMut
where
F: Fn() -> C + 'static,
C: RenderHtml<Dom> + 'static,
{
#[inline]
fn to_children(f: F) -> Self {
Box::new(move || f().into_any())
}
}
impl<F, C> ToChildren<F> for BoxedChildrenFn
where
F: Fn() -> C + 'static,
C: RenderHtml<Dom> + 'static,
{
#[inline]
fn to_children(f: F) -> Self {
Box::new(move || f().into_any())
}
}
/// New-type wrapper for the a function that returns a view with `From` and `Default` traits implemented
/// to enable optional props in for example `<Show>` and `<Suspense>`.
#[derive(Clone)]
pub struct ViewFn(Arc<dyn Fn() -> AnyView<Dom>>);
impl Default for ViewFn {
fn default() -> Self {
Self(Arc::new(|| ().into_any()))
}
}
impl<F, C> From<F> for ViewFn
where
F: Fn() -> C + 'static,
C: RenderHtml<Dom> + 'static,
{
fn from(value: F) -> Self {
Self(Arc::new(move || value().into_any()))
}
}
impl ViewFn {
/// Execute the wrapped function
pub fn run(&self) -> AnyView<Dom> {
(self.0)()
}
}

View file

@ -1,83 +0,0 @@
//! Utility traits and functions that allow building components,
//! as either functions of their props or functions with no arguments,
//! without knowing the name of the props struct.
pub trait Component<P> {}
pub trait Props {
type Builder;
fn builder() -> Self::Builder;
}
#[doc(hidden)]
pub trait PropsOrNoPropsBuilder {
type Builder;
fn builder_or_not() -> Self::Builder;
}
#[doc(hidden)]
#[derive(Copy, Clone, Debug, Default)]
pub struct EmptyPropsBuilder {}
impl EmptyPropsBuilder {
pub fn build(self) {}
}
impl<P: Props> PropsOrNoPropsBuilder for P {
type Builder = <P as Props>::Builder;
fn builder_or_not() -> Self::Builder {
Self::builder()
}
}
impl PropsOrNoPropsBuilder for EmptyPropsBuilder {
type Builder = EmptyPropsBuilder;
fn builder_or_not() -> Self::Builder {
EmptyPropsBuilder {}
}
}
impl<F, R> Component<EmptyPropsBuilder> for F where F: FnOnce() -> R {}
impl<P, F, R> Component<P> for F
where
F: FnOnce(P) -> R,
P: Props,
{
}
pub fn component_props_builder<P: PropsOrNoPropsBuilder>(
_f: &impl Component<P>,
) -> <P as PropsOrNoPropsBuilder>::Builder {
<P as PropsOrNoPropsBuilder>::builder_or_not()
}
pub fn component_view<P, T>(f: impl ComponentConstructor<P, T>, props: P) -> T {
f.construct(props)
}
pub trait ComponentConstructor<P, T> {
fn construct(self, props: P) -> T;
}
impl<Func, T> ComponentConstructor<(), T> for Func
where
Func: FnOnce() -> T,
{
fn construct(self, (): ()) -> T {
(self)()
}
}
impl<Func, T, P> ComponentConstructor<P, T> for Func
where
Func: FnOnce(P) -> T,
P: PropsOrNoPropsBuilder,
{
fn construct(self, props: P) -> T {
(self)(props)
}
}

View file

@ -1,322 +0,0 @@
use crate::runtime::with_runtime;
use std::any::{Any, TypeId};
/// Provides a context value of type `T` to the current reactive node
/// and all of its descendants. This can be consumed using [`use_context`](crate::use_context).
///
/// This is useful for passing values down to components or functions lower in a
/// hierarchy without needs to “prop drill” by passing them through each layer as
/// arguments to a function or properties of a component.
///
/// Context works similarly to variable scope: a context that is provided higher in
/// the reactive graph can be used lower down, but a context that is provided lower
/// down cannot be used higher up.
///
/// ```
/// use leptos::*;
///
/// // define a newtype we'll provide as context
/// // contexts are stored by their types, so it can be useful to create
/// // a new type to avoid confusion with other `WriteSignal<i32>`s we may have
/// // all types to be shared via context should implement `Clone`
/// #[derive(Copy, Clone)]
/// struct ValueSetter(WriteSignal<i32>);
///
/// #[component]
/// pub fn Provider() -> impl IntoView {
/// let (value, set_value) = create_signal(0);
///
/// // the newtype pattern isn't *necessary* here but is a good practice
/// // it avoids confusion with other possible future `WriteSignal<bool>` contexts
/// // and makes it easier to refer to it in ButtonD
/// provide_context(ValueSetter(set_value));
///
/// // because <Consumer/> is nested inside <Provider/>,
/// // it has access to the provided context
/// view! { <div><Consumer/></div> }
/// }
///
/// #[component]
/// pub fn Consumer() -> impl IntoView {
/// // consume the provided context of type `ValueSetter` using `use_context`
/// // this traverses up the reactive graph and gets the nearest provided `ValueSetter`
/// let set_value = use_context::<ValueSetter>().unwrap().0;
/// }
/// ```
///
/// ## Warning: Shadowing Context Correctly
///
/// The reactive graph exists alongside the component tree. Generally
/// speaking, context provided by a parent component can be accessed by its children
/// and other descendants, and not vice versa. But components do not exist at
/// runtime: a parent and children that are all rendered unconditionally exist in the same
/// reactive scope.
///
/// This can have unexpected effects on context: namely, children can sometimes override
/// contexts provided by their parents, including for their siblings, if they “shadow” context
/// by providing another context of the same kind.
/// ```rust
/// use leptos::*;
///
/// #[component]
/// fn Parent() -> impl IntoView {
/// provide_context("parent_context");
/// view! {
/// <Child /> // this is receiving "parent_context" as expected
/// <Child /> // but this is receiving "child_context" instead of "parent_context"!
/// }
/// }
///
/// #[component]
/// fn Child() -> impl IntoView {
/// // first, we receive context from parent (just before the override)
/// let context = expect_context::<&'static str>();
/// // then we provide context under the same type
/// provide_context("child_context");
/// view! {
/// <div>{format!("child (context: {context})")}</div>
/// }
/// }
/// ```
/// In this case, neither of the children is rendered dynamically, so there is no wrapping
/// effect created around either. All three components here have the same reactive owner, so
/// providing a new context of the same type in the first `<Child/>` overrides the context
/// that was provided in `<Parent/>`, meaning that the second `<Child/>` receives the context
/// from its sibling instead.
///
/// ### Solution
///
/// If you are using the full Leptos framework, you can use the [`Provider`](../leptos/fn.Provider.html)
/// component to solve this issue.
///
/// ```rust
/// # use leptos::*;
/// #[component]
/// fn Child() -> impl IntoView {
/// let context = expect_context::<&'static str>();
/// // creates a new reactive node, which means the context will
/// // only be provided to its children, not modified in the parent
/// view! {
/// <Provider value="child_context">
/// <div>{format!("child (context: {context})")}</div>
/// </Provider>
/// }
/// }
/// ```
///
/// ### Alternate Solution
///
/// This can also be solved by introducing some additional reactivity. In this case, its simplest
/// to simply make the body of `<Child/>` a function, which means it will be wrapped in a
/// new reactive node when rendered:
/// ```rust
/// # use leptos::*;
/// #[component]
/// fn Child() -> impl IntoView {
/// let context = expect_context::<&'static str>();
/// // creates a new reactive node, which means the context will
/// // only be provided to its children, not modified in the parent
/// move || {
/// provide_context("child_context");
/// view! {
/// <div>{format!("child (context: {context})")}</div>
/// }
/// }
/// }
/// ```
///
/// This is equivalent to the difference between two different forms of variable shadowing
/// in ordinary Rust:
/// ```rust
/// // shadowing in a flat hierarchy overrides value for siblings
/// // <Parent/>: declares variable
/// let context = "parent_context";
/// // First <Child/>: consumes variable, then shadows
/// println!("{context:?}");
/// let context = "child_context";
/// // Second <Child/>: consumes variable, then shadows
/// println!("{context:?}");
/// let context = "child_context";
///
/// // but shadowing in nested scopes works as expected
/// // <Parent/>
/// let context = "parent_context";
///
/// // First <Child/>
/// {
/// println!("{context:?}");
/// let context = "child_context";
/// }
///
/// // Second <Child/>
/// {
/// println!("{context:?}");
/// let context = "child_context";
/// }
/// ```
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
#[track_caller]
pub fn provide_context<T>(value: T)
where
T: Clone + 'static,
{
let id = value.type_id();
#[cfg(debug_assertions)]
let defined_at = std::panic::Location::caller();
with_runtime(|runtime| {
let mut contexts = runtime.contexts.borrow_mut();
let owner = runtime.owner.get();
if let Some(owner) = owner {
let context = contexts.entry(owner).unwrap().or_default();
context.insert(id, Box::new(value) as Box<dyn Any>);
} else {
crate::macros::debug_warn!(
"At {defined_at}, you are calling provide_context() outside \
the reactive system.",
);
}
})
.expect("provide_context failed");
}
/// Extracts a context value of type `T` from the reactive system by traversing
/// it upwards, beginning from the current reactive owner and iterating
/// through its parents, if any. The context value should have been provided elsewhere
/// using [`provide_context`](crate::provide_context).
///
/// This is useful for passing values down to components or functions lower in a
/// hierarchy without needs to “prop drill” by passing them through each layer as
/// arguments to a function or properties of a component.
///
/// Context works similarly to variable scope: a context that is provided higher in
/// the reactive graph can be used lower down, but a context that is provided lower
/// in the tree cannot be used higher up.
///
/// ```
/// use leptos::*;
///
/// // define a newtype we'll provide as context
/// // contexts are stored by their types, so it can be useful to create
/// // a new type to avoid confusion with other `WriteSignal<i32>`s we may have
/// // all types to be shared via context should implement `Clone`
/// #[derive(Copy, Clone)]
/// struct ValueSetter(WriteSignal<i32>);
///
/// #[component]
/// pub fn Provider() -> impl IntoView {
/// let (value, set_value) = create_signal(0);
///
/// // the newtype pattern isn't *necessary* here but is a good practice
/// // it avoids confusion with other possible future `WriteSignal<bool>` contexts
/// // and makes it easier to refer to it in ButtonD
/// provide_context(ValueSetter(set_value));
///
/// // because <Consumer/> is nested inside <Provider/>,
/// // it has access to the provided context
/// view! { <div><Consumer/></div> }
/// }
///
/// #[component]
/// pub fn Consumer() -> impl IntoView {
/// // consume the provided context of type `ValueSetter` using `use_context`
/// // this traverses up the reactive graph and gets the nearest provided `ValueSetter`
/// let set_value = use_context::<ValueSetter>().unwrap().0;
///
/// }
/// ```
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn use_context<T>() -> Option<T>
where
T: Clone + 'static,
{
let ty = TypeId::of::<T>();
with_runtime(|runtime| {
let owner = runtime.owner.get();
if let Some(owner) = owner {
runtime.get_context(owner, ty)
} else {
crate::macros::debug_warn!(
"At {}, you are calling use_context() outside the reactive \
system.",
std::panic::Location::caller()
);
None
}
})
.ok()
.flatten()
}
/// Extracts a context value of type `T` from the reactive system by traversing
/// it upwards, beginning from the current reactive owner and iterating
/// through its parents, if any. The context value should have been provided elsewhere
/// using [provide_context](crate::provide_context).
///
/// This is useful for passing values down to components or functions lower in a
/// hierarchy without needs to “prop drill” by passing them through each layer as
/// arguments to a function or properties of a component.
///
/// Context works similarly to variable scope: a context that is provided higher in
/// the reactive graph can be used lower down, but a context that is provided lower
/// in the tree cannot be used higher up.
///
/// ```
/// use leptos::*;
///
/// // define a newtype we'll provide as context
/// // contexts are stored by their types, so it can be useful to create
/// // a new type to avoid confusion with other `WriteSignal<i32>`s we may have
/// // all types to be shared via context should implement `Clone`
/// #[derive(Copy, Clone)]
/// struct ValueSetter(WriteSignal<i32>);
///
/// #[component]
/// pub fn Provider() -> impl IntoView {
/// let (value, set_value) = create_signal(0);
///
/// // the newtype pattern isn't *necessary* here but is a good practice
/// // it avoids confusion with other possible future `WriteSignal<bool>` contexts
/// // and makes it easier to refer to it in ButtonD
/// provide_context(ValueSetter(set_value));
///
/// // because <Consumer/> is nested inside <Provider/>,
/// // it has access to the provided context
/// view! { <div><Consumer/></div> }
/// }
///
/// #[component]
/// pub fn Consumer() -> impl IntoView {
/// // consume the provided context of type `ValueSetter` using `use_context`
/// // this traverses up the reactive graph and gets the nearest provided `ValueSetter`
/// let set_value = expect_context::<ValueSetter>().0;
///
/// todo!()
/// }
/// ```
///
/// ## Panics
/// Panics if a context of this type is not found in the current reactive
/// owner or its ancestors.
#[track_caller]
pub fn expect_context<T>() -> T
where
T: Clone + 'static,
{
let location = std::panic::Location::caller();
use_context().unwrap_or_else(|| {
panic!(
"{:?} expected context of type {:?} to be present",
location,
std::any::type_name::<T>()
)
})
}

View file

@ -1,78 +0,0 @@
// The point of these diagnostics is to give useful error messages when someone
// tries to access a reactive variable outside the reactive scope. They track when
// you create a signal/memo, and where you access it non-reactively.
#[cfg(debug_assertions)]
#[allow(dead_code)] // allowed for SSR
#[derive(Copy, Clone)]
pub(crate) struct AccessDiagnostics {
pub defined_at: &'static std::panic::Location<'static>,
pub called_at: &'static std::panic::Location<'static>,
}
#[cfg(not(debug_assertions))]
#[derive(Copy, Clone, Default)]
pub(crate) struct AccessDiagnostics {}
/// This just tracks whether we're currently in a context in which it really doesn't
/// matter whether something is reactive: for example, in an event listener or timeout.
/// Entering this zone basically turns off the warnings, and exiting it turns them back on.
/// All of this is a no-op in release mode.
#[doc(hidden)]
pub struct SpecialNonReactiveZone {}
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
use std::cell::Cell;
thread_local! {
static IS_SPECIAL_ZONE: Cell<bool> = const { Cell::new(false) };
}
}
}
impl SpecialNonReactiveZone {
#[allow(dead_code)] // allowed for SSR
#[inline(always)]
pub(crate) fn is_inside() -> bool {
#[cfg(debug_assertions)]
{
IS_SPECIAL_ZONE.with(|val| val.get())
}
#[cfg(not(debug_assertions))]
false
}
#[cfg(debug_assertions)]
pub fn enter() -> bool {
IS_SPECIAL_ZONE.with(|val| {
let prev = val.get();
val.set(true);
prev
})
}
#[cfg(debug_assertions)]
pub fn exit(prev: bool) {
if !prev {
IS_SPECIAL_ZONE.with(|val| val.set(false))
}
}
}
#[doc(hidden)]
#[macro_export]
macro_rules! diagnostics {
($this:ident) => {{
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
AccessDiagnostics {
defined_at: $this.defined_at,
called_at: std::panic::Location::caller()
}
} else {
AccessDiagnostics { }
}
}
}};
}

View file

@ -1,377 +0,0 @@
use crate::{node::NodeId, with_runtime, Disposer, Runtime, SignalDispose};
use cfg_if::cfg_if;
use std::{any::Any, cell::RefCell, marker::PhantomData, rc::Rc};
/// Effects run a certain chunk of code whenever the signals they depend on change.
/// `create_effect` queues the given function to run once, tracks its dependence
/// on any signal values read within it, and reruns the function whenever the value
/// of a dependency changes.
///
/// Effects are intended to run *side-effects* of the system, not to synchronize state
/// *within* the system. In other words: don't write to signals within effects, unless
/// youre coordinating with some other non-reactive side effect.
/// (If you need to define a signal that depends on the value of other signals, use a
/// derived signal or [`create_memo`](crate::create_memo)).
///
/// This first run is queued for the next microtask, i.e., it runs after all other
/// synchronous code has completed. In practical terms, this means that if you use
/// `create_effect` in the body of the component, it will run *after* the view has been
/// created and (presumably) mounted. (If you need an effect that runs immediately, use
/// [`create_render_effect`].)
///
/// The effect function is called with an argument containing whatever value it returned
/// the last time it ran. On the initial run, this is `None`.
///
/// By default, effects **do not run on the server**. This means you can call browser-specific
/// APIs within the effect function without causing issues. If you need an effect to run on
/// the server, use [`create_isomorphic_effect`].
/// ```
/// # use leptos_reactive::*;
/// # use log::*;
/// # let runtime = create_runtime();
/// let (a, set_a) = create_signal(0);
/// let (b, set_b) = create_signal(0);
///
/// // ✅ use effects to interact between reactive state and the outside world
/// create_effect(move |_| {
/// // immediately prints "Value: 0" and subscribes to `a`
/// log::debug!("Value: {}", a.get());
/// });
///
/// set_a.set(1);
/// // ✅ because it's subscribed to `a`, the effect reruns and prints "Value: 1"
///
/// // ❌ don't use effects to synchronize state within the reactive system
/// create_effect(move |_| {
/// // this technically works but can cause unnecessary re-renders
/// // and easily lead to problems like infinite loops
/// set_b.set(a.get() + 1);
/// });
/// # if !cfg!(feature = "ssr") {
/// # assert_eq!(b.get(), 2);
/// # }
/// # runtime.dispose();
/// ```
#[cfg_attr(
any(debug_assertions, feature="ssr"),
instrument(
level = "trace",
skip_all,
fields(
ty = %std::any::type_name::<T>()
)
)
)]
#[track_caller]
#[inline(always)]
pub fn create_effect<T>(f: impl Fn(Option<T>) -> T + 'static) -> Effect<T>
where
T: 'static,
{
cfg_if! {
if #[cfg(not(feature = "ssr"))] {
use crate::{Owner, queue_microtask, with_owner};
let runtime = Runtime::current();
let owner = Owner::current();
let id = runtime.create_effect(f);
queue_microtask(move || {
with_owner(owner.unwrap(), move || {
_ = with_runtime( |runtime| {
runtime.update_if_necessary(id);
});
});
});
Effect { id, ty: PhantomData }
} else {
// clear warnings
_ = f;
Effect { id: Default::default(), ty: PhantomData }
}
}
}
impl<T> Effect<T>
where
T: 'static,
{
/// Effects run a certain chunk of code whenever the signals they depend on change.
/// `create_effect` immediately runs the given function once, tracks its dependence
/// on any signal values read within it, and reruns the function whenever the value
/// of a dependency changes.
///
/// Effects are intended to run *side-effects* of the system, not to synchronize state
/// *within* the system. In other words: don't write to signals within effects.
/// (If you need to define a signal that depends on the value of other signals, use a
/// derived signal or [`create_memo`](crate::create_memo)).
///
/// The effect function is called with an argument containing whatever value it returned
/// the last time it ran. On the initial run, this is `None`.
///
/// By default, effects **do not run on the server**. This means you can call browser-specific
/// APIs within the effect function without causing issues. If you need an effect to run on
/// the server, use [`create_isomorphic_effect`].
/// ```
/// # use leptos_reactive::*;
/// # use log::*;
/// # let runtime = create_runtime();
/// let a = RwSignal::new(0);
/// let b = RwSignal::new(0);
///
/// // ✅ use effects to interact between reactive state and the outside world
/// Effect::new(move |_| {
/// // immediately prints "Value: 0" and subscribes to `a`
/// log::debug!("Value: {}", a.get());
/// });
///
/// a.set(1);
/// // ✅ because it's subscribed to `a`, the effect reruns and prints "Value: 1"
///
/// // ❌ don't use effects to synchronize state within the reactive system
/// Effect::new(move |_| {
/// // this technically works but can cause unnecessary re-renders
/// // and easily lead to problems like infinite loops
/// b.set(a.get() + 1);
/// });
/// # if !cfg!(feature = "ssr") {
/// # assert_eq!(b.get(), 2);
/// # }
/// # runtime.dispose();
/// ```
#[track_caller]
#[inline(always)]
pub fn new(f: impl Fn(Option<T>) -> T + 'static) -> Self {
create_effect(f)
}
/// Creates an effect; unlike effects created by [`create_effect`], isomorphic effects will run on
/// the server as well as the client.
/// ```
/// # use leptos_reactive::*;
/// # use log::*;
/// # let runtime = create_runtime();
/// let a = RwSignal::new(0);
/// let b = RwSignal::new(0);
///
/// // ✅ use effects to interact between reactive state and the outside world
/// Effect::new_isomorphic(move |_| {
/// // immediately prints "Value: 0" and subscribes to `a`
/// log::debug!("Value: {}", a.get());
/// });
///
/// a.set(1);
/// // ✅ because it's subscribed to `a`, the effect reruns and prints "Value: 1"
///
/// // ❌ don't use effects to synchronize state within the reactive system
/// Effect::new_isomorphic(move |_| {
/// // this technically works but can cause unnecessary re-renders
/// // and easily lead to problems like infinite loops
/// b.set(a.get() + 1);
/// });
/// # assert_eq!(b.get(), 2);
/// # runtime.dispose();
#[track_caller]
#[inline(always)]
pub fn new_isomorphic(f: impl Fn(Option<T>) -> T + 'static) -> Self {
create_isomorphic_effect(f)
}
/// Applies the given closure to the most recent value of the effect.
///
/// Because effect functions can return values, each time an effect runs it
/// consumes its previous value. This allows an effect to store additional state
/// (like a DOM node, a timeout handle, or a type that implements `Drop`) and
/// keep it alive across multiple runs.
///
/// This method allows access to the effects value outside the effect function.
/// The next time a signal change causes the effect to run, it will receive the
/// mutated value.
pub fn with_value_mut<U>(
&self,
f: impl FnOnce(&mut Option<T>) -> U,
) -> Option<U> {
with_runtime(|runtime| {
let nodes = runtime.nodes.borrow();
let node = nodes.get(self.id)?;
let value = node.value.clone()?;
let mut value = value.borrow_mut();
let value = value.downcast_mut()?;
Some(f(value))
})
.ok()
.flatten()
}
}
/// Creates an effect; unlike effects created by [`create_effect`], isomorphic effects will run on
/// the server as well as the client.
/// ```
/// # use leptos_reactive::*;
/// # use log::*;
/// # let runtime = create_runtime();
/// let (a, set_a) = create_signal(0);
/// let (b, set_b) = create_signal(0);
///
/// // ✅ use effects to interact between reactive state and the outside world
/// create_isomorphic_effect(move |_| {
/// // immediately prints "Value: 0" and subscribes to `a`
/// log::debug!("Value: {}", a.get());
/// });
///
/// set_a.set(1);
/// // ✅ because it's subscribed to `a`, the effect reruns and prints "Value: 1"
///
/// // ❌ don't use effects to synchronize state within the reactive system
/// create_isomorphic_effect(move |_| {
/// // this technically works but can cause unnecessary re-renders
/// // and easily lead to problems like infinite loops
/// set_b.set(a.get() + 1);
/// });
/// # assert_eq!(b.get(), 2);
/// # runtime.dispose();
#[cfg_attr(
any(debug_assertions, feature="ssr"),
instrument(
level = "trace",
skip_all,
fields(
ty = %std::any::type_name::<T>()
)
)
)]
#[track_caller]
#[inline(always)]
pub fn create_isomorphic_effect<T>(
f: impl Fn(Option<T>) -> T + 'static,
) -> Effect<T>
where
T: 'static,
{
let runtime = Runtime::current();
let id = runtime.create_effect(f);
//crate::macros::debug_warn!("creating effect {e:?}");
_ = with_runtime(|runtime| {
runtime.update_if_necessary(id);
});
Effect {
id,
ty: PhantomData,
}
}
/// Creates an effect exactly like [`create_effect`], but runs immediately rather
/// than being queued until the end of the current microtask. This is mostly used
/// inside the renderer but is available for use cases in which scheduling the effect
/// for the next tick is not optimal.
#[cfg_attr(
any(debug_assertions, feature="ssr"),
instrument(
level = "trace",
skip_all,
fields(
ty = %std::any::type_name::<T>()
)
)
)]
#[inline(always)]
pub fn create_render_effect<T>(
f: impl Fn(Option<T>) -> T + 'static,
) -> Effect<T>
where
T: 'static,
{
cfg_if! {
if #[cfg(not(feature = "ssr"))] {
let runtime = Runtime::current();
let id = runtime.create_effect(f);
_ = with_runtime( |runtime| {
runtime.update_if_necessary(id);
});
Effect { id, ty: PhantomData }
} else {
// clear warnings
_ = f;
Effect { id: Default::default(), ty: PhantomData }
}
}
}
/// A handle to an effect, can be used to explicitly dispose of the effect.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct Effect<T> {
pub(crate) id: NodeId,
ty: PhantomData<T>,
}
impl<T> From<Effect<T>> for Disposer {
fn from(effect: Effect<T>) -> Self {
Disposer(effect.id)
}
}
impl<T> SignalDispose for Effect<T> {
fn dispose(self) {
drop(Disposer::from(self));
}
}
pub(crate) struct EffectState<T, F>
where
T: 'static,
F: Fn(Option<T>) -> T,
{
pub(crate) f: F,
pub(crate) ty: PhantomData<T>,
#[cfg(any(debug_assertions, feature = "ssr"))]
pub(crate) defined_at: &'static std::panic::Location<'static>,
}
pub(crate) trait AnyComputation {
fn run(&self, value: Rc<RefCell<dyn Any>>) -> bool;
}
impl<T, F> AnyComputation for EffectState<T, F>
where
T: 'static,
F: Fn(Option<T>) -> T,
{
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
name = "Effect::run()",
level = "trace",
skip_all,
fields(
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
fn run(&self, value: Rc<RefCell<dyn Any>>) -> bool {
// we defensively take and release the BorrowMut twice here
// in case a change during the effect running schedules a rerun
// ideally this should never happen, but this guards against panic
let curr_value = {
// downcast value
let mut value = value.borrow_mut();
let value = value
.downcast_mut::<Option<T>>()
.expect("to downcast effect value");
value.take()
};
// run the effect
let new_value = (self.f)(curr_value);
// set new value
let mut value = value.borrow_mut();
let value = value
.downcast_mut::<Option<T>>()
.expect("to downcast effect value");
*value = Some(new_value);
true
}
}

View file

@ -1,61 +0,0 @@
use std::{hash::Hash, marker::PhantomData};
use tachy_maccy::component;
use tachydom::{
renderer::Renderer,
view::{keyed::keyed, RenderHtml},
};
#[component]
pub fn For<Rndr, IF, I, T, EF, N, KF, K>(
/// Items over which the component should iterate.
each: IF,
/// A key function that will be applied to each item.
key: KF,
/// A function that takes the item, and returns the view that will be displayed for each item.
children: EF,
#[prop(optional)] _rndr: PhantomData<Rndr>,
) -> impl RenderHtml<Rndr>
where
IF: Fn() -> I + 'static,
I: IntoIterator<Item = T>,
EF: Fn(T) -> N + Clone + 'static,
N: RenderHtml<Rndr> + 'static,
KF: Fn(&T) -> K + Clone + 'static,
K: Eq + Hash + 'static,
T: 'static,
Rndr: Renderer + 'static,
{
move || keyed(each(), key.clone(), children.clone())
}
#[cfg(test)]
mod tests {
use crate::For;
use tachy_maccy::view;
use tachy_reaccy::{signal::RwSignal, signal_traits::SignalGet};
use tachydom::{
html::element::HtmlElement, prelude::ElementChild,
renderer::mock_dom::MockDom, view::Render,
};
#[test]
fn creates_list() {
let values = RwSignal::new(vec![1, 2, 3, 4, 5]);
let list: HtmlElement<_, _, _, MockDom> = view! {
<ol>
<For
each=move || values.get()
key=|i| *i
let:i
>
<li>{i}</li>
</For>
</ol>
};
let list = list.build();
assert_eq!(
list.el.to_debug_html(),
"<ol><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li></ol>"
);
}
}

View file

@ -1,369 +0,0 @@
#[cfg(all(feature = "hydrate", feature = "experimental-islands"))]
use crate::Owner;
use crate::{
runtime::PinnedFuture, suspense::StreamChunk, with_runtime, ResourceId,
SignalGet, SuspenseContext,
};
use futures::stream::FuturesUnordered;
#[cfg(feature = "experimental-islands")]
use std::cell::Cell;
use std::collections::{HashMap, HashSet, VecDeque};
#[doc(hidden)]
/// Hydration data and other context that is shared between the server
/// and the client.
pub struct SharedContext {
/// Resources that initially needed to resolve from the server.
pub server_resources: HashSet<ResourceId>,
/// Resources that have not yet resolved.
pub pending_resources: HashSet<ResourceId>,
/// Resources that have already resolved.
pub resolved_resources: HashMap<ResourceId, String>,
/// Suspended fragments that have not yet resolved.
pub pending_fragments: HashMap<String, FragmentData>,
/// Suspense fragments that contain only local resources.
pub fragments_with_local_resources: HashSet<String>,
#[cfg(feature = "experimental-islands")]
pub no_hydrate: bool,
#[cfg(all(feature = "hydrate", feature = "experimental-islands"))]
pub islands: HashMap<Owner, web_sys::HtmlElement>,
}
impl SharedContext {
/// Returns IDs for all [`Resource`](crate::Resource)s found on any scope.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn all_resources() -> Vec<ResourceId> {
with_runtime(|runtime| runtime.all_resources()).unwrap_or_default()
}
/// Returns IDs for all [`Resource`](crate::Resource)s found on any scope that are
/// pending from the server.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn pending_resources() -> Vec<ResourceId> {
with_runtime(|runtime| runtime.pending_resources()).unwrap_or_default()
}
/// Returns IDs for all [`Resource`](crate::Resource)s found on any scope.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn serialization_resolvers(
) -> FuturesUnordered<PinnedFuture<(ResourceId, String)>> {
with_runtime(|runtime| runtime.serialization_resolvers())
.unwrap_or_default()
}
/// Registers the given [`SuspenseContext`](crate::SuspenseContext) with the current scope,
/// calling the `resolver` when its resources are all resolved.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn register_suspense(
context: SuspenseContext,
key: &str,
out_of_order_resolver: impl FnOnce() -> String + 'static,
in_order_resolver: impl FnOnce() -> VecDeque<StreamChunk> + 'static,
) {
use crate::create_isomorphic_effect;
use futures::StreamExt;
_ = with_runtime(|runtime| {
let mut shared_context = runtime.shared_context.borrow_mut();
let (tx1, mut rx1) = futures::channel::mpsc::unbounded();
let (tx2, mut rx2) = futures::channel::mpsc::unbounded();
let (tx3, mut rx3) = futures::channel::mpsc::unbounded();
create_isomorphic_effect(move |_| {
let pending = context
.pending_serializable_resources
.read_only()
.try_get()
.unwrap_or_default();
if pending.is_empty() {
_ = tx1.unbounded_send(());
_ = tx2.unbounded_send(());
_ = tx3.unbounded_send(());
}
});
shared_context.pending_fragments.insert(
key.to_string(),
FragmentData {
out_of_order: Box::pin(async move {
rx1.next().await;
out_of_order_resolver()
}),
in_order: Box::pin(async move {
rx2.next().await;
in_order_resolver()
}),
should_block: context.should_block(),
is_ready: Some(Box::pin(async move {
rx3.next().await;
})),
local_only: context.has_local_only(),
},
);
})
}
/// Takes the pending HTML for a single `<Suspense/>` node.
///
/// Returns a tuple of two pinned `Future`s that return content for out-of-order
/// and in-order streaming, respectively.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn take_pending_fragment(id: &str) -> Option<FragmentData> {
with_runtime(|runtime| {
let mut shared_context = runtime.shared_context.borrow_mut();
shared_context.pending_fragments.remove(id)
})
.ok()
.flatten()
}
/// A future that will resolve when all blocking fragments are ready.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn blocking_fragments_ready() -> PinnedFuture<()> {
use futures::StreamExt;
let mut ready = with_runtime(|runtime| {
let mut shared_context = runtime.shared_context.borrow_mut();
let ready = FuturesUnordered::new();
for (_, data) in shared_context.pending_fragments.iter_mut() {
if data.should_block {
if let Some(is_ready) = data.is_ready.take() {
ready.push(is_ready);
}
}
}
ready
})
.unwrap_or_default();
Box::pin(async move { while ready.next().await.is_some() {} })
}
/// The set of all HTML fragments currently pending.
///
/// The keys are hydration IDs. Values are tuples of two pinned
/// `Future`s that return content for out-of-order and in-order streaming, respectively.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn pending_fragments() -> HashMap<String, FragmentData> {
with_runtime(|runtime| {
let mut shared_context = runtime.shared_context.borrow_mut();
std::mem::take(&mut shared_context.pending_fragments)
})
.unwrap_or_default()
}
/// Registers the given element as an island with the current reactive owner.
#[cfg(all(feature = "hydrate", feature = "experimental-islands"))]
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn register_island(el: &web_sys::HtmlElement) {
if let Some(owner) = Owner::current() {
let el = el.clone();
_ = with_runtime(|runtime| {
let mut shared_context = runtime.shared_context.borrow_mut();
shared_context.islands.insert(owner, el);
});
}
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn fragment_has_local_resources(fragment: &str) -> bool {
with_runtime(|runtime| {
let mut shared_context = runtime.shared_context.borrow_mut();
shared_context
.fragments_with_local_resources
.remove(fragment)
})
.unwrap_or_default()
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn fragments_with_local_resources() -> HashSet<String> {
with_runtime(|runtime| {
let mut shared_context = runtime.shared_context.borrow_mut();
std::mem::take(&mut shared_context.fragments_with_local_resources)
})
.unwrap_or_default()
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn register_local_fragment(key: String) {
with_runtime(|runtime| {
let mut shared_context = runtime.shared_context.borrow_mut();
shared_context.fragments_with_local_resources.insert(key);
})
.unwrap_or_default()
}
}
/// Represents its pending `<Suspense/>` fragment.
pub struct FragmentData {
/// Future that represents how it should be render for an out-of-order stream.
pub out_of_order: PinnedFuture<String>,
/// Future that represents how it should be render for an in-order stream.
pub in_order: PinnedFuture<VecDeque<StreamChunk>>,
/// Whether the stream should wait for this fragment before sending any data.
pub should_block: bool,
/// Future that will resolve when the fragment is ready.
pub is_ready: Option<PinnedFuture<()>>,
/// Whether the fragment contains only local resources.
pub local_only: bool,
}
impl core::fmt::Debug for SharedContext {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("SharedContext").finish()
}
}
impl PartialEq for SharedContext {
fn eq(&self, other: &Self) -> bool {
self.pending_resources == other.pending_resources
&& self.resolved_resources == other.resolved_resources
}
}
impl Eq for SharedContext {}
#[allow(clippy::derivable_impls)]
impl Default for SharedContext {
fn default() -> Self {
#[cfg(all(feature = "hydrate", target_arch = "wasm32"))]
{
let pending_resources = js_sys::Reflect::get(
&web_sys::window().unwrap(),
&wasm_bindgen::JsValue::from_str("__LEPTOS_PENDING_RESOURCES"),
);
let pending_resources: HashSet<ResourceId> = pending_resources
.map_err(|_| ())
.and_then(|pr| {
serde_wasm_bindgen::from_value(pr).map_err(|_| ())
})
.unwrap();
let fragments_with_local_resources = js_sys::Reflect::get(
&web_sys::window().unwrap(),
&wasm_bindgen::JsValue::from_str("__LEPTOS_LOCAL_ONLY"),
);
let fragments_with_local_resources: HashSet<String> =
fragments_with_local_resources
.map_err(|_| ())
.and_then(|pr| {
serde_wasm_bindgen::from_value(pr).map_err(|_| ())
})
.unwrap_or_default();
let resolved_resources = js_sys::Reflect::get(
&web_sys::window().unwrap(),
&wasm_bindgen::JsValue::from_str("__LEPTOS_RESOLVED_RESOURCES"),
)
.unwrap(); // unwrap_or(wasm_bindgen::JsValue::NULL);
let resolved_resources =
serde_wasm_bindgen::from_value(resolved_resources).unwrap();
Self {
server_resources: pending_resources.clone(),
//events: Default::default(),
pending_resources,
resolved_resources,
fragments_with_local_resources,
pending_fragments: Default::default(),
#[cfg(feature = "experimental-islands")]
no_hydrate: true,
#[cfg(all(
feature = "hydrate",
feature = "experimental-islands"
))]
islands: Default::default(),
}
}
#[cfg(not(all(feature = "hydrate", target_arch = "wasm32")))]
{
Self {
server_resources: Default::default(),
//events: Default::default(),
pending_resources: Default::default(),
resolved_resources: Default::default(),
pending_fragments: Default::default(),
fragments_with_local_resources: Default::default(),
#[cfg(feature = "experimental-islands")]
no_hydrate: true,
#[cfg(all(
feature = "hydrate",
feature = "experimental-islands"
))]
islands: Default::default(),
}
}
}
}
#[cfg(feature = "experimental-islands")]
thread_local! {
pub static NO_HYDRATE: Cell<bool> = const { Cell::new(true) };
}
#[cfg(feature = "experimental-islands")]
impl SharedContext {
/// Whether the renderer should currently add hydration IDs.
pub fn no_hydrate() -> bool {
NO_HYDRATE.with(Cell::get)
}
/// Sets whether the renderer should not add hydration IDs.
pub fn set_no_hydrate(hydrate: bool) {
NO_HYDRATE.with(|cell| cell.set(hydrate));
}
/// Turns on hydration for the duration of the function call
#[inline(always)]
pub fn with_hydration<T>(f: impl FnOnce() -> T) -> T {
let prev = SharedContext::no_hydrate();
SharedContext::set_no_hydrate(false);
let v = f();
SharedContext::set_no_hydrate(prev);
v
}
/// Turns off hydration for the duration of the function call
#[inline(always)]
pub fn no_hydration<T>(f: impl FnOnce() -> T) -> T {
let prev = SharedContext::no_hydrate();
SharedContext::set_no_hydrate(true);
let v = f();
SharedContext::set_no_hydrate(prev);
v
}
}

View file

@ -1,8 +0,0 @@
(function (pkg_path, output_name, wasm_output_name) {
import(`/${pkg_path}/${output_name}.js`)
.then(mod => {
mod.default(`/${pkg_path}/${wasm_output_name}.wasm`).then(() => {
mod.hydrate();
});
})
})

View file

@ -1,26 +0,0 @@
(function (pkg_path, output_name, wasm_output_name) {
function idle(c) {
if ("requestIdleCallback" in window) {
window.requestIdleCallback(c);
} else {
c();
}
}
idle(() => {
import(`/${pkg_path}/${output_name}.js`)
.then(mod => {
mod.default(`/${pkg_path}/${wasm_output_name}.wasm`).then(() => {
mod.hydrate();
for (let e of document.querySelectorAll("leptos-island")) {
const l = e.dataset.component;
const islandFn = mod["_island_" + l];
if (islandFn) {
islandFn(e);
} else {
console.warn(`Could not find WASM function for the island ${l}.`);
}
}
});
})
});
})

View file

@ -1,57 +0,0 @@
#![allow(clippy::needless_lifetimes)]
use crate::prelude::*;
use leptos_config::LeptosOptions;
use tachydom::view::RenderHtml;
#[component]
pub fn AutoReload<'a>(
#[prop(optional)] disable_watch: bool,
#[prop(optional)] nonce: Option<&'a str>,
options: LeptosOptions,
) -> impl RenderHtml<Dom> + 'a {
(!disable_watch && std::env::var("LEPTOS_WATCH").is_ok()).then(|| {
let reload_port = match options.reload_external_port {
Some(val) => val,
None => options.reload_port,
};
let protocol = match options.reload_ws_protocol {
leptos_config::ReloadWSProtocol::WS => "'ws://'",
leptos_config::ReloadWSProtocol::WSS => "'wss://'",
};
let script = include_str!("reload_script.js");
view! {
<script crossorigin=nonce>
{format!("{script}({reload_port:?}, {protocol})")}
</script>
}
})
}
#[component]
pub fn HydrationScripts(
options: LeptosOptions,
#[prop(optional)] islands: bool,
) -> impl RenderHtml<Dom> {
let pkg_path = &options.site_pkg_dir;
let output_name = &options.output_name;
let mut wasm_output_name = output_name.clone();
if std::option_env!("LEPTOS_OUTPUT_NAME").is_none() {
wasm_output_name.push_str("_bg");
}
let nonce = None::<String>; // use_nonce(); // TODO
let script = if islands {
include_str!("./island_script.js")
} else {
include_str!("./hydration_script.js")
};
view! {
<link rel="modulepreload" href=format!("/{pkg_path}/{output_name}.js") nonce=nonce.clone()/>
<link rel="preload" href=format!("/{pkg_path}/{wasm_output_name}.wasm") r#as="fetch" r#type="application/wasm" crossorigin=nonce.clone().unwrap_or_default()/>
<script type="module" nonce=nonce>
{format!("{script}({pkg_path:?}, {output_name:?}, {wasm_output_name:?})")}
</script>
}
}

View file

@ -1,23 +0,0 @@
(function (reload_port, protocol) {
let host = window.location.hostname;
let ws = new WebSocket(`${protocol}${host}:${reload_port}/live_reload`);
ws.onmessage = (ev) => {
let msg = JSON.parse(ev.data);
if (msg.all) window.location.reload();
if (msg.css) {
let found = false;
document.querySelectorAll("link").forEach((link) => {
if (link.getAttribute('href').includes(msg.css)) {
let newHref = '/' + msg.css + '?version=' + new Date().getMilliseconds();
link.setAttribute('href', newHref);
found = true;
}
});
if (!found) console.warn(`CSS hot-reload: Could not find a <link href=/\"${msg.css}\"> element`);
};
if(msg.view) {
patch(msg.view);
}
};
ws.onclose = () => console.warn('Live-reload stopped. Manual reload necessary.');
})

View file

@ -1,155 +0,0 @@
#![forbid(unsafe_code)]
#![deny(missing_docs)]
#![cfg_attr(feature = "nightly", feature(fn_traits))]
#![cfg_attr(feature = "nightly", feature(unboxed_closures))]
#![cfg_attr(feature = "nightly", feature(type_name_of_val))]
#![cfg_attr(feature = "nightly", feature(auto_traits))]
#![cfg_attr(feature = "nightly", feature(negative_impls))]
// to prevent warnings from popping up when a nightly feature is stabilized
#![allow(stable_features)]
//! The reactive system for the [Leptos](https://docs.rs/leptos/latest/leptos/) Web framework.
//!
//! ## Fine-Grained Reactivity
//!
//! Leptos is built on a fine-grained reactive system, which means that individual reactive values
//! (“signals,” sometimes known as observables) trigger the code that reacts to them (“effects,”
//! sometimes known as observers) to re-run. These two halves of the reactive system are inter-dependent.
//! Without effects, signals can change within the reactive system but never be observed in a way
//! that interacts with the outside world. Without signals, effects run once but never again, as
//! theres no observable value to subscribe to.
//!
//! Here are the most commonly-used functions and types you'll need to build a reactive system:
//!
//! ### Signals
//! 1. *Signals:* [`create_signal`], which returns a ([`ReadSignal`],
//! [`WriteSignal`] tuple, or [`create_rw_signal`], which returns
//! a signal [`RwSignal`] without this read-write segregation.
//! 2. *Derived Signals:* any function that relies on another signal.
//! 3. *Memos:* [`create_memo`], which returns a [`Memo`].
//! 4. *Resources:* [`create_resource`], which converts an `async` [`Future`](std::future::Future) into a
//! synchronous [`Resource`] signal.
//! 5. *Triggers:* [`create_trigger`], creates a purely reactive [`Trigger`] primitive without any associated state.
//!
//! ### Effects
//! 1. Use [`create_effect`] when you need to synchronize the reactive system
//! with something outside it (for example: logging to the console, writing to a file or local storage)
//! 2. The Leptos DOM renderer wraps any [`Fn`] in your template with [`create_effect`], so
//! components you write do *not* need explicit effects to synchronize with the DOM.
//!
//! ### Example
//! ```
//! use leptos_reactive::*;
//!
//! // creates a new reactive runtime
//! // this is omitted from most of the examples in the docs
//! // you usually won't need to call it yourself
//! let runtime = create_runtime();
//! // a signal: returns a (getter, setter) pair
//! let (count, set_count) = create_signal(0);
//!
//! // calling the getter gets the value
//! // can be `count()` on nightly
//! assert_eq!(count.get(), 0);
//! // calling the setter sets the value
//! // can be `set_count(1)` on nightly
//! set_count.set(1);
//! // or we can mutate it in place with update()
//! set_count.update(|n| *n += 1);
//!
//! // a derived signal: a plain closure that relies on the signal
//! // the closure will run whenever we *access* double_count()
//! let double_count = move || count.get() * 2;
//! assert_eq!(double_count(), 4);
//!
//! // a memo: subscribes to the signal
//! // the closure will run only when count changes
//! let memoized_triple_count = create_memo(move |_| count.get() * 3);
//! // can be `memoized_triple_count()` on nightly
//! assert_eq!(memoized_triple_count.get(), 6);
//!
//! // this effect will run whenever `count` changes
//! create_effect(move |_| {
//! println!("Count = {}", count.get());
//! });
//!
//! // disposes of the reactive runtime
//! runtime.dispose();
//! ```
#[cfg_attr(any(debug_assertions, feature = "ssr"), macro_use)]
extern crate tracing;
#[macro_use]
mod signal;
pub mod callback;
mod context;
#[macro_use]
mod diagnostics;
mod effect;
mod hydration;
// contains "private" implementation details right now.
// could make this unhidden in the future if needed.
// macro_export makes it public from the crate root anyways
#[doc(hidden)]
pub mod macros;
mod memo;
mod node;
mod resource;
mod runtime;
mod selector;
#[cfg(any(doc, feature = "serde"))]
mod serde;
mod serialization;
mod signal_wrappers_read;
mod signal_wrappers_write;
mod slice;
mod spawn;
mod spawn_microtask;
mod stored_value;
pub mod suspense;
mod trigger;
mod watch;
pub use callback::*;
pub use context::*;
pub use diagnostics::SpecialNonReactiveZone;
pub use effect::*;
pub use hydration::{FragmentData, SharedContext};
pub use memo::*;
pub use node::Disposer;
pub use oco::*;
pub use oco_ref as oco;
pub use resource::*;
use runtime::*;
pub use runtime::{
as_child_of_current_owner, batch, create_runtime, current_runtime,
on_cleanup, run_as_child, set_current_runtime,
spawn_local_with_current_owner, spawn_local_with_owner, try_batch,
try_spawn_local_with_current_owner, try_spawn_local_with_owner,
try_with_owner, untrack, untrack_with_diagnostics, with_current_owner,
with_owner, Owner, RuntimeId, ScopedFuture,
};
pub use selector::*;
pub use serialization::*;
pub use signal::{prelude as signal_prelude, *};
pub use signal_wrappers_read::*;
pub use signal_wrappers_write::*;
pub use slice::*;
pub use spawn::*;
pub use spawn_microtask::*;
pub use stored_value::*;
pub use suspense::{GlobalSuspenseContext, SuspenseContext};
pub use trigger::*;
pub use watch::*;
#[doc(hidden)]
pub fn console_warn(s: &str) {
cfg_if::cfg_if! {
if #[cfg(all(target_arch = "wasm32", any(feature = "csr", feature = "hydrate")))] {
web_sys::console::warn_1(&wasm_bindgen::JsValue::from_str(s));
} else {
eprintln!("{s}");
}
}
}

View file

@ -1,344 +0,0 @@
macro_rules! debug_warn {
($($x:tt)*) => {
{
#[cfg(debug_assertions)]
{
($crate::console_warn(&format_args!($($x)*).to_string()))
}
#[cfg(not(debug_assertions))]
{
($($x)*)
}
}
}
}
pub(crate) use debug_warn;
/// Provides a simpler way to use [`SignalWith::with`](crate::SignalWith::with).
///
/// This macro also supports [stored values](crate::StoredValue). If you would
/// like to distinguish between the two, you can also use [`with_value`](crate::with_value)
/// for stored values only.
///
/// The general syntax looks like:
/// ```ignore
/// with!(|capture1, capture2, ...| body);
/// ```
/// The variables within the 'closure' arguments are captured from the
/// environment, and can be used within the body with the same name.
///
/// `move` can also be added before the closure arguments to add `move` to all
/// expanded closures.
///
/// # Examples
/// ```
/// # use leptos_reactive::*;
/// # let runtime = create_runtime();
/// let (first, _) = create_signal("Bob".to_string());
/// let (middle, _) = create_signal("J.".to_string());
/// let (last, _) = create_signal("Smith".to_string());
/// let name = with!(|first, middle, last| format!("{first} {middle} {last}"));
/// assert_eq!(name, "Bob J. Smith");
/// # runtime.dispose();
/// ```
///
/// The `with!` macro in the above example expands to:
/// ```ignore
/// first.with(|first| {
/// middle.with(|middle| {
/// last.with(|last| format!("{first} {middle} {last}"))
/// })
/// })
/// ```
///
/// If `move` is added:
/// ```ignore
/// with!(move |first, last| format!("{first} {last}"))
/// ```
///
/// Then all closures are also `move`.
/// ```ignore
/// first.with(move |first| {
/// last.with(move |last| format!("{first} {last}"))
/// })
/// ```
#[macro_export]
macro_rules! with {
(|$ident:ident $(,)?| $body:expr) => {
$crate::macros::__private::Withable::call_with(&$ident, |$ident| $body)
};
(move |$ident:ident $(,)?| $body:expr) => {
$crate::macros::__private::Withable::call_with(&$ident, move |$ident| $body)
};
(|$first:ident, $($rest:ident),+ $(,)? | $body:expr) => {
$crate::macros::__private::Withable::call_with(
&$first,
|$first| with!(|$($rest),+| $body)
)
};
(move |$first:ident, $($rest:ident),+ $(,)? | $body:expr) => {
$crate::macros::__private::Withable::call_with(
&$first,
move |$first| with!(|$($rest),+| $body)
)
};
}
/// Provides a simpler way to use
/// [`StoredValue::with_value`](crate::StoredValue::with_value).
///
/// To use with [signals](crate::SignalWith::with), see the [`with!`] macro
/// instead.
///
/// Note that the [`with!`] macro also works with
/// [`StoredValue`](crate::StoredValue). Use this macro if you would like to
/// distinguish between signals and stored values.
///
/// The general syntax looks like:
/// ```ignore
/// with_value!(|capture1, capture2, ...| body);
/// ```
/// The variables within the 'closure' arguments are captured from the
/// environment, and can be used within the body with the same name.
///
/// `move` can also be added before the closure arguments to add `move` to all
/// expanded closures.
///
/// # Examples
/// ```
/// # use leptos_reactive::*;
/// # let runtime = create_runtime();
/// let first = store_value("Bob".to_string());
/// let middle = store_value("J.".to_string());
/// let last = store_value("Smith".to_string());
/// let name = with_value!(|first, middle, last| {
/// format!("{first} {middle} {last}")
/// });
/// assert_eq!(name, "Bob J. Smith");
/// # runtime.dispose();
/// ```
/// The `with_value!` macro in the above example expands to:
/// ```ignore
/// first.with_value(|first| {
/// middle.with_value(|middle| {
/// last.with_value(|last| format!("{first} {middle} {last}"))
/// })
/// })
/// ```
///
/// If `move` is added:
/// ```ignore
/// with_value!(move |first, last| format!("{first} {last}"))
/// ```
///
/// Then all closures are also `move`.
/// ```ignore
/// first.with_value(move |first| {
/// last.with_value(move |last| format!("{first} {last}"))
/// })
/// ```
#[macro_export]
macro_rules! with_value {
(|$ident:ident $(,)?| $body:expr) => {
$ident.with_value(|$ident| $body)
};
(move |$ident:ident $(,)?| $body:expr) => {
$ident.with_value(move |$ident| $body)
};
(|$first:ident, $($rest:ident),+ $(,)? | $body:expr) => {
$first.with_value(|$first| with_value!(|$($rest),+| $body))
};
(move |$first:ident, $($rest:ident),+ $(,)? | $body:expr) => {
$first.with_value(move |$first| with_value!(move |$($rest),+| $body))
};
}
/// Provides a simpler way to use
/// [`SignalUpdate::update`](crate::SignalUpdate::update).
///
/// This macro also supports [stored values](crate::StoredValue). If you would
/// like to distinguish between the two, you can also use [`update_value`](crate::update_value)
/// for stored values only.
///
/// The general syntax looks like:
/// ```ignore
/// update!(|capture1, capture2, ...| body);
/// ```
/// The variables within the 'closure' arguments are captured from the
/// environment, and can be used within the body with the same name.
///
/// `move` can also be added before the closure arguments to add `move` to all
/// expanded closures.
///
/// # Examples
/// ```
/// # use leptos_reactive::*;
/// # let runtime = create_runtime();
/// let a = create_rw_signal(1);
/// let b = create_rw_signal(2);
/// update!(|a, b| *a = *a + *b);
/// assert_eq!(a.get(), 3);
/// # runtime.dispose();
/// ```
/// The `update!` macro in the above example expands to:
/// ```ignore
/// a.update(|a| {
/// b.update(|b| *a = *a + *b)
/// })
/// ```
///
/// If `move` is added:
/// ```ignore
/// update!(move |a, b| *a = *a + *b + something_else)
/// ```
///
/// Then all closures are also `move`.
/// ```ignore
/// first.update(move |a| {
/// last.update(move |b| *a = *a + *b + something_else)
/// })
/// ```
#[macro_export]
macro_rules! update {
(|$ident:ident $(,)?| $body:expr) => {
$crate::macros::__private::Updatable::call_update(&$ident, |$ident| $body)
};
(move |$ident:ident $(,)?| $body:expr) => {
$crate::macros::__private::Updatable::call_update(&$ident, move |$ident| $body)
};
(|$first:ident, $($rest:ident),+ $(,)? | $body:expr) => {
$crate::macros::__private::Updatable::call_update(
&$first,
|$first| update!(|$($rest),+| $body)
)
};
(move |$first:ident, $($rest:ident),+ $(,)? | $body:expr) => {
$crate::macros::__private::Updatable::call_update(
&$first,
move |$first| update!(|$($rest),+| $body)
)
};
}
/// Provides a simpler way to use
/// [`StoredValue::update_value`](crate::StoredValue::update_value).
///
/// To use with [signals](crate::SignalUpdate::update), see the [`update`]
/// macro instead.
///
/// Note that the [`update!`] macro also works with
/// [`StoredValue`](crate::StoredValue). Use this macro if you would like to
/// distinguish between signals and stored values.
///
/// The general syntax looks like:
/// ```ignore
/// update_value!(|capture1, capture2, ...| body);
/// ```
/// The variables within the 'closure' arguments are captured from the
/// environment, and can be used within the body with the same name.
///
/// `move` can also be added before the closure arguments to add `move` to all
/// expanded closures.
///
/// # Examples
/// ```
/// # use leptos_reactive::*;
/// # let runtime = create_runtime();
/// let a = store_value(1);
/// let b = store_value(2);
/// update_value!(|a, b| *a = *a + *b);
/// assert_eq!(a.get_value(), 3);
/// # runtime.dispose();
/// ```
/// The `update_value!` macro in the above example expands to:
/// ```ignore
/// a.update_value(|a| {
/// b.update_value(|b| *a = *a + *b)
/// })
/// ```
/// If `move` is added:
/// ```ignore
/// update_value!(move |a, b| *a = *a + *b + something_else)
/// ```
///
/// Then all closures are also `move`.
/// ```ignore
/// first.update_value(move |a| {
/// last.update_value(move |b| *a = *a + *b + something_else)
/// })
/// ```
#[macro_export]
macro_rules! update_value {
(|$ident:ident $(,)?| $body:expr) => {
$ident.update_value(|$ident| $body)
};
(move |$ident:ident $(,)?| $body:expr) => {
$ident.update_value(move |$ident| $body)
};
(|$first:ident, $($rest:ident),+ $(,)? | $body:expr) => {
$first.update_value(|$first| update_value!(|$($rest),+| $body))
};
(move |$first:ident, $($rest:ident),+ $(,)? | $body:expr) => {
$first.update_value(move |$first| update_value!(move |$($rest),+| $body))
};
}
/// This is a private module intended to only be used by macros. Do not access
/// this directly!
#[doc(hidden)]
pub mod __private {
use crate::{SignalUpdate, SignalWith, StoredValue};
pub trait Withable {
type Value;
// don't use `&self` or r-a will suggest importing this trait
// and using it as a method
#[track_caller]
fn call_with<O>(item: &Self, f: impl FnOnce(&Self::Value) -> O) -> O;
}
impl<T> Withable for StoredValue<T> {
type Value = T;
#[inline(always)]
fn call_with<O>(item: &Self, f: impl FnOnce(&Self::Value) -> O) -> O {
item.with_value(f)
}
}
impl<S: SignalWith> Withable for S {
type Value = S::Value;
#[inline(always)]
fn call_with<O>(item: &Self, f: impl FnOnce(&Self::Value) -> O) -> O {
item.with(f)
}
}
pub trait Updatable {
type Value;
#[track_caller]
fn call_update(item: &Self, f: impl FnOnce(&mut Self::Value));
}
impl<T> Updatable for StoredValue<T> {
type Value = T;
#[inline(always)]
fn call_update(item: &Self, f: impl FnOnce(&mut Self::Value)) {
item.update_value(f)
}
}
impl<S: SignalUpdate> Updatable for S {
type Value = S::Value;
#[inline(always)]
fn call_update(item: &Self, f: impl FnOnce(&mut Self::Value)) {
item.update(f)
}
}
}

View file

@ -1,749 +0,0 @@
use crate::{
create_isomorphic_effect, diagnostics::AccessDiagnostics, node::NodeId,
on_cleanup, with_runtime, AnyComputation, Runtime, SignalDispose,
SignalGet, SignalGetUntracked, SignalStream, SignalWith,
SignalWithUntracked,
};
use std::{any::Any, cell::RefCell, fmt, marker::PhantomData, rc::Rc};
// IMPLEMENTATION NOTE:
// Memos are implemented "lazily," i.e., the inner computation is not run
// when the memo is created or when its value is marked as stale, but on demand
// when it is accessed, if the value is stale. This means that the value is stored
// internally as Option<T>, even though it can always be accessed by the user as T.
// This means the inner value can be unwrapped in circumstances in which we know
// `Runtime::update_if_necessary()` has already been called, e.g., in the
// `.try_with_no_subscription()` calls below that are unwrapped with
// `.expect("invariant: must have already been initialized")`.
/// Creates an efficient derived reactive value based on other reactive values.
///
/// Unlike a "derived signal," a memo comes with two guarantees:
/// 1. The memo will only run *once* per change, no matter how many times you
/// access its value.
/// 2. The memo will only notify its dependents if the value of the computation changes.
///
/// This makes a memo the perfect tool for expensive computations.
///
/// Memos have a certain overhead compared to derived signals. In most cases, you should
/// create a derived signal. But if the derivation calculation is expensive, you should
/// create a memo.
///
/// As with [`create_effect`](crate::create_effect), the argument to the memo function is the previous value,
/// i.e., the current value of the memo, which will be `None` for the initial calculation.
///
/// ```
/// # use leptos_reactive::*;
/// # fn really_expensive_computation(value: i32) -> i32 { value };
/// # let runtime = create_runtime();
/// let (value, set_value) = create_signal(0);
///
/// // 🆗 we could create a derived signal with a simple function
/// let double_value = move || value.get() * 2;
/// set_value.set(2);
/// assert_eq!(double_value(), 4);
///
/// // but imagine the computation is really expensive
/// let expensive = move || really_expensive_computation(value.get()); // lazy: doesn't run until called
/// create_effect(move |_| {
/// // 🆗 run #1: calls `really_expensive_computation` the first time
/// log::debug!("expensive = {}", expensive());
/// });
/// create_effect(move |_| {
/// // ❌ run #2: this calls `really_expensive_computation` a second time!
/// let value = expensive();
/// // do something else...
/// });
///
/// // instead, we create a memo
/// // 🆗 run #1: the calculation runs once immediately
/// let memoized = create_memo(move |_| really_expensive_computation(value.get()));
/// create_effect(move |_| {
/// // 🆗 reads the current value of the memo
/// // can be `memoized()` on nightly
/// log::debug!("memoized = {}", memoized.get());
/// });
/// create_effect(move |_| {
/// // ✅ reads the current value **without re-running the calculation**
/// let value = memoized.get();
/// // do something else...
/// });
/// # runtime.dispose();
/// ```
#[cfg_attr(
any(debug_assertions, feature="ssr"),
instrument(
level = "trace",
skip_all,
fields(
ty = %std::any::type_name::<T>()
)
)
)]
#[track_caller]
#[inline(always)]
pub fn create_memo<T>(f: impl Fn(Option<&T>) -> T + 'static) -> Memo<T>
where
T: PartialEq + 'static,
{
Runtime::current().create_owning_memo(move |current_value| {
let new_value = f(current_value.as_ref());
let is_different = current_value.as_ref() != Some(&new_value);
(new_value, is_different)
})
}
/// Like [`create_memo`], `create_owning_memo` creates an efficient derived reactive value based on
/// other reactive values, but with two differences:
/// 1. The argument to the memo function is owned instead of borrowed.
/// 2. The function must also return whether the value has changed, as the first element of the tuple.
///
/// All of the other caveats and guarantees are the same as the usual "borrowing" memos.
///
/// This type of memo is useful for memos which can avoid computation by re-using the last value,
/// especially slices that need to allocate.
///
/// ```
/// # use leptos_reactive::*;
/// # fn really_expensive_computation(value: i32) -> i32 { value };
/// # let runtime = create_runtime();
/// pub struct State {
/// name: String,
/// token: String,
/// }
///
/// let state = create_rw_signal(State {
/// name: "Alice".to_owned(),
/// token: "abcdef".to_owned(),
/// });
///
/// // If we used `create_memo`, we'd need to allocate every time the state changes, but by using
/// // `create_owning_memo` we can allocate only when `state.name` changes.
/// let name = create_owning_memo(move |old_name| {
/// state.with(move |state| {
/// if let Some(name) =
/// old_name.filter(|old_name| old_name == &state.name)
/// {
/// (name, false)
/// } else {
/// (state.name.clone(), true)
/// }
/// })
/// });
/// let set_name = move |name| state.update(|state| state.name = name);
///
/// // We can also re-use the last allocation even when the value changes, which is usually faster,
/// // but may have some caveats (e.g. if the value size is drastically reduced, the memory will
/// // still be used for the life of the memo).
/// let token = create_owning_memo(move |old_token| {
/// state.with(move |state| {
/// let is_different = old_token.as_ref() != Some(&state.token);
/// let mut token = old_token.unwrap_or_else(String::new);
///
/// if is_different {
/// token.clone_from(&state.token);
/// }
/// (token, is_different)
/// })
/// });
/// let set_token = move |new_token| state.update(|state| state.token = new_token);
/// # runtime.dispose();
/// ```
#[cfg_attr(
any(debug_assertions, feature="ssr"),
instrument(
level = "trace",
skip_all,
fields(
ty = %std::any::type_name::<T>()
)
)
)]
#[track_caller]
#[inline(always)]
pub fn create_owning_memo<T>(
f: impl Fn(Option<T>) -> (T, bool) + 'static,
) -> Memo<T>
where
T: 'static,
{
Runtime::current().create_owning_memo(f)
}
/// An efficient derived reactive value based on other reactive values.
///
/// Unlike a "derived signal," a memo comes with two guarantees:
/// 1. The memo will only run *once* per change, no matter how many times you
/// access its value.
/// 2. The memo will only notify its dependents if the value of the computation changes.
///
/// This makes a memo the perfect tool for expensive computations.
///
/// Memos have a certain overhead compared to derived signals. In most cases, you should
/// create a derived signal. But if the derivation calculation is expensive, you should
/// create a memo.
///
/// As with [`create_effect`](crate::create_effect), the argument to the memo function is the previous value,
/// i.e., the current value of the memo, which will be `None` for the initial calculation.
///
/// ## Core Trait Implementations
/// - [`.get()`](#impl-SignalGet<T>-for-Memo<T>) (or calling the signal as a function) clones the current
/// value of the signal. If you call it within an effect, it will cause that effect
/// to subscribe to the signal, and to re-run whenever the value of the signal changes.
/// - [`.get_untracked()`](#impl-SignalGetUntracked<T>-for-Memo<T>) clones the value of the signal
/// without reactively tracking it.
/// - [`.with()`](#impl-SignalWith<T>-for-Memo<T>) allows you to reactively access the signals value without
/// cloning by applying a callback function.
/// - [`.with_untracked()`](#impl-SignalWithUntracked<T>-for-Memo<T>) allows you to access the signals
/// value without reactively tracking it.
/// - [`.to_stream()`](#impl-SignalStream<T>-for-Memo<T>) converts the signal to an `async` stream of values.
///
/// ## Examples
/// ```
/// # use leptos_reactive::*;
/// # fn really_expensive_computation(value: i32) -> i32 { value };
/// # let runtime = create_runtime();
/// let (value, set_value) = create_signal(0);
///
/// // 🆗 we could create a derived signal with a simple function
/// let double_value = move || value.get() * 2;
/// set_value.set(2);
/// assert_eq!(double_value(), 4);
///
/// // but imagine the computation is really expensive
/// let expensive = move || really_expensive_computation(value.get()); // lazy: doesn't run until called
/// create_effect(move |_| {
/// // 🆗 run #1: calls `really_expensive_computation` the first time
/// log::debug!("expensive = {}", expensive());
/// });
/// create_effect(move |_| {
/// // ❌ run #2: this calls `really_expensive_computation` a second time!
/// let value = expensive();
/// // do something else...
/// });
///
/// // instead, we create a memo
/// // 🆗 run #1: the calculation runs once immediately
/// let memoized = create_memo(move |_| really_expensive_computation(value.get()));
/// create_effect(move |_| {
/// // 🆗 reads the current value of the memo
/// log::debug!("memoized = {}", memoized.get());
/// });
/// create_effect(move |_| {
/// // ✅ reads the current value **without re-running the calculation**
/// // can be `memoized()` on nightly
/// let value = memoized.get();
/// // do something else...
/// });
/// # runtime.dispose();
/// ```
pub struct Memo<T>
where
T: 'static,
{
pub(crate) id: NodeId,
pub(crate) ty: PhantomData<T>,
#[cfg(any(debug_assertions, feature = "ssr"))]
pub(crate) defined_at: &'static std::panic::Location<'static>,
}
impl<T> Memo<T> {
/// Creates a new memo from the given function.
///
/// This is identical to [`create_memo`].
/// ```
/// # use leptos_reactive::*;
/// # fn really_expensive_computation(value: i32) -> i32 { value };
/// # let runtime = create_runtime();
/// let value = RwSignal::new(0);
///
/// // 🆗 we could create a derived signal with a simple function
/// let double_value = move || value.get() * 2;
/// value.set(2);
/// assert_eq!(double_value(), 4);
///
/// // but imagine the computation is really expensive
/// let expensive = move || really_expensive_computation(value.get()); // lazy: doesn't run until called
/// Effect::new(move |_| {
/// // 🆗 run #1: calls `really_expensive_computation` the first time
/// log::debug!("expensive = {}", expensive());
/// });
/// Effect::new(move |_| {
/// // ❌ run #2: this calls `really_expensive_computation` a second time!
/// let value = expensive();
/// // do something else...
/// });
///
/// // instead, we create a memo
/// // 🆗 run #1: the calculation runs once immediately
/// let memoized = Memo::new(move |_| really_expensive_computation(value.get()));
/// Effect::new(move |_| {
/// // 🆗 reads the current value of the memo
/// // can be `memoized()` on nightly
/// log::debug!("memoized = {}", memoized.get());
/// });
/// Effect::new(move |_| {
/// // ✅ reads the current value **without re-running the calculation**
/// let value = memoized.get();
/// // do something else...
/// });
/// # runtime.dispose();
/// ```
#[inline(always)]
#[track_caller]
pub fn new(f: impl Fn(Option<&T>) -> T + 'static) -> Memo<T>
where
T: PartialEq + 'static,
{
create_memo(f)
}
/// Creates a new owning memo from the given function.
///
/// This is identical to [`create_owning_memo`].
///
/// ```
/// # use leptos_reactive::*;
/// # fn really_expensive_computation(value: i32) -> i32 { value };
/// # let runtime = create_runtime();
/// pub struct State {
/// name: String,
/// token: String,
/// }
///
/// let state = RwSignal::new(State {
/// name: "Alice".to_owned(),
/// token: "abcdef".to_owned(),
/// });
///
/// // If we used `Memo::new`, we'd need to allocate every time the state changes, but by using
/// // `Memo::new_owning` we can allocate only when `state.name` changes.
/// let name = Memo::new_owning(move |old_name| {
/// state.with(move |state| {
/// if let Some(name) =
/// old_name.filter(|old_name| old_name == &state.name)
/// {
/// (name, false)
/// } else {
/// (state.name.clone(), true)
/// }
/// })
/// });
/// let set_name = move |name| state.update(|state| state.name = name);
///
/// // We can also re-use the last allocation even when the value changes, which is usually faster,
/// // but may have some caveats (e.g. if the value size is drastically reduced, the memory will
/// // still be used for the life of the memo).
/// let token = Memo::new_owning(move |old_token| {
/// state.with(move |state| {
/// let is_different = old_token.as_ref() != Some(&state.token);
/// let mut token = old_token.unwrap_or_else(String::new);
///
/// if is_different {
/// token.clone_from(&state.token);
/// }
/// (token, is_different)
/// })
/// });
/// let set_token = move |new_token| state.update(|state| state.token = new_token);
/// # runtime.dispose();
/// ```
#[inline(always)]
#[track_caller]
pub fn new_owning(f: impl Fn(Option<T>) -> (T, bool) + 'static) -> Memo<T>
where
T: 'static,
{
create_owning_memo(f)
}
}
impl<T> Clone for Memo<T>
where
T: 'static,
{
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for Memo<T> {}
impl<T> fmt::Debug for Memo<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut s = f.debug_struct("Memo");
s.field("id", &self.id);
s.field("ty", &self.ty);
#[cfg(any(debug_assertions, feature = "ssr"))]
s.field("defined_at", &self.defined_at);
s.finish()
}
}
impl<T> Eq for Memo<T> {}
impl<T> PartialEq for Memo<T> {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
fn forward_ref_to<T, O, F: FnOnce(&T) -> O>(
f: F,
) -> impl FnOnce(&Option<T>) -> O {
|maybe_value: &Option<T>| {
let ref_t = maybe_value
.as_ref()
.expect("invariant: must have already been initialized");
f(ref_t)
}
}
impl<T: Clone> SignalGetUntracked for Memo<T> {
type Value = T;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "Memo::get_untracked()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
fn get_untracked(&self) -> T {
with_runtime(move |runtime| {
let f = |maybe_value: &Option<T>| {
maybe_value
.clone()
.expect("invariant: must have already been initialized")
};
match self.id.try_with_no_subscription(runtime, f) {
Ok(t) => t,
Err(_) => panic_getting_dead_memo(
#[cfg(any(debug_assertions, feature = "ssr"))]
self.defined_at,
),
}
})
.expect("runtime to be alive")
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "Memo::try_get_untracked()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
#[inline(always)]
fn try_get_untracked(&self) -> Option<T> {
self.try_with_untracked(T::clone)
}
}
impl<T> SignalWithUntracked for Memo<T> {
type Value = T;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "Memo::with_untracked()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
fn with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> O {
with_runtime(|runtime| {
match self.id.try_with_no_subscription(runtime, forward_ref_to(f)) {
Ok(t) => t,
Err(_) => panic_getting_dead_memo(
#[cfg(any(debug_assertions, feature = "ssr"))]
self.defined_at,
),
}
})
.expect("runtime to be alive")
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "Memo::try_with_untracked()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
#[inline]
fn try_with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
with_runtime(|runtime| {
self.id
.try_with_no_subscription(runtime, |v: &Option<T>| {
v.as_ref().map(f)
})
.ok()
.flatten()
})
.ok()
.flatten()
}
}
/// # Examples
///
/// ```
/// # use leptos_reactive::*;
/// # let runtime = create_runtime();
/// let (count, set_count) = create_signal(0);
/// let double_count = create_memo(move |_| count.get() * 2);
///
/// assert_eq!(double_count.get(), 0);
/// set_count.set(1);
///
/// // can be `double_count()` on nightly
/// // assert_eq!(double_count(), 2);
/// assert_eq!(double_count.get(), 2);
/// # runtime.dispose();
/// #
/// ```
impl<T: Clone> SignalGet for Memo<T> {
type Value = T;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
name = "Memo::get()",
level = "trace",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
#[track_caller]
#[inline(always)]
fn get(&self) -> T {
self.with(T::clone)
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "Memo::try_get()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
#[track_caller]
#[inline(always)]
fn try_get(&self) -> Option<T> {
self.try_with(T::clone)
}
}
impl<T> SignalWith for Memo<T> {
type Value = T;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "Memo::with()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
#[track_caller]
fn with<O>(&self, f: impl FnOnce(&T) -> O) -> O {
match self.try_with(f) {
Some(t) => t,
None => panic_getting_dead_memo(
#[cfg(any(debug_assertions, feature = "ssr"))]
self.defined_at,
),
}
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "Memo::try_with()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
#[track_caller]
fn try_with<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
let diagnostics = diagnostics!(self);
with_runtime(|runtime| {
self.id.subscribe(runtime, diagnostics);
self.id
.try_with_no_subscription(runtime, forward_ref_to(f))
.ok()
})
.ok()
.flatten()
}
}
impl<T: Clone> SignalStream<T> for Memo<T> {
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "Memo::to_stream()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
fn to_stream(&self) -> std::pin::Pin<Box<dyn futures::Stream<Item = T>>> {
let (tx, rx) = futures::channel::mpsc::unbounded();
let close_channel = tx.clone();
on_cleanup(move || close_channel.close_channel());
let this = *self;
create_isomorphic_effect(move |_| {
let _ = tx.unbounded_send(this.get());
});
Box::pin(rx)
}
}
impl<T> SignalDispose for Memo<T> {
fn dispose(self) {
_ = with_runtime(|runtime| runtime.dispose_node(self.id));
}
}
impl_get_fn_traits![Memo];
pub(crate) struct MemoState<T, F>
where
T: 'static,
F: Fn(Option<T>) -> (T, bool),
{
pub f: F,
pub t: PhantomData<T>,
#[cfg(any(debug_assertions, feature = "ssr"))]
pub(crate) defined_at: &'static std::panic::Location<'static>,
}
impl<T, F> AnyComputation for MemoState<T, F>
where
T: 'static,
F: Fn(Option<T>) -> (T, bool),
{
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
name = "Memo::run()",
level = "trace",
skip_all,
fields(
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
fn run(&self, value: Rc<RefCell<dyn Any>>) -> bool {
let mut value = value.borrow_mut();
let curr_value = value
.downcast_mut::<Option<T>>()
.expect("to downcast memo value");
// run the memo
let (new_value, is_different) = (self.f)(curr_value.take());
// set new value
*curr_value = Some(new_value);
is_different
}
}
#[cold]
#[inline(never)]
#[track_caller]
fn format_memo_warning(
msg: &str,
#[cfg(any(debug_assertions, feature = "ssr"))]
defined_at: &'static std::panic::Location<'static>,
) -> String {
let location = std::panic::Location::caller();
let defined_at_msg = {
#[cfg(any(debug_assertions, feature = "ssr"))]
{
format!("signal created here: {defined_at}\n")
}
#[cfg(not(any(debug_assertions, feature = "ssr")))]
{
String::default()
}
};
format!("{msg}\n{defined_at_msg}warning happened here: {location}",)
}
#[cold]
#[inline(never)]
#[track_caller]
pub(crate) fn panic_getting_dead_memo(
#[cfg(any(debug_assertions, feature = "ssr"))]
defined_at: &'static std::panic::Location<'static>,
) -> ! {
panic!(
"{}",
format_memo_warning(
"Attempted to get a memo after it was disposed.",
#[cfg(any(debug_assertions, feature = "ssr"))]
defined_at,
)
)
}

View file

@ -1,51 +0,0 @@
use crate::{with_runtime, AnyComputation};
use std::{any::Any, cell::RefCell, rc::Rc};
slotmap::new_key_type! {
/// Unique ID assigned to a signal.
pub struct NodeId;
}
/// Handle to dispose of a reactive node.
#[derive(Debug, PartialEq, Eq)]
pub struct Disposer(pub(crate) NodeId);
impl Drop for Disposer {
fn drop(&mut self) {
let id = self.0;
_ = with_runtime(|runtime| runtime.dispose_node(id));
}
}
#[derive(Clone)]
pub(crate) struct ReactiveNode {
pub value: Option<Rc<RefCell<dyn Any>>>,
pub state: ReactiveNodeState,
pub node_type: ReactiveNodeType,
}
impl ReactiveNode {
pub fn value(&self) -> Rc<RefCell<dyn Any>> {
self.value
.clone()
.expect("ReactiveNode.value to have a value")
}
}
#[derive(Clone)]
pub(crate) enum ReactiveNodeType {
Trigger,
Signal,
Memo { f: Rc<dyn AnyComputation> },
Effect { f: Rc<dyn AnyComputation> },
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub(crate) enum ReactiveNodeState {
Clean,
Check,
Dirty,
/// Dirty and Marked as visited
DirtyMarked,
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,189 +0,0 @@
use crate::{
create_isomorphic_effect, create_rw_signal, runtime::with_owner, Owner,
RwSignal, SignalUpdate, SignalWith,
};
use std::{cell::RefCell, collections::HashMap, hash::Hash, rc::Rc};
/// Creates a conditional signal that only notifies subscribers when a change
/// in the source signals value changes whether it is equal to the key value
/// (as determined by [`PartialEq`].)
///
/// **You probably dont need this,** but it can be a very useful optimization
/// in certain situations (e.g., “set the class `selected` if `selected() == this_row_index`)
/// because it reduces them from `O(n)` to `O(1)`.
///
/// ```
/// # use leptos_reactive::*;
/// # use std::rc::Rc;
/// # use std::cell::RefCell;
/// # let runtime = create_runtime();
/// let (a, set_a) = create_signal(0);
/// let is_selected = create_selector(move || a.get());
/// let total_notifications = Rc::new(RefCell::new(0));
/// let not = Rc::clone(&total_notifications);
/// create_isomorphic_effect({
/// let is_selected = is_selected.clone();
/// move |_| {
/// if is_selected.selected(5) {
/// *not.borrow_mut() += 1;
/// }
/// }
/// });
///
/// assert_eq!(is_selected.selected(5), false);
/// assert_eq!(*total_notifications.borrow(), 0);
/// set_a.set(5);
/// assert_eq!(is_selected.selected(5), true);
/// assert_eq!(*total_notifications.borrow(), 1);
/// set_a.set(5);
/// assert_eq!(is_selected.selected(5), true);
/// assert_eq!(*total_notifications.borrow(), 1);
/// set_a.set(4);
/// assert_eq!(is_selected.selected(5), false);
/// # runtime.dispose()
/// ```
#[inline(always)]
pub fn create_selector<T>(
source: impl Fn() -> T + Clone + 'static,
) -> Selector<T>
where
T: PartialEq + Eq + Clone + Hash + 'static,
{
create_selector_with_fn(source, PartialEq::eq)
}
/// Creates a conditional signal that only notifies subscribers when a change
/// in the source signals value changes whether the given function is true.
///
/// **You probably dont need this,** but it can be a very useful optimization
/// in certain situations (e.g., “set the class `selected` if `selected() == this_row_index`)
/// because it reduces them from `O(n)` to `O(1)`.
pub fn create_selector_with_fn<T>(
source: impl Fn() -> T + 'static,
f: impl Fn(&T, &T) -> bool + Clone + 'static,
) -> Selector<T>
where
T: PartialEq + Eq + Clone + Hash + 'static,
{
#[allow(clippy::type_complexity)]
let subs: Rc<RefCell<HashMap<T, RwSignal<bool>>>> =
Rc::new(RefCell::new(HashMap::new()));
let v = Rc::new(RefCell::new(None));
let owner = Owner::current()
.expect("create_selector called outside the reactive system");
let f = Rc::new(f) as Rc<dyn Fn(&T, &T) -> bool>;
create_isomorphic_effect({
let subs = Rc::clone(&subs);
let f = Rc::clone(&f);
let v = Rc::clone(&v);
move |prev: Option<T>| {
let next_value = source();
*v.borrow_mut() = Some(next_value.clone());
if prev.as_ref() != Some(&next_value) {
let subs = { subs.borrow().clone() };
for (key, signal) in subs.into_iter() {
if f(&key, &next_value)
|| (prev.is_some() && f(&key, prev.as_ref().unwrap()))
{
signal.update(|n| *n = true);
}
}
}
next_value
}
});
Selector { subs, v, owner, f }
}
/// A conditional signal that only notifies subscribers when a change
/// in the source signals value changes whether the given function is true.
#[derive(Clone)]
pub struct Selector<T>
where
T: PartialEq + Eq + Clone + Hash + 'static,
{
subs: Rc<RefCell<HashMap<T, RwSignal<bool>>>>,
v: Rc<RefCell<Option<T>>>,
owner: Owner,
#[allow(clippy::type_complexity)] // lol
f: Rc<dyn Fn(&T, &T) -> bool>,
}
impl<T> core::fmt::Debug for Selector<T>
where
T: PartialEq + Eq + Clone + Hash + 'static,
{
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Selector").finish()
}
}
impl<T> Selector<T>
where
T: PartialEq + Eq + Clone + Hash + 'static,
{
/// Creates a conditional signal that only notifies subscribers when a change
/// in the source signals value changes whether it is equal to the key value
/// (as determined by [`PartialEq`].)
///
/// **You probably dont need this,** but it can be a very useful optimization
/// in certain situations (e.g., “set the class `selected` if `selected() == this_row_index`)
/// because it reduces them from `O(n)` to `O(1)`.
///
/// ```
/// # use leptos_reactive::*;
/// # use std::rc::Rc;
/// # use std::cell::RefCell;
/// # let runtime = create_runtime();
/// let a = RwSignal::new(0);
/// let is_selected = Selector::new(move || a.get());
/// let total_notifications = Rc::new(RefCell::new(0));
/// let not = Rc::clone(&total_notifications);
/// create_isomorphic_effect({
/// let is_selected = is_selected.clone();
/// move |_| {
/// if is_selected.selected(5) {
/// *not.borrow_mut() += 1;
/// }
/// }
/// });
///
/// assert_eq!(is_selected.selected(5), false);
/// assert_eq!(*total_notifications.borrow(), 0);
/// a.set(5);
/// assert_eq!(is_selected.selected(5), true);
/// assert_eq!(*total_notifications.borrow(), 1);
/// a.set(5);
/// assert_eq!(is_selected.selected(5), true);
/// assert_eq!(*total_notifications.borrow(), 1);
/// a.set(4);
/// assert_eq!(is_selected.selected(5), false);
/// # runtime.dispose()
/// ```
#[inline(always)]
#[track_caller]
pub fn new(source: impl Fn() -> T + Clone + 'static) -> Self {
create_selector_with_fn(source, PartialEq::eq)
}
/// Reactively checks whether the given key is selected.
pub fn selected(&self, key: T) -> bool {
let owner = self.owner;
let read = {
let mut subs = self.subs.borrow_mut();
*(subs.entry(key.clone()).or_insert_with(|| {
with_owner(owner, || create_rw_signal(false))
}))
};
_ = read.try_with(|n| *n);
(self.f)(&key, self.v.borrow().as_ref().unwrap())
}
/// Removes the listener for the given key.
pub fn remove(&self, key: &T) {
let mut subs = self.subs.borrow_mut();
subs.remove(key);
}
}

View file

@ -1,91 +0,0 @@
use crate::{
create_rw_signal, MaybeProp, MaybeSignal, Memo, ReadSignal, RwSignal,
Signal, SignalGet, SignalWith,
};
use serde::{Deserialize, Serialize};
/* Serialization for signal types */
impl<T: Serialize> Serialize for ReadSignal<T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.with(|value| value.serialize(serializer))
}
}
impl<T: Serialize> Serialize for RwSignal<T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.with(|value| value.serialize(serializer))
}
}
impl<T: Serialize> Serialize for Memo<T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.with(|value| value.serialize(serializer))
}
}
impl<T: Serialize> Serialize for MaybeSignal<T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.with(|value| value.serialize(serializer))
}
}
impl<T: Serialize> Serialize for MaybeProp<T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match &self.0 {
None | Some(MaybeSignal::Static(None)) => {
None::<T>.serialize(serializer)
}
Some(MaybeSignal::Static(Some(value))) => {
value.serialize(serializer)
}
Some(MaybeSignal::Dynamic(signal)) => {
signal.with(|value| value.serialize(serializer))
}
}
}
}
impl<T: Clone + Serialize> Serialize for Signal<T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.get().serialize(serializer)
}
}
/* Deserialization for signal types */
impl<'de, T: Deserialize<'de>> Deserialize<'de> for RwSignal<T> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
T::deserialize(deserializer).map(create_rw_signal)
}
}
impl<'de, T: Deserialize<'de>> Deserialize<'de> for MaybeSignal<T> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
T::deserialize(deserializer).map(MaybeSignal::Static)
}
}

View file

@ -1,121 +0,0 @@
use cfg_if::cfg_if;
use std::rc::Rc;
use thiserror::Error;
/// Describes errors that can occur while serializing and deserializing data,
/// typically during the process of streaming [`Resource`](crate::Resource)s from
/// the server to the client.
#[derive(Debug, Clone, Error)]
pub enum SerializationError {
/// Errors that occur during serialization.
#[error("error serializing Resource: {0}")]
Serialize(Rc<dyn std::error::Error>),
/// Errors that occur during deserialization.
#[error("error deserializing Resource: {0}")]
Deserialize(Rc<dyn std::error::Error>),
}
/// Describes an object that can be serialized to or from a supported format
/// Currently those are JSON and Cbor
///
/// This is primarily used for serializing and deserializing [`Resource`](crate::Resource)s
/// so they can begin on the server and be resolved on the client, but can be used
/// for any data that needs to be serialized/deserialized.
///
/// This trait is intended to abstract over various serialization crates,
/// as selected between by the crate features `serde` (default), `serde-lite`,
/// and `miniserde`.
pub trait Serializable
where
Self: Sized,
{
/// Serializes the object to a string.
fn ser(&self) -> Result<String, SerializationError>;
/// Deserializes the object from some bytes.
fn de(bytes: &str) -> Result<Self, SerializationError>;
}
cfg_if! {
if #[cfg(feature = "rkyv")] {
use rkyv::{Archive, CheckBytes, Deserialize, Serialize, ser::serializers::AllocSerializer, de::deserializers::SharedDeserializeMap, validation::validators::DefaultValidator};
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD_NO_PAD;
impl<T> Serializable for T
where
T: Serialize<AllocSerializer<1024>>,
T: Archive,
T::Archived: for<'b> CheckBytes<DefaultValidator<'b>> + Deserialize<T, SharedDeserializeMap>,
{
fn ser(&self) -> Result<String, SerializationError> {
let bytes = rkyv::to_bytes::<T, 1024>(self).map_err(|e| SerializationError::Serialize(Rc::new(e)))?;
Ok(STANDARD_NO_PAD.encode(bytes))
}
fn de(serialized: &str) -> Result<Self, SerializationError> {
let bytes = STANDARD_NO_PAD.decode(serialized.as_bytes()).map_err(|e| SerializationError::Deserialize(Rc::new(e)))?;
rkyv::from_bytes::<T>(&bytes).map_err(|e| SerializationError::Deserialize(Rc::new(e)))
}
}
}
// prefer miniserde if it's chosen
else if #[cfg(feature = "miniserde")] {
use miniserde::{json, Deserialize, Serialize};
impl<T> Serializable for T
where
T: Serialize + Deserialize,
{
fn ser(&self) -> Result<String, SerializationError> {
Ok(json::to_string(&self))
}
fn de(json: &str) -> Result<Self, SerializationError> {
json::from_str(json).map_err(|e| SerializationError::Deserialize(Rc::new(e)))
}
}
}
// use serde-lite if enabled
else if #[cfg(feature = "serde-lite")] {
use serde_lite::{Deserialize, Serialize};
impl<T> Serializable for T
where
T: Serialize + Deserialize,
{
fn ser(&self) -> Result<String, SerializationError> {
let intermediate = self
.serialize()
.map_err(|e| SerializationError::Serialize(Rc::new(e)))?;
serde_json::to_string(&intermediate).map_err(|e| SerializationError::Serialize(Rc::new(e)))
}
fn de(json: &str) -> Result<Self, SerializationError> {
let intermediate =
serde_json::from_str(json).map_err(|e| SerializationError::Deserialize(Rc::new(e)))?;
Self::deserialize(&intermediate).map_err(|e| SerializationError::Deserialize(Rc::new(e)))
}
}
}
// otherwise, or if serde is chosen, default to serde
else {
use serde::{de::DeserializeOwned, Serialize};
impl<T> Serializable for T
where
T: DeserializeOwned + Serialize,
{
fn ser(&self) -> Result<String, SerializationError> {
serde_json::to_string(&self).map_err(|e| SerializationError::Serialize(Rc::new(e)))
}
fn de(json: &str) -> Result<Self, SerializationError> {
serde_json::from_str(json).map_err(|e| SerializationError::Deserialize(Rc::new(e)))
}
}
}
}

View file

@ -1,28 +0,0 @@
use crate::children::{ChildrenFn, ViewFn};
use tachy_maccy::component;
use tachy_reaccy::{memo::ArcMemo, signal_traits::SignalGet};
use tachydom::{
renderer::dom::Dom,
view::{either::Either, RenderHtml},
};
#[component]
pub fn Show<W>(
/// The children will be shown whenever the condition in the `when` closure returns `true`.
children: ChildrenFn,
/// A closure that returns a bool that determines whether this thing runs
when: W,
/// A closure that returns what gets rendered if the when statement is false. By default this is the empty view.
#[prop(optional, into)]
fallback: ViewFn,
) -> impl RenderHtml<Dom>
where
W: Fn() -> bool + Send + Sync + 'static,
{
let memoized_when = ArcMemo::new(move |_| when());
move || match memoized_when.get() {
true => Either::Left(children()),
false => Either::Right(fallback.run()),
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,267 +0,0 @@
use crate::{store_value, RwSignal, SignalSet, StoredValue, WriteSignal};
/// Helper trait for converting `Fn(T)` into [`SignalSetter<T>`].
pub trait IntoSignalSetter<T>: Sized {
/// Consumes `self`, returning [`SignalSetter<T>`].
#[deprecated = "Will be removed in `leptos v0.6`. Please use \
`IntoSignalSetter::into_signal_setter()` instead."]
fn mapped_signal_setter(self) -> SignalSetter<T>;
/// Consumes `self`, returning [`SignalSetter<T>`].
fn into_signal_setter(self) -> SignalSetter<T>;
}
impl<F, T> IntoSignalSetter<T> for F
where
F: Fn(T) + 'static,
{
fn mapped_signal_setter(self) -> SignalSetter<T> {
self.into_signal_setter()
}
fn into_signal_setter(self) -> SignalSetter<T> {
SignalSetter::map(self)
}
}
/// A wrapper for any kind of settable reactive signal: a [`WriteSignal`](crate::WriteSignal),
/// [`RwSignal`](crate::RwSignal), or closure that receives a value and sets a signal depending
/// on it.
///
/// This allows you to create APIs that take any kind of `SignalSetter<T>` as an argument,
/// rather than adding a generic `F: Fn(T)`. Values can be set with the same
/// function call or `set()`, API as other signals.
///
/// ## Core Trait Implementations
/// - [`.set()`](#impl-SignalSet<T>-for-SignalSetter<T>) (or calling the setter as a function)
/// sets the signals value, and notifies all subscribers that the signals value has changed.
/// to subscribe to the signal, and to re-run whenever the value of the signal changes.
///
/// ## Examples
/// ```rust
/// # use leptos_reactive::*;
/// # let runtime = create_runtime();
/// let (count, set_count) = create_signal(2);
/// let set_double_input = SignalSetter::map(move |n| set_count.set(n * 2));
///
/// // this function takes any kind of signal setter
/// fn set_to_4(setter: &SignalSetter<i32>) {
/// // ✅ calling the signal sets the value
/// // can be `setter(4)` on nightly
/// setter.set(4);
/// }
///
/// set_to_4(&set_count.into());
/// assert_eq!(count.get(), 4);
/// set_to_4(&set_double_input);
/// assert_eq!(count.get(), 8);
/// # runtime.dispose();
/// ```
#[derive(Debug, PartialEq, Eq)]
pub struct SignalSetter<T>
where
T: 'static,
{
inner: SignalSetterTypes<T>,
#[cfg(any(debug_assertions, feature = "ssr"))]
defined_at: &'static std::panic::Location<'static>,
}
impl<T> Clone for SignalSetter<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T: Default + 'static> Default for SignalSetter<T> {
#[track_caller]
fn default() -> Self {
Self {
inner: SignalSetterTypes::Default,
#[cfg(any(debug_assertions, feature = "ssr"))]
defined_at: std::panic::Location::caller(),
}
}
}
impl<T> Copy for SignalSetter<T> {}
impl<T> SignalSet for SignalSetter<T> {
type Value = T;
fn set(&self, new_value: T) {
match self.inner {
SignalSetterTypes::Default => {}
SignalSetterTypes::Write(w) => w.set(new_value),
SignalSetterTypes::Mapped(s) => {
s.with_value(|setter| setter(new_value))
}
}
}
fn try_set(&self, new_value: T) -> Option<T> {
match self.inner {
SignalSetterTypes::Default => Some(new_value),
SignalSetterTypes::Write(w) => w.try_set(new_value),
SignalSetterTypes::Mapped(s) => {
let mut new_value = Some(new_value);
let _ = s
.try_with_value(|setter| setter(new_value.take().unwrap()));
new_value
}
}
}
}
impl<T> SignalSetter<T>
where
T: 'static,
{
/// Wraps a signal-setting closure, i.e., any computation that sets one or more
/// reactive signals.
/// ```rust
/// # use leptos_reactive::*;
/// # let runtime = create_runtime();
/// let (count, set_count) = create_signal(2);
/// let set_double_count = SignalSetter::map(move |n| set_count.set(n * 2));
///
/// // this function takes any kind of signal setter
/// fn set_to_4(setter: &SignalSetter<i32>) {
/// // ✅ calling the signal sets the value
/// // can be `setter(4)` on nightly
/// setter.set(4)
/// }
///
/// set_to_4(&set_count.into());
/// assert_eq!(count.get(), 4);
/// set_to_4(&set_double_count);
/// assert_eq!(count.get(), 8);
/// # runtime.dispose();
/// ```
#[track_caller]
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all)
)]
pub fn map(mapped_setter: impl Fn(T) + 'static) -> Self {
Self {
inner: SignalSetterTypes::Mapped(store_value(Box::new(
mapped_setter,
))),
#[cfg(any(debug_assertions, feature = "ssr"))]
defined_at: std::panic::Location::caller(),
}
}
/// Calls the setter function with the given value.
///
/// ```rust
/// # use leptos_reactive::*;
/// # let runtime = create_runtime();
/// let (count, set_count) = create_signal(2);
/// let set_double_count = SignalSetter::map(move |n| set_count.set(n * 2));
///
/// // this function takes any kind of signal setter
/// fn set_to_4(setter: &SignalSetter<i32>) {
/// // ✅ calling the signal sets the value
/// // can be `setter(4)` on nightly
/// setter.set(4);
/// }
///
/// set_to_4(&set_count.into());
/// assert_eq!(count.get(), 4);
/// set_to_4(&set_double_count);
/// assert_eq!(count.get(), 8);
/// # runtime.dispose();
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
skip_all,
fields(
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
pub fn set(&self, value: T) {
match &self.inner {
SignalSetterTypes::Write(s) => s.set(value),
SignalSetterTypes::Mapped(s) => s.with_value(|s| s(value)),
SignalSetterTypes::Default => {}
}
}
}
impl<T> From<WriteSignal<T>> for SignalSetter<T> {
#[track_caller]
fn from(value: WriteSignal<T>) -> Self {
Self {
inner: SignalSetterTypes::Write(value),
#[cfg(any(debug_assertions, feature = "ssr"))]
defined_at: std::panic::Location::caller(),
}
}
}
impl<T> From<RwSignal<T>> for SignalSetter<T> {
#[track_caller]
fn from(value: RwSignal<T>) -> Self {
Self {
inner: SignalSetterTypes::Write(value.write_only()),
#[cfg(any(debug_assertions, feature = "ssr"))]
defined_at: std::panic::Location::caller(),
}
}
}
enum SignalSetterTypes<T>
where
T: 'static,
{
Write(WriteSignal<T>),
Mapped(StoredValue<Box<dyn Fn(T)>>),
Default,
}
impl<T> Clone for SignalSetterTypes<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for SignalSetterTypes<T> {}
impl<T> core::fmt::Debug for SignalSetterTypes<T>
where
T: core::fmt::Debug,
{
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Write(arg0) => {
f.debug_tuple("WriteSignal").field(arg0).finish()
}
Self::Mapped(_) => f.debug_tuple("Mapped").finish(),
Self::Default => f.debug_tuple("SignalSetter<Default>").finish(),
}
}
}
impl<T> PartialEq for SignalSetterTypes<T>
where
T: PartialEq,
{
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Write(l0), Self::Write(r0)) => l0 == r0,
(Self::Mapped(l0), Self::Mapped(r0)) => std::ptr::eq(l0, r0),
_ => false,
}
}
}
impl<T> Eq for SignalSetterTypes<T> where T: PartialEq {}
impl_set_fn_traits![SignalSetter];

View file

@ -1,108 +0,0 @@
use crate::{
create_memo, IntoSignalSetter, RwSignal, Signal, SignalSetter,
SignalUpdate, SignalWith,
};
/// Derives a reactive slice of an [`RwSignal`](crate::RwSignal).
///
/// Slices have the same guarantees as [`Memo`s](crate::Memo):
/// they only emit their value when it has actually been changed.
///
/// Slices need a getter and a setter, and you must make sure that
/// the setter and getter only touch their respective field and nothing else.
/// They optimally should not have any side effects.
///
/// You can use slices whenever you want to react to only parts
/// of a bigger signal. The prime example would be state management,
/// where you want all state variables grouped together, but also need
/// fine-grained signals for each or some of these variables.
/// In the example below, setting an auth token will only trigger
/// the token signal, but none of the other derived signals.
/// ```
/// # use leptos_reactive::*;
/// # let runtime = create_runtime();
///
/// // some global state with independent fields
/// #[derive(Default, Clone, Debug)]
/// struct GlobalState {
/// count: u32,
/// name: String,
/// }
///
/// let state = create_rw_signal(GlobalState::default());
///
/// // `create_slice` lets us create a "lens" into the data
/// let (count, set_count) = create_slice(
/// // we take a slice *from* `state`
/// state,
/// // our getter returns a "slice" of the data
/// |state| state.count,
/// // our setter describes how to mutate that slice, given a new value
/// |state, n| state.count = n,
/// );
///
/// // this slice is completely independent of the `count` slice
/// // neither of them will cause the other to rerun
/// let (name, set_name) = create_slice(
/// // we take a slice *from* `state`
/// state,
/// // our getter returns a "slice" of the data
/// |state| state.name.clone(),
/// // our setter describes how to mutate that slice, given a new value
/// |state, n| state.name = n,
/// );
///
/// create_effect(move |_| {
/// // note: in the browser, use leptos::log! instead
/// println!("name is {}", name.get());
/// });
/// create_effect(move |_| {
/// println!("count is {}", count.get());
/// });
///
/// // setting count only causes count to log, not name
/// set_count.set(42);
///
/// // setting name only causes name to log, not count
/// set_name.set("Bob".into());
///
/// # runtime.dispose();
/// ```
#[track_caller]
pub fn create_slice<T, O, S>(
signal: RwSignal<T>,
getter: impl Fn(&T) -> O + Copy + 'static,
setter: impl Fn(&mut T, S) + Copy + 'static,
) -> (Signal<O>, SignalSetter<S>)
where
O: PartialEq,
{
(
create_read_slice(signal, getter),
create_write_slice(signal, setter),
)
}
/// Takes a memoized, read-only slice of a signal. This is equivalent to the
/// read-only half of [`create_slice`].
#[track_caller]
pub fn create_read_slice<T, O>(
signal: RwSignal<T>,
getter: impl Fn(&T) -> O + Copy + 'static,
) -> Signal<O>
where
O: PartialEq,
{
create_memo(move |_| signal.with(getter)).into()
}
/// Creates a setter to access one slice of a signal. This is equivalent to the
/// write-only half of [`create_slice`].
#[track_caller]
pub fn create_write_slice<T, O>(
signal: RwSignal<T>,
setter: impl Fn(&mut T, O) + Copy + 'static,
) -> SignalSetter<O> {
let setter = move |value| signal.update(|x| setter(x, value));
setter.into_signal_setter()
}

View file

@ -1,97 +0,0 @@
use cfg_if::cfg_if;
use std::future::Future;
/// Spawns and runs a thread-local [`Future`] in a platform-independent way.
///
/// This can be used to interface with any `async` code by spawning a task
/// to run a `Future`.
///
/// ## Limitations
///
/// You should not use `spawn_local` to synchronize `async` code with a
/// signals value during server rendering. The server response will not
/// be notified to wait for the spawned task to complete, creating a race
/// condition between the response and your task. Instead, use
/// [`create_resource`](crate::create_resource) and `<Suspense/>` to coordinate
/// asynchronous work with the rendering process.
///
/// ```
/// # use leptos::*;
/// # #[cfg(not(any(feature = "csr", feature = "serde-lite", feature = "miniserde", feature = "rkyv")))]
/// # {
///
/// async fn get_user(user: String) -> Result<String, ServerFnError> {
/// Ok(format!("this user is {user}"))
/// }
///
/// // ❌ Write into a signal from `spawn_local` on the serevr
/// #[component]
/// fn UserBad() -> impl IntoView {
/// let signal = create_rw_signal(String::new());
///
/// // ❌ If the rest of the response is already complete,
/// // `signal` will no longer exist when `get_user` resolves
/// #[cfg(feature = "ssr")]
/// spawn_local(async move {
/// let user_res = get_user("user".into()).await.unwrap_or_default();
/// signal.set(user_res);
/// });
///
/// view! {
/// <p>
/// "This will be empty (hopefully the client will render it) -> "
/// {move || signal.get()}
/// </p>
/// }
/// }
///
/// // ✅ Use a resource and suspense
/// #[component]
/// fn UserGood() -> impl IntoView {
/// // new resource with no dependencies (it will only called once)
/// let user = create_resource(|| (), |_| async { get_user("john".into()).await });
/// view! {
/// // handles the loading
/// <Suspense fallback=move || view! {<p>"Loading User"</p> }>
/// // handles the error from the resource
/// <ErrorBoundary fallback=|_| {view! {<p>"Something went wrong"</p>}}>
/// {move || {
/// user.read().map(move |x| {
/// // the resource has a result
/// x.map(move |y| {
/// // successful call from the server fn
/// view! {<p>"User result filled in server and client: "{y}</p>}
/// })
/// })
/// }}
/// </ErrorBoundary>
/// </Suspense>
/// }
/// }
/// # }
/// ```
pub fn spawn_local<F>(fut: F)
where
F: Future<Output = ()> + 'static,
{
cfg_if! {
if #[cfg(all(target_arch = "wasm32", target_os = "wasi", feature = "ssr", feature = "spin"))] {
spin_sdk::http::run(fut)
}
else if #[cfg(target_arch = "wasm32")] {
wasm_bindgen_futures::spawn_local(fut)
}
else if #[cfg(any(test, doctest))] {
tokio_test::block_on(fut);
} else if #[cfg(feature = "ssr")] {
use crate::Runtime;
let runtime = Runtime::current();
tokio::task::spawn_local(async move {
crate::TASK_RUNTIME.scope(Some(runtime), fut).await
});
} else {
futures::executor::block_on(fut)
}
}
}

View file

@ -1,34 +0,0 @@
/// The microtask is a short function which will run after the current task has
/// completed its work and when there is no other code waiting to be run before
/// control of the execution context is returned to the browser's event loop.
///
/// Microtasks are especially useful for libraries and frameworks that need
/// to perform final cleanup or other just-before-rendering tasks.
///
/// [MDN queueMicrotask](https://developer.mozilla.org/en-US/docs/Web/API/queueMicrotask)
pub fn queue_microtask(task: impl FnOnce() + 'static) {
#[cfg(not(all(
target_arch = "wasm32",
any(feature = "hydrate", feature = "csr")
)))]
{
task();
}
#[cfg(all(
target_arch = "wasm32",
any(feature = "hydrate", feature = "csr")
))]
{
use js_sys::{Function, Reflect};
use wasm_bindgen::prelude::*;
let task = Closure::once_into_js(task);
let window = web_sys::window().expect("window not available");
let queue_microtask =
Reflect::get(&window, &JsValue::from_str("queueMicrotask"))
.expect("queueMicrotask not available");
let queue_microtask = queue_microtask.unchecked_into::<Function>();
_ = queue_microtask.call1(&JsValue::UNDEFINED, &task);
}
}

View file

@ -1,370 +0,0 @@
use crate::{with_runtime, Runtime, ScopeProperty};
use std::{
cell::RefCell,
fmt,
hash::{Hash, Hasher},
marker::PhantomData,
rc::Rc,
};
slotmap::new_key_type! {
/// Unique ID assigned to a [`StoredValue`].
pub(crate) struct StoredValueId;
}
/// A **non-reactive** wrapper for any value, which can be created with [`store_value`].
///
/// If you want a reactive wrapper, use [`create_signal`](crate::create_signal).
///
/// This allows you to create a stable reference for any value by storing it within
/// the reactive system. Like the signal types (e.g., [`ReadSignal`](crate::ReadSignal)
/// and [`RwSignal`](crate::RwSignal)), it is `Copy` and `'static`. Unlike the signal
/// types, it is not reactive; accessing it does not cause effects to subscribe, and
/// updating it does not notify anything else.
pub struct StoredValue<T>
where
T: 'static,
{
id: StoredValueId,
ty: PhantomData<T>,
}
impl<T: Default> Default for StoredValue<T> {
fn default() -> Self {
Self::new(Default::default())
}
}
impl<T> Clone for StoredValue<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for StoredValue<T> {}
impl<T> fmt::Debug for StoredValue<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("StoredValue")
.field("id", &self.id)
.field("ty", &self.ty)
.finish()
}
}
impl<T> Eq for StoredValue<T> {}
impl<T> PartialEq for StoredValue<T> {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl<T> Hash for StoredValue<T> {
fn hash<H: Hasher>(&self, state: &mut H) {
Runtime::current().hash(state);
self.id.hash(state);
}
}
impl<T> StoredValue<T> {
/// Returns a clone of the current stored value.
///
/// # Panics
/// Panics if you try to access a value owned by a reactive node that has been disposed.
///
/// # Examples
/// ```
/// # use leptos_reactive::*;
/// # let runtime = create_runtime();
///
/// #[derive(Clone)]
/// pub struct MyCloneableData {
/// pub value: String,
/// }
/// let data = store_value(MyCloneableData { value: "a".into() });
///
/// // calling .get_value() clones and returns the value
/// assert_eq!(data.get_value().value, "a");
/// // can be `data().value` on nightly
/// // assert_eq!(data().value, "a");
/// # runtime.dispose();
/// ```
#[track_caller]
pub fn get_value(&self) -> T
where
T: Clone,
{
self.try_get_value().expect("could not get stored value")
}
/// Same as [`StoredValue::get_value`] but will not panic by default.
#[track_caller]
pub fn try_get_value(&self) -> Option<T>
where
T: Clone,
{
self.try_with_value(T::clone)
}
/// Applies a function to the current stored value and returns the result.
///
/// # Panics
/// Panics if you try to access a value owned by a reactive node that has been disposed.
///
/// # Examples
/// ```
/// # use leptos_reactive::*;
/// # let runtime = create_runtime();
///
/// pub struct MyUncloneableData {
/// pub value: String,
/// }
/// let data = store_value(MyUncloneableData { value: "a".into() });
///
/// // calling .with_value() to extract the value
/// assert_eq!(data.with_value(|data| data.value.clone()), "a");
/// # runtime.dispose();
/// ```
#[track_caller]
// track the stored value. This method will also be removed in \
// a future version of `leptos`"]
pub fn with_value<U>(&self, f: impl FnOnce(&T) -> U) -> U {
self.try_with_value(f).expect("could not get stored value")
}
/// Same as [`StoredValue::with_value`] but returns [`Some(O)]` only if
/// the stored value has not yet been disposed. [`None`] otherwise.
pub fn try_with_value<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
with_runtime(|runtime| {
let value = {
let values = runtime.stored_values.borrow();
values.get(self.id)?.clone()
};
let value = value.borrow();
let value = value.downcast_ref::<T>()?;
Some(f(value))
})
.ok()
.flatten()
}
/// Updates the stored value.
///
/// # Examples
/// ```
/// # use leptos_reactive::*;
/// # let runtime = create_runtime();
///
/// pub struct MyUncloneableData {
/// pub value: String,
/// }
/// let data = store_value(MyUncloneableData { value: "a".into() });
/// data.update_value(|data| data.value = "b".into());
/// assert_eq!(data.with_value(|data| data.value.clone()), "b");
/// # runtime.dispose();
/// ```
///
/// ```
/// use leptos_reactive::*;
/// # let runtime = create_runtime();
///
/// pub struct MyUncloneableData {
/// pub value: String,
/// }
///
/// let data = store_value(MyUncloneableData { value: "a".into() });
/// let updated = data.try_update_value(|data| {
/// data.value = "b".into();
/// data.value.clone()
/// });
///
/// assert_eq!(data.with_value(|data| data.value.clone()), "b");
/// assert_eq!(updated, Some(String::from("b")));
/// # runtime.dispose();
/// ```
///
/// ## Panics
/// Panics if there is no current reactive runtime, or if the
/// stored value has been disposed.
#[track_caller]
pub fn update_value(&self, f: impl FnOnce(&mut T)) {
self.try_update_value(f)
.expect("could not set stored value");
}
/// Same as [`Self::update_value`], but returns [`Some(O)`] if the
/// stored value has not yet been disposed, [`None`] otherwise.
pub fn try_update_value<O>(self, f: impl FnOnce(&mut T) -> O) -> Option<O> {
with_runtime(|runtime| {
let value = {
let values = runtime.stored_values.borrow();
values.get(self.id)?.clone()
};
let mut value = value.borrow_mut();
let value = value.downcast_mut::<T>()?;
Some(f(value))
})
.ok()
.flatten()
}
/// Disposes of the stored value
pub fn dispose(self) {
_ = with_runtime(|runtime| {
runtime.stored_values.borrow_mut().remove(self.id);
});
}
/// Sets the stored value.
///
/// # Examples
/// ```
/// # use leptos_reactive::*;
/// # let runtime = create_runtime();
///
/// pub struct MyUncloneableData {
/// pub value: String,
/// }
/// let data = store_value(MyUncloneableData { value: "a".into() });
/// data.set_value(MyUncloneableData { value: "b".into() });
/// assert_eq!(data.with_value(|data| data.value.clone()), "b");
/// # runtime.dispose();
/// ```
#[track_caller]
pub fn set_value(&self, value: T) {
self.try_set_value(value);
}
/// Same as [`Self::set_value`], but returns [`None`] if the
/// stored value has not yet been disposed, [`Some(T)`] otherwise.
pub fn try_set_value(&self, value: T) -> Option<T> {
with_runtime(|runtime| {
let n = {
let values = runtime.stored_values.borrow();
values.get(self.id).cloned()
};
if let Some(n) = n {
let mut n = n.borrow_mut();
let n = n.downcast_mut::<T>();
if let Some(n) = n {
*n = value;
None
} else {
Some(value)
}
} else {
Some(value)
}
})
.ok()
.flatten()
}
}
/// Creates a **non-reactive** wrapper for any value by storing it within
/// the reactive system.
///
/// Like the signal types (e.g., [`ReadSignal`](crate::ReadSignal)
/// and [`RwSignal`](crate::RwSignal)), it is `Copy` and `'static`. Unlike the signal
/// types, it is not reactive; accessing it does not cause effects to subscribe, and
/// updating it does not notify anything else.
/// ```compile_fail
/// # use leptos_reactive::*;
/// # let runtime = create_runtime();
/// // this structure is neither `Copy` nor `Clone`
/// pub struct MyUncloneableData {
/// pub value: String
/// }
///
/// // ❌ this won't compile, as it can't be cloned or copied into the closures
/// let data = MyUncloneableData { value: "a".into() };
/// let callback_a = move || data.value == "a";
/// let callback_b = move || data.value == "b";
/// # runtime.dispose();
/// ```
/// ```
/// # use leptos_reactive::*;
/// # let runtime = create_runtime();
/// // this structure is neither `Copy` nor `Clone`
/// pub struct MyUncloneableData {
/// pub value: String,
/// }
///
/// // ✅ you can move the `StoredValue` and access it with .with_value()
/// let data = store_value(MyUncloneableData { value: "a".into() });
/// let callback_a = move || data.with_value(|data| data.value == "a");
/// let callback_b = move || data.with_value(|data| data.value == "b");
/// # runtime.dispose();
/// ```
///
/// ## Panics
/// Panics if there is no current reactive runtime.
#[track_caller]
pub fn store_value<T>(value: T) -> StoredValue<T>
where
T: 'static,
{
let id = with_runtime(|runtime| {
let id = runtime
.stored_values
.borrow_mut()
.insert(Rc::new(RefCell::new(value)));
runtime.push_scope_property(ScopeProperty::StoredValue(id));
id
})
.expect("store_value failed to find the current runtime");
StoredValue {
id,
ty: PhantomData,
}
}
impl<T> StoredValue<T> {
/// Creates a **non-reactive** wrapper for any value by storing it within
/// the reactive system.
///
/// Like the signal types (e.g., [`ReadSignal`](crate::ReadSignal)
/// and [`RwSignal`](crate::RwSignal)), it is `Copy` and `'static`. Unlike the signal
/// types, it is not reactive; accessing it does not cause effects to subscribe, and
/// updating it does not notify anything else.
/// ```compile_fail
/// # use leptos_reactive::*;
/// # let runtime = create_runtime();
/// // this structure is neither `Copy` nor `Clone`
/// pub struct MyUncloneableData {
/// pub value: String
/// }
///
/// // ❌ this won't compile, as it can't be cloned or copied into the closures
/// let data = MyUncloneableData { value: "a".into() };
/// let callback_a = move || data.value == "a";
/// let callback_b = move || data.value == "b";
/// # runtime.dispose();
/// ```
/// ```
/// # use leptos_reactive::*;
/// # let runtime = create_runtime();
/// // this structure is neither `Copy` nor `Clone`
/// pub struct MyUncloneableData {
/// pub value: String,
/// }
///
/// // ✅ you can move the `StoredValue` and access it with .with_value()
/// let data = StoredValue::new(MyUncloneableData { value: "a".into() });
/// let callback_a = move || data.with_value(|data| data.value == "a");
/// let callback_b = move || data.with_value(|data| data.value == "b");
/// # runtime.dispose();
/// ```
///
/// ## Panics
/// Panics if there is no current reactive runtime.
#[inline(always)]
#[track_caller]
pub fn new(value: T) -> Self {
store_value(value)
}
}
impl_get_fn_traits!(StoredValue(get_value));

View file

@ -1,301 +0,0 @@
//! Types that handle asynchronous data loading via `<Suspense/>`.
use crate::{
batch, create_isomorphic_effect, create_memo, create_rw_signal,
create_signal, oco::Oco, queue_microtask, store_value, Memo, ReadSignal,
ResourceId, RwSignal, SignalSet, SignalUpdate, SignalWith, StoredValue,
WriteSignal,
};
use futures::Future;
use rustc_hash::FxHashSet;
use std::{cell::RefCell, collections::VecDeque, pin::Pin, rc::Rc};
/// Tracks [`Resource`](crate::Resource)s that are read under a suspense context,
/// i.e., within a [`Suspense`](https://docs.rs/leptos_core/latest/leptos_core/fn.Suspense.html) component.
#[derive(Copy, Clone, Debug)]
pub struct SuspenseContext {
/// The number of resources that are currently pending.
pub pending_resources: ReadSignal<usize>,
set_pending_resources: WriteSignal<usize>,
// NOTE: For correctness reasons, we really need to move to this
// However, for API stability reasons, I need to keep the counter-incrementing version too
pub(crate) pending: RwSignal<FxHashSet<ResourceId>>,
pub(crate) pending_serializable_resources: RwSignal<FxHashSet<ResourceId>>,
pub(crate) pending_serializable_resources_count: RwSignal<usize>,
pub(crate) local_status: StoredValue<Option<LocalStatus>>,
pub(crate) should_block: StoredValue<bool>,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub(crate) enum LocalStatus {
LocalOnly,
Mixed,
SerializableOnly,
}
/// A single, global suspense context that will be checked when resources
/// are read. This wont be “blocked” by lower suspense components. This is
/// useful for e.g., holding route transitions.
#[derive(Clone, Debug)]
pub struct GlobalSuspenseContext(Rc<RefCell<SuspenseContext>>);
impl GlobalSuspenseContext {
/// Creates an empty global suspense context.
pub fn new() -> Self {
Self(Rc::new(RefCell::new(SuspenseContext::new())))
}
/// Runs a function with a reference to the underlying suspense context.
pub fn with_inner<T>(&self, f: impl FnOnce(&SuspenseContext) -> T) -> T {
f(&self.0.borrow())
}
/// Runs a function with a reference to the underlying suspense context.
pub fn reset(&self) {
let mut inner = self.0.borrow_mut();
_ = std::mem::replace(&mut *inner, SuspenseContext::new());
}
}
impl Default for GlobalSuspenseContext {
fn default() -> Self {
Self::new()
}
}
impl SuspenseContext {
/// Whether the suspense contains local resources at this moment,
/// and therefore can't be serialized
pub fn has_local_only(&self) -> bool {
matches!(self.local_status.get_value(), Some(LocalStatus::LocalOnly))
}
/// Whether the suspense contains any local resources at this moment.
pub fn has_any_local(&self) -> bool {
matches!(
self.local_status.get_value(),
Some(LocalStatus::LocalOnly) | Some(LocalStatus::Mixed)
)
}
/// Whether any blocking resources are read under this suspense context,
/// meaning the HTML stream should not begin until it has resolved.
pub fn should_block(&self) -> bool {
self.should_block.get_value()
}
/// Returns a `Future` that resolves when this suspense is resolved.
pub fn to_future(&self) -> impl Future<Output = ()> {
use futures::StreamExt;
let pending = self.pending;
let (tx, mut rx) = futures::channel::mpsc::channel(1);
let tx = RefCell::new(tx);
queue_microtask(move || {
create_isomorphic_effect(move |_| {
if pending.with(|p| p.is_empty()) {
_ = tx.borrow_mut().try_send(());
}
});
});
async move {
rx.next().await;
}
}
/// Reactively checks whether there are no pending resources in the suspense.
pub fn none_pending(&self) -> bool {
self.pending.with(|p| p.is_empty())
}
}
impl std::hash::Hash for SuspenseContext {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.pending.id.hash(state);
}
}
impl PartialEq for SuspenseContext {
fn eq(&self, other: &Self) -> bool {
self.pending.id == other.pending.id
}
}
impl Eq for SuspenseContext {}
impl SuspenseContext {
/// Creates an empty suspense context.
pub fn new() -> Self {
let (pending_resources, set_pending_resources) = create_signal(0); // can be removed when possible
let pending_serializable_resources =
create_rw_signal(Default::default());
let pending_serializable_resources_count = create_rw_signal(0); // can be removed when possible
let local_status = store_value(None);
let should_block = store_value(false);
let pending = create_rw_signal(Default::default());
Self {
pending,
pending_resources,
set_pending_resources,
pending_serializable_resources,
pending_serializable_resources_count,
local_status,
should_block,
}
}
/// Notifies the suspense context that a new resource is now pending.
pub fn increment(&self, serializable: bool) {
let setter = self.set_pending_resources;
let serializable_resources = self.pending_serializable_resources_count;
let local_status = self.local_status;
setter.update(|n| *n += 1);
if serializable {
serializable_resources.update(|n| *n += 1);
local_status.update_value(|status| {
*status = Some(match status {
None => LocalStatus::SerializableOnly,
Some(LocalStatus::LocalOnly) => LocalStatus::LocalOnly,
Some(LocalStatus::Mixed) => LocalStatus::Mixed,
Some(LocalStatus::SerializableOnly) => {
LocalStatus::SerializableOnly
}
});
});
} else {
local_status.update_value(|status| {
*status = Some(match status {
None => LocalStatus::LocalOnly,
Some(LocalStatus::LocalOnly) => LocalStatus::LocalOnly,
Some(LocalStatus::Mixed) => LocalStatus::Mixed,
Some(LocalStatus::SerializableOnly) => LocalStatus::Mixed,
});
});
}
}
/// Notifies the suspense context that a resource has resolved.
pub fn decrement(&self, serializable: bool) {
let setter = self.set_pending_resources;
let serializable_resources = self.pending_serializable_resources_count;
setter.update(|n| {
if *n > 0 {
*n -= 1
}
});
if serializable {
serializable_resources.update(|n| {
if *n > 0 {
*n -= 1;
}
});
}
}
/// Notifies the suspense context that a new resource is now pending.
pub(crate) fn increment_for_resource(
&self,
serializable: bool,
resource: ResourceId,
) {
let pending = self.pending;
let serializable_resources = self.pending_serializable_resources;
let local_status = self.local_status;
batch(move || {
pending.update(|n| {
n.insert(resource);
});
if serializable {
serializable_resources.update(|n| {
n.insert(resource);
});
local_status.update_value(|status| {
*status = Some(match status {
None => LocalStatus::SerializableOnly,
Some(LocalStatus::LocalOnly) => LocalStatus::LocalOnly,
Some(LocalStatus::Mixed) => LocalStatus::Mixed,
Some(LocalStatus::SerializableOnly) => {
LocalStatus::SerializableOnly
}
});
});
} else {
local_status.update_value(|status| {
*status = Some(match status {
None => LocalStatus::LocalOnly,
Some(LocalStatus::LocalOnly) => LocalStatus::LocalOnly,
Some(LocalStatus::Mixed) => LocalStatus::Mixed,
Some(LocalStatus::SerializableOnly) => {
LocalStatus::Mixed
}
});
});
}
});
}
/// Notifies the suspense context that a resource has resolved.
pub fn decrement_for_resource(
&self,
serializable: bool,
resource: ResourceId,
) {
let setter = self.pending;
let serializable_resources = self.pending_serializable_resources;
batch(move || {
setter.update(|n| {
n.remove(&resource);
});
if serializable {
serializable_resources.update(|n| {
n.remove(&resource);
});
}
});
}
/// Resets the counter of pending resources.
pub fn clear(&self) {
batch(move || {
self.set_pending_resources.set(0);
self.pending.update(|p| p.clear());
self.pending_serializable_resources.update(|p| p.clear());
});
}
/// Tests whether all of the pending resources have resolved.
pub fn ready(&self) -> Memo<bool> {
let pending = self.pending;
create_memo(move |_| {
pending.try_with(|n| n.is_empty()).unwrap_or(false)
})
}
}
impl Default for SuspenseContext {
fn default() -> Self {
Self::new()
}
}
/// Represents a chunk in a stream of HTML.
pub enum StreamChunk {
/// A chunk of synchronous HTML.
Sync(Oco<'static, str>),
/// A future that resolves to be a list of additional chunks.
Async {
/// The HTML chunks this contains.
chunks: Pin<Box<dyn Future<Output = VecDeque<StreamChunk>>>>,
/// Whether this should block the stream.
should_block: bool,
},
}
impl core::fmt::Debug for StreamChunk {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
StreamChunk::Sync(data) => write!(f, "StreamChunk::Sync({data:?})"),
StreamChunk::Async { .. } => write!(f, "StreamChunk::Async(_)"),
}
}
}

View file

@ -1,298 +0,0 @@
use crate::{
diagnostics,
diagnostics::*,
node::NodeId,
runtime::{with_runtime, Runtime},
SignalGet, SignalSet, SignalUpdate,
};
/// Reactive Trigger, notifies reactive code to rerun.
///
/// See [`create_trigger`] for more.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Trigger {
pub(crate) id: NodeId,
#[cfg(debug_assertions)]
pub(crate) defined_at: &'static std::panic::Location<'static>,
}
impl Trigger {
/// Creates a [`Trigger`](crate::Trigger), a kind of reactive primitive.
///
/// A trigger is a data-less signal with the sole purpose
/// of notifying other reactive code of a change. This can be useful
/// for when using external data not stored in signals, for example.
///
/// This is identical to [`create_trigger`].
///
/// ```
/// # use leptos_reactive::*;
/// # let runtime = create_runtime();
/// use std::{cell::RefCell, fmt::Write, rc::Rc};
///
/// let external_data = Rc::new(RefCell::new(1));
/// let output = Rc::new(RefCell::new(String::new()));
///
/// let rerun_on_data = Trigger::new();
///
/// let o = output.clone();
/// let e = external_data.clone();
/// create_effect(move |_| {
/// // can be `rerun_on_data()` on nightly
/// rerun_on_data.track();
/// write!(o.borrow_mut(), "{}", *e.borrow());
/// *e.borrow_mut() += 1;
/// });
/// # if !cfg!(feature = "ssr") {
/// assert_eq!(*output.borrow(), "1");
///
/// rerun_on_data.notify(); // reruns the above effect
///
/// assert_eq!(*output.borrow(), "12");
/// # }
/// # runtime.dispose();
/// ```
#[inline(always)]
#[track_caller]
pub fn new() -> Self {
create_trigger()
}
/// Notifies any reactive code where this trigger is tracked to rerun.
///
/// ## Panics
/// Panics if there is no current reactive runtime, or if the
/// trigger has been disposed.
pub fn notify(&self) {
assert!(self.try_notify(), "Trigger::notify(): runtime not alive")
}
/// Attempts to notify any reactive code where this trigger is tracked to rerun.
///
/// Returns `false` if there is no current reactive runtime.
pub fn try_notify(&self) -> bool {
with_runtime(|runtime| {
runtime.mark_dirty(self.id);
runtime.run_effects();
})
.is_ok()
}
/// Subscribes the running effect to this trigger.
///
/// ## Panics
/// Panics if there is no current reactive runtime, or if the
/// trigger has been disposed.
pub fn track(&self) {
assert!(self.try_track(), "Trigger::track(): runtime not alive")
}
/// Attempts to subscribe the running effect to this trigger, returning
/// `false` if there is no current reactive runtime.
pub fn try_track(&self) -> bool {
let diagnostics = diagnostics!(self);
with_runtime(|runtime| {
runtime.update_if_necessary(self.id);
self.id.subscribe(runtime, diagnostics);
})
.is_ok()
}
}
/// Creates a [`Trigger`](crate::Trigger), a kind of reactive primitive.
///
/// A trigger is a data-less signal with the sole purpose
/// of notifying other reactive code of a change. This can be useful
/// for when using external data not stored in signals, for example.
/// ```
/// # use leptos_reactive::*;
/// # let runtime = create_runtime();
/// use std::{cell::RefCell, fmt::Write, rc::Rc};
///
/// let external_data = Rc::new(RefCell::new(1));
/// let output = Rc::new(RefCell::new(String::new()));
///
/// let rerun_on_data = create_trigger();
///
/// let o = output.clone();
/// let e = external_data.clone();
/// create_effect(move |_| {
/// // can be `rerun_on_data()` on nightly
/// rerun_on_data.track();
/// write!(o.borrow_mut(), "{}", *e.borrow());
/// *e.borrow_mut() += 1;
/// });
/// # if !cfg!(feature = "ssr") {
/// assert_eq!(*output.borrow(), "1");
///
/// rerun_on_data.notify(); // reruns the above effect
///
/// assert_eq!(*output.borrow(), "12");
/// # }
/// # runtime.dispose();
/// ```
#[cfg_attr(debug_assertions, instrument(level = "trace", skip_all,))]
#[track_caller]
pub fn create_trigger() -> Trigger {
Runtime::current().create_trigger()
}
impl Default for Trigger {
fn default() -> Self {
Self::new()
}
}
impl SignalGet for Trigger {
type Value = ();
#[cfg_attr(
debug_assertions,
instrument(
level = "trace",
name = "Trigger::get()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at
)
)
)]
#[track_caller]
#[inline(always)]
fn get(&self) {
self.track()
}
#[cfg_attr(
debug_assertions,
instrument(
level = "trace",
name = "Trigger::try_get()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at
)
)
)]
#[inline(always)]
fn try_get(&self) -> Option<()> {
self.try_track().then_some(())
}
}
impl SignalUpdate for Trigger {
type Value = ();
#[cfg_attr(
debug_assertions,
instrument(
name = "Trigger::update()",
level = "trace",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at
)
)
)]
#[inline(always)]
fn update(&self, f: impl FnOnce(&mut ())) {
self.try_update(f).expect("runtime to be alive")
}
#[cfg_attr(
debug_assertions,
instrument(
name = "Trigger::try_update()",
level = "trace",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at
)
)
)]
#[inline(always)]
fn try_update<O>(&self, f: impl FnOnce(&mut ()) -> O) -> Option<O> {
// run callback with runtime before dirtying the trigger,
// consistent with signals.
with_runtime(|runtime| {
let res = f(&mut ());
runtime.mark_dirty(self.id);
runtime.run_effects();
Some(res)
})
.ok()
.flatten()
}
}
impl SignalSet for Trigger {
type Value = ();
#[cfg_attr(
debug_assertions,
instrument(
level = "trace",
name = "Trigger::set()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at
)
)
)]
#[inline(always)]
fn set(&self, _: ()) {
self.notify();
}
#[cfg_attr(
debug_assertions,
instrument(
level = "trace",
name = "Trigger::try_set()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at
)
)
)]
#[inline(always)]
fn try_set(&self, _: ()) -> Option<()> {
self.try_notify().then_some(())
}
}
#[cfg(feature = "nightly")]
impl FnOnce<()> for Trigger {
type Output = ();
#[inline(always)]
extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
self.track()
}
}
#[cfg(feature = "nightly")]
impl FnMut<()> for Trigger {
#[inline(always)]
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
self.track()
}
}
#[cfg(feature = "nightly")]
impl Fn<()> for Trigger {
#[inline(always)]
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
self.track()
}
}

View file

@ -1,118 +0,0 @@
use crate::{with_runtime, Runtime, ScopeProperty};
/// A version of [`create_effect`](crate::create_effect) that listens to any dependency
/// that is accessed inside `deps` and returns a stop handler.
///
/// The return value of `deps` is passed into `callback` as an argument together with the previous value.
/// Additionally the last return value of `callback` is provided as a third argument as is done in [`create_effect`](crate::create_effect).
///
/// ## Usage
///
/// ```
/// # use leptos_reactive::*;
/// # use log;
/// # let runtime = create_runtime();
/// let (num, set_num) = create_signal(0);
///
/// let stop = watch(
/// move || num.get(),
/// move |num, prev_num, _| {
/// log::debug!("Number: {}; Prev: {:?}", num, prev_num);
/// },
/// false,
/// );
///
/// set_num.set(1); // > "Number: 1; Prev: Some(0)"
///
/// stop(); // stop watching
///
/// set_num.set(2); // (nothing happens)
/// # runtime.dispose();
/// ```
///
/// The callback itself doesn't track any signal that is accessed within it.
///
/// ```
/// # use leptos_reactive::*;
/// # use log;
/// # let runtime = create_runtime();
/// let (num, set_num) = create_signal(0);
/// let (cb_num, set_cb_num) = create_signal(0);
///
/// watch(
/// move || num.get(),
/// move |num, _, _| {
/// log::debug!("Number: {}; Cb: {}", num, cb_num.get());
/// },
/// false,
/// );
///
/// set_num.set(1); // > "Number: 1; Cb: 0"
///
/// set_cb_num.set(1); // (nothing happens)
///
/// set_num.set(2); // > "Number: 2; Cb: 1"
/// # runtime.dispose();
/// ```
///
/// ## Immediate
///
/// If the final parameter `immediate` is true, the `callback` will run immediately.
/// If it's `false`, the `callback` will run only after
/// the first change is detected of any signal that is accessed in `deps`.
///
/// ```
/// # use leptos_reactive::*;
/// # use log;
/// # let runtime = create_runtime();
/// let (num, set_num) = create_signal(0);
///
/// watch(
/// move || num.get(),
/// move |num, prev_num, _| {
/// log::debug!("Number: {}; Prev: {:?}", num, prev_num);
/// },
/// true,
/// ); // > "Number: 0; Prev: None"
///
/// set_num.set(1); // > "Number: 1; Prev: Some(0)"
/// # runtime.dispose();
/// ```
#[cfg_attr(
any(debug_assertions, feature="ssr"),
instrument(
level = "trace",
skip_all,
fields(
ty = %std::any::type_name::<T>()
)
)
)]
#[track_caller]
#[inline(always)]
pub fn watch<W, T>(
deps: impl Fn() -> W + 'static,
callback: impl Fn(&W, Option<&W>, Option<T>) -> T + Clone + 'static,
immediate: bool,
) -> impl Fn() + Clone
where
W: Clone + 'static,
T: 'static,
{
let runtime = Runtime::current();
let (e, stop) = runtime.watch(deps, callback, immediate);
let prop = ScopeProperty::Effect(e);
let owner = crate::Owner::current();
_ = with_runtime(|runtime| {
runtime.update_if_necessary(e);
});
move || {
stop();
if let Some(owner) = owner {
_ = with_runtime(|runtime| {
runtime.remove_scope_property(owner.0, prop)
});
}
}
}

View file

@ -1,108 +0,0 @@
#[test]
fn cleanup() {
use leptos_reactive::{
create_isomorphic_effect, create_runtime, create_signal, on_cleanup,
SignalSet, SignalWith,
};
use std::{cell::Cell, rc::Rc};
let runtime = create_runtime();
let runs = Rc::new(Cell::new(0));
let cleanups = Rc::new(Cell::new(0));
let (a, set_a) = create_signal(-1);
create_isomorphic_effect({
let cleanups = Rc::clone(&cleanups);
let runs = Rc::clone(&runs);
move |_| {
a.track();
runs.set(runs.get() + 1);
on_cleanup({
let cleanups = Rc::clone(&cleanups);
move || {
cleanups.set(cleanups.get() + 1);
}
});
}
});
assert_eq!(cleanups.get(), 0);
assert_eq!(runs.get(), 1);
set_a.set(1);
assert_eq!(runs.get(), 2);
assert_eq!(cleanups.get(), 1);
set_a.set(2);
assert_eq!(runs.get(), 3);
assert_eq!(cleanups.get(), 2);
runtime.dispose();
}
#[test]
fn cleanup_on_dispose() {
use leptos_reactive::{
create_memo, create_runtime, create_trigger, on_cleanup, SignalDispose,
SignalGetUntracked,
};
struct ExecuteOnDrop(Option<Box<dyn FnOnce()>>);
impl ExecuteOnDrop {
fn new(f: impl FnOnce() + 'static) -> Self {
Self(Some(Box::new(f)))
}
}
impl Drop for ExecuteOnDrop {
fn drop(&mut self) {
self.0.take().unwrap()();
}
}
let runtime = create_runtime();
let trigger = create_trigger();
println!("STARTING");
let memo = create_memo(move |_| {
trigger.track();
// An example of why you might want to do this is that
// when something goes out of reactive scope you want it to be cleaned up.
// The cleaning up might have side effects, and those side effects might cause
// re-renders where new `on_cleanup` are registered.
let on_drop = ExecuteOnDrop::new(|| {
on_cleanup(|| println!("Nested cleanup in progress."))
});
on_cleanup(move || {
println!("Cleanup in progress.");
drop(on_drop)
});
});
println!("Memo 1: {:?}", memo);
let _ = memo.get_untracked(); // First cleanup registered.
memo.dispose(); // Cleanup not run here.
println!("Cleanup should have been executed.");
let memo = create_memo(move |_| {
// New cleanup registered. It'll panic here.
on_cleanup(move || println!("Test passed."));
});
println!("Memo 2: {:?}", memo);
println!("^ Note how the memos have the same key (different versions).");
let _ = memo.get_untracked(); // First cleanup registered.
println!("Test passed.");
memo.dispose();
runtime.dispose();
}

View file

@ -1,43 +0,0 @@
#[test]
fn context() {
use leptos_reactive::{
create_isomorphic_effect, create_runtime, provide_context, use_context,
};
let runtime = create_runtime();
create_isomorphic_effect({
move |_| {
provide_context(String::from("test"));
assert_eq!(use_context::<String>(), Some(String::from("test")));
assert_eq!(use_context::<i32>(), None);
assert_eq!(use_context::<bool>(), None);
create_isomorphic_effect({
move |_| {
provide_context(0i32);
assert_eq!(
use_context::<String>(),
Some(String::from("test"))
);
assert_eq!(use_context::<i32>(), Some(0));
assert_eq!(use_context::<bool>(), None);
create_isomorphic_effect({
move |_| {
provide_context(false);
assert_eq!(
use_context::<String>(),
Some(String::from("test"))
);
assert_eq!(use_context::<i32>(), Some(0));
assert_eq!(use_context::<bool>(), Some(false));
}
});
}
});
}
});
runtime.dispose();
}

View file

@ -1,133 +0,0 @@
use leptos_reactive::{
batch, create_isomorphic_effect, create_memo, create_runtime,
create_rw_signal, create_signal, untrack, SignalGet, SignalSet,
};
#[test]
fn effect_runs() {
use std::{cell::RefCell, rc::Rc};
let runtime = create_runtime();
let (a, set_a) = create_signal(-1);
// simulate an arbitrary side effect
let b = Rc::new(RefCell::new(String::new()));
create_isomorphic_effect({
let b = b.clone();
move |_| {
let formatted = format!("Value is {}", a.get());
*b.borrow_mut() = formatted;
}
});
assert_eq!(b.borrow().as_str(), "Value is -1");
set_a.set(1);
assert_eq!(b.borrow().as_str(), "Value is 1");
runtime.dispose();
}
#[test]
fn effect_tracks_memo() {
use std::{cell::RefCell, rc::Rc};
let runtime = create_runtime();
let (a, set_a) = create_signal(-1);
let b = create_memo(move |_| format!("Value is {}", a.get()));
// simulate an arbitrary side effect
let c = Rc::new(RefCell::new(String::new()));
create_isomorphic_effect({
let c = c.clone();
move |_| {
*c.borrow_mut() = b.get();
}
});
assert_eq!(b.get().as_str(), "Value is -1");
assert_eq!(c.borrow().as_str(), "Value is -1");
set_a.set(1);
assert_eq!(b.get().as_str(), "Value is 1");
assert_eq!(c.borrow().as_str(), "Value is 1");
runtime.dispose();
}
#[test]
fn untrack_mutes_effect() {
use std::{cell::RefCell, rc::Rc};
let runtime = create_runtime();
let (a, set_a) = create_signal(-1);
// simulate an arbitrary side effect
let b = Rc::new(RefCell::new(String::new()));
create_isomorphic_effect({
let b = b.clone();
move |_| {
let formatted = format!("Value is {}", untrack(move || a.get()));
*b.borrow_mut() = formatted;
}
});
assert_eq!(a.get(), -1);
assert_eq!(b.borrow().as_str(), "Value is -1");
set_a.set(1);
assert_eq!(a.get(), 1);
assert_eq!(b.borrow().as_str(), "Value is -1");
runtime.dispose();
}
#[test]
fn batching_actually_batches() {
use std::{cell::Cell, rc::Rc};
let runtime = create_runtime();
let first_name = create_rw_signal("Greg".to_string());
let last_name = create_rw_signal("Johnston".to_string());
// simulate an arbitrary side effect
let count = Rc::new(Cell::new(0));
create_isomorphic_effect({
let count = count.clone();
move |_| {
_ = first_name.get();
_ = last_name.get();
count.set(count.get() + 1);
}
});
// runs once initially
assert_eq!(count.get(), 1);
// individual updates run effect once each
first_name.set("Alice".to_string());
assert_eq!(count.get(), 2);
last_name.set("Smith".to_string());
assert_eq!(count.get(), 3);
// batched effect only runs twice
batch(move || {
first_name.set("Bob".to_string());
last_name.set("Williams".to_string());
});
assert_eq!(count.get(), 4);
runtime.dispose();
}

View file

@ -1,321 +0,0 @@
use leptos_reactive::*;
#[test]
fn basic_memo() {
let runtime = create_runtime();
let a = create_memo(|_| 5);
assert_eq!(a.get(), 5);
runtime.dispose();
}
#[test]
fn signal_with_untracked() {
use leptos_reactive::SignalWithUntracked;
let runtime = create_runtime();
let m = create_memo(move |_| 5);
let copied_out = m.with_untracked(|value| *value);
assert_eq!(copied_out, 5);
runtime.dispose();
}
#[test]
fn signal_get_untracked() {
use leptos_reactive::SignalGetUntracked;
let runtime = create_runtime();
let m = create_memo(move |_| "memo".to_owned());
let cloned_out = m.get_untracked();
assert_eq!(cloned_out, "memo".to_owned());
runtime.dispose();
}
#[test]
fn memo_with_computed_value() {
let runtime = create_runtime();
let (a, set_a) = create_signal(0);
let (b, set_b) = create_signal(0);
let c = create_memo(move |_| a.get() + b.get());
assert_eq!(c.get(), 0);
set_a.set(5);
assert_eq!(c.get(), 5);
set_b.set(1);
assert_eq!(c.get(), 6);
runtime.dispose();
}
#[test]
fn nested_memos() {
let runtime = create_runtime();
let (a, set_a) = create_signal(0); // 1
let (b, set_b) = create_signal(0); // 2
let c = create_memo(move |_| a.get() + b.get()); // 3
let d = create_memo(move |_| c.get() * 2); // 4
let e = create_memo(move |_| d.get() + 1); // 5
assert_eq!(d.get(), 0);
set_a.set(5);
assert_eq!(e.get(), 11);
assert_eq!(d.get(), 10);
assert_eq!(c.get(), 5);
set_b.set(1);
assert_eq!(e.get(), 13);
assert_eq!(d.get(), 12);
assert_eq!(c.get(), 6);
runtime.dispose();
}
#[test]
fn memo_runs_only_when_inputs_change() {
use std::{cell::Cell, rc::Rc};
let runtime = create_runtime();
let call_count = Rc::new(Cell::new(0));
let (a, set_a) = create_signal(0);
let (b, _) = create_signal(0);
let (c, _) = create_signal(0);
// pretend that this is some kind of expensive computation and we need to access its its value often
// we could do this with a derived signal, but that would re-run the computation
// memos should only run when their inputs actually change: this is the only point
let c = create_memo({
let call_count = call_count.clone();
move |_| {
call_count.set(call_count.get() + 1);
a.get() + b.get() + c.get()
}
});
// initially the memo has not been called at all, because it's lazy
assert_eq!(call_count.get(), 0);
// here we access the value a bunch of times
assert_eq!(c.get(), 0);
assert_eq!(c.get(), 0);
assert_eq!(c.get(), 0);
assert_eq!(c.get(), 0);
assert_eq!(c.get(), 0);
// we've still only called the memo calculation once
assert_eq!(call_count.get(), 1);
// and we only call it again when an input changes
set_a.set(1);
assert_eq!(c.get(), 1);
assert_eq!(call_count.get(), 2);
runtime.dispose();
}
#[test]
fn diamond_problem() {
use std::{cell::Cell, rc::Rc};
let runtime = create_runtime();
let (name, set_name) = create_signal("Greg Johnston".to_string());
let first = create_memo(move |_| {
name.get().split_whitespace().next().unwrap().to_string()
});
let last = create_memo(move |_| {
name.get().split_whitespace().nth(1).unwrap().to_string()
});
let combined_count = Rc::new(Cell::new(0));
let combined = create_memo({
let combined_count = Rc::clone(&combined_count);
move |_| {
combined_count.set(combined_count.get() + 1);
format!("{} {}", first.get(), last.get())
}
});
assert_eq!(first.get(), "Greg");
assert_eq!(last.get(), "Johnston");
set_name.set("Will Smith".to_string());
assert_eq!(first.get(), "Will");
assert_eq!(last.get(), "Smith");
assert_eq!(combined.get(), "Will Smith");
// should not have run the memo logic twice, even
// though both paths have been updated
assert_eq!(combined_count.get(), 1);
runtime.dispose();
}
#[test]
fn dynamic_dependencies() {
use leptos_reactive::create_isomorphic_effect;
use std::{cell::Cell, rc::Rc};
let runtime = create_runtime();
let (first, set_first) = create_signal("Greg");
let (last, set_last) = create_signal("Johnston");
let (use_last, set_use_last) = create_signal(true);
let name = create_memo(move |_| {
if use_last.get() {
format!("{} {}", first.get(), last.get())
} else {
first.get().to_string()
}
});
let combined_count = Rc::new(Cell::new(0));
create_isomorphic_effect({
let combined_count = Rc::clone(&combined_count);
move |_| {
_ = name.get();
combined_count.set(combined_count.get() + 1);
}
});
assert_eq!(combined_count.get(), 1);
set_first.set("Bob");
assert_eq!(name.get(), "Bob Johnston");
assert_eq!(combined_count.get(), 2);
set_last.set("Thompson");
assert_eq!(combined_count.get(), 3);
set_use_last.set(false);
assert_eq!(name.get(), "Bob");
assert_eq!(combined_count.get(), 4);
assert_eq!(combined_count.get(), 4);
set_last.set("Jones");
assert_eq!(combined_count.get(), 4);
set_last.set("Smith");
assert_eq!(combined_count.get(), 4);
set_last.set("Stevens");
assert_eq!(combined_count.get(), 4);
set_use_last.set(true);
assert_eq!(name.get(), "Bob Stevens");
assert_eq!(combined_count.get(), 5);
runtime.dispose();
}
#[test]
fn owning_memo_slice() {
use std::rc::Rc;
let runtime = create_runtime();
// this could be serialized to and from localstorage with miniserde
pub struct State {
name: String,
token: String,
}
let state = create_rw_signal(State {
name: "Alice".to_owned(),
token: "is this a token????".to_owned(),
});
// We can allocate only when `state.name` changes
let name = create_owning_memo(move |old_name| {
state.with(move |state| {
if let Some(name) =
old_name.filter(|old_name| old_name == &state.name)
{
(name, false)
} else {
(state.name.clone(), true)
}
})
});
let set_name = move |name| state.update(|state| state.name = name);
// We can also re-use the last token allocation, which may be even better if the tokens are
// always of the same length
let token = create_owning_memo(move |old_token| {
state.with(move |state| {
let is_different = old_token.as_ref() != Some(&state.token);
let mut token = old_token.unwrap_or_default();
if is_different {
token.clone_from(&state.token);
}
(token, is_different)
})
});
let set_token =
move |new_token| state.update(|state| state.token = new_token);
let count_name_updates = Rc::new(std::cell::Cell::new(0));
assert_eq!(count_name_updates.get(), 0);
create_isomorphic_effect({
let count_name_updates = Rc::clone(&count_name_updates);
move |_| {
name.track();
count_name_updates.set(count_name_updates.get() + 1);
}
});
assert_eq!(count_name_updates.get(), 1);
let count_token_updates = Rc::new(std::cell::Cell::new(0));
assert_eq!(count_token_updates.get(), 0);
create_isomorphic_effect({
let count_token_updates = Rc::clone(&count_token_updates);
move |_| {
token.track();
count_token_updates.set(count_token_updates.get() + 1);
}
});
assert_eq!(count_token_updates.get(), 1);
set_name("Bob".to_owned());
name.with(|name| assert_eq!(name, "Bob"));
assert_eq!(count_name_updates.get(), 2);
assert_eq!(count_token_updates.get(), 1);
set_token("this is not a token!".to_owned());
token.with(|token| assert_eq!(token, "this is not a token!"));
assert_eq!(count_name_updates.get(), 2);
assert_eq!(count_token_updates.get(), 2);
runtime.dispose();
}
#[test]
fn leak_on_dispose() {
use std::rc::Rc;
let runtime = create_runtime();
let trigger = create_trigger();
let value = Rc::new(());
let weak = Rc::downgrade(&value);
let memo = create_memo(move |_| {
trigger.track();
create_rw_signal(value.clone());
});
memo.get_untracked();
memo.dispose();
assert!(weak.upgrade().is_none()); // Should have been dropped.
runtime.dispose();
}

View file

@ -1,66 +0,0 @@
#[test]
fn resource_returns_last_future() {
#[cfg(feature = "ssr")]
{
use futures::{channel::oneshot::channel, FutureExt};
use leptos_reactive::{
create_resource, create_runtime, create_signal, SignalGet,
SignalSet,
};
use tokio::task;
use tokio_test::block_on;
let runtime = create_runtime();
block_on(task::LocalSet::new().run_until(async move {
task::spawn_local(async move {
// Set up a resource that can listen to two different futures that we can resolve independently
let (tx_1, rx_1) = channel::<()>();
let (tx_2, rx_2) = channel::<()>();
let rx_1 = rx_1.shared();
let rx_2 = rx_2.shared();
let (channel_number, set_channel_number) = create_signal(1);
let resource = create_resource(
move || channel_number.get(),
move |channel_number| {
let rx_1 = rx_1.clone();
let rx_2 = rx_2.clone();
async move {
match channel_number {
1 => rx_1.await,
2 => rx_2.await,
_ => unreachable!(),
}
.unwrap();
channel_number
}
},
);
// Switch to waiting to second future while first is still loading
set_channel_number.set(2);
// Resolve first future
tx_1.send(()).unwrap();
task::yield_now().await;
// Resource should still be loading
assert_eq!(resource.get(), None);
// Resolve second future
tx_2.send(()).unwrap();
task::yield_now().await;
// Resource should now be loaded
assert_eq!(resource.get(), Some(2));
})
.await
.unwrap();
}));
runtime.dispose();
}
}

View file

@ -1,29 +0,0 @@
use leptos_reactive::*;
#[test]
fn basic_signal() {
let runtime = create_runtime();
let (a, set_a) = create_signal(0);
assert_eq!(a.get(), 0);
set_a.set(5);
assert_eq!(a.get(), 5);
runtime.dispose();
}
#[test]
fn derived_signals() {
let runtime = create_runtime();
let (a, set_a) = create_signal(0);
let (b, set_b) = create_signal(0);
let c = move || a.get() + b.get();
assert_eq!(c(), 0);
set_a.set(5);
assert_eq!(c(), 5);
set_b.set(1);
assert_eq!(c(), 6);
runtime.dispose();
}

View file

@ -1,53 +0,0 @@
use std::rc::Rc;
#[test]
fn slice() {
use leptos_reactive::*;
let runtime = create_runtime();
// this could be serialized to and from localstorage with miniserde
pub struct State {
token: String,
dark_mode: bool,
}
let state = create_rw_signal(State {
token: "".into(),
// this would cause flickering on reload,
// use a cookie for the initial value in real projects
dark_mode: false,
});
let (token, set_token) = create_slice(
state,
|state| state.token.clone(),
|state, value| state.token = value,
);
let (_, set_dark_mode) = create_slice(
state,
|state| state.dark_mode,
|state, value| state.dark_mode = value,
);
let count_token_updates = Rc::new(std::cell::Cell::new(0));
assert_eq!(count_token_updates.get(), 0);
create_isomorphic_effect({
let count_token_updates = Rc::clone(&count_token_updates);
move |_| {
token.track();
count_token_updates.set(count_token_updates.get() + 1);
}
});
assert_eq!(count_token_updates.get(), 1);
set_token.set("this is not a token!".into());
// token was updated with the new token
token.with(|token| assert_eq!(token, "this is not a token!"));
assert_eq!(count_token_updates.get(), 2);
set_dark_mode.set(true);
// since token didn't change, there was also no update emitted
assert_eq!(count_token_updates.get(), 2);
runtime.dispose();
}

View file

@ -1,77 +0,0 @@
use leptos_reactive::{
create_isomorphic_effect, create_runtime, signal_prelude::*,
};
#[test]
fn untracked_set_doesnt_trigger_effect() {
use std::{cell::RefCell, rc::Rc};
let runtime = create_runtime();
let (a, set_a) = create_signal(-1);
// simulate an arbitrary side effect
let b = Rc::new(RefCell::new(String::new()));
create_isomorphic_effect({
let b = b.clone();
move |_| {
let formatted = format!("Value is {}", a.get());
*b.borrow_mut() = formatted;
}
});
assert_eq!(b.borrow().as_str(), "Value is -1");
set_a.set(1);
assert_eq!(b.borrow().as_str(), "Value is 1");
set_a.set_untracked(-1);
assert_eq!(b.borrow().as_str(), "Value is 1");
runtime.dispose();
}
#[test]
fn untracked_get_doesnt_trigger_effect() {
use std::{cell::RefCell, rc::Rc};
let runtime = create_runtime();
let (a, set_a) = create_signal(-1);
let (a2, set_a2) = create_signal(1);
// simulate an arbitrary side effect
let b = Rc::new(RefCell::new(String::new()));
create_isomorphic_effect({
let b = b.clone();
move |_| {
let formatted =
format!("Values are {} and {}", a.get(), a2.get_untracked());
*b.borrow_mut() = formatted;
}
});
assert_eq!(b.borrow().as_str(), "Values are -1 and 1");
set_a.set(1);
assert_eq!(b.borrow().as_str(), "Values are 1 and 1");
set_a.set_untracked(-1);
assert_eq!(b.borrow().as_str(), "Values are 1 and 1");
set_a2.set(-1);
assert_eq!(b.borrow().as_str(), "Values are 1 and 1");
set_a.set(-1);
assert_eq!(b.borrow().as_str(), "Values are -1 and -1");
runtime.dispose();
}

View file

@ -1,140 +0,0 @@
use leptos_reactive::{
create_runtime, create_signal, watch, SignalGet, SignalSet,
};
use std::{cell::RefCell, rc::Rc};
#[test]
fn watch_runs() {
let runtime = create_runtime();
let (a, set_a) = create_signal(-1);
// simulate an arbitrary side effect
let b = Rc::new(RefCell::new(String::new()));
let stop = watch(
move || a.get(),
{
let b = b.clone();
move |a, prev_a, prev_ret| {
let formatted = format!(
"Value is {a}; Prev is {prev_a:?}; Prev return is \
{prev_ret:?}"
);
*b.borrow_mut() = formatted;
a + 10
}
},
false,
);
assert_eq!(b.borrow().as_str(), "");
set_a.set(1);
assert_eq!(
b.borrow().as_str(),
"Value is 1; Prev is Some(-1); Prev return is None"
);
set_a.set(2);
assert_eq!(
b.borrow().as_str(),
"Value is 2; Prev is Some(1); Prev return is Some(11)"
);
stop();
*b.borrow_mut() = "nothing happened".to_string();
set_a.set(3);
assert_eq!(b.borrow().as_str(), "nothing happened");
runtime.dispose();
}
#[test]
fn watch_runs_immediately() {
let runtime = create_runtime();
let (a, set_a) = create_signal(-1);
// simulate an arbitrary side effect
let b = Rc::new(RefCell::new(String::new()));
let _ = watch(
move || a.get(),
{
let b = b.clone();
move |a, prev_a, prev_ret| {
let formatted = format!(
"Value is {a}; Prev is {prev_a:?}; Prev return is \
{prev_ret:?}"
);
*b.borrow_mut() = formatted;
a + 10
}
},
true,
);
assert_eq!(
b.borrow().as_str(),
"Value is -1; Prev is None; Prev return is None"
);
set_a.set(1);
assert_eq!(
b.borrow().as_str(),
"Value is 1; Prev is Some(-1); Prev return is Some(9)"
);
runtime.dispose();
}
#[test]
fn watch_ignores_callback() {
let runtime = create_runtime();
let (a, set_a) = create_signal(-1);
let (b, set_b) = create_signal(0);
// simulate an arbitrary side effect
let s = Rc::new(RefCell::new(String::new()));
let _ = watch(
move || a.get(),
{
let s = s.clone();
move |a, _, _| {
let formatted =
format!("Value a is {}; Value b is {}", a, b.get());
*s.borrow_mut() = formatted;
}
},
false,
);
set_a.set(1);
assert_eq!(s.borrow().as_str(), "Value a is 1; Value b is 0");
*s.borrow_mut() = "nothing happened".to_string();
set_b.set(10);
assert_eq!(s.borrow().as_str(), "nothing happened");
set_a.set(2);
assert_eq!(s.borrow().as_str(), "Value a is 2; Value b is 10");
runtime.dispose();
}