mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-14 16:47:19 +00:00
remove leptos_reactive (moved into reactive_graph and leptos_server)
This commit is contained in:
parent
ea76a0f74e
commit
6088da7342
44 changed files with 0 additions and 13152 deletions
|
@ -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" }
|
||||
|
|
|
@ -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"]
|
|
@ -1 +0,0 @@
|
|||
extend = { path = "../cargo-make/main.toml" }
|
|
@ -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 don’t 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();
|
||||
}
|
||||
}
|
|
@ -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)()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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, it’s 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>()
|
||||
)
|
||||
})
|
||||
}
|
|
@ -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 { }
|
||||
}
|
||||
}
|
||||
}};
|
||||
}
|
|
@ -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
|
||||
/// you’re 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 effect’s 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
|
||||
}
|
||||
}
|
|
@ -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>"
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
})
|
||||
})
|
|
@ -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}.`);
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
});
|
||||
})
|
|
@ -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>
|
||||
}
|
||||
}
|
|
@ -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.');
|
||||
})
|
|
@ -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
|
||||
//! there’s 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}");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 signal’s value without
|
||||
/// cloning by applying a callback function.
|
||||
/// - [`.with_untracked()`](#impl-SignalWithUntracked<T>-for-Memo<T>) allows you to access the signal’s
|
||||
/// 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,
|
||||
)
|
||||
)
|
||||
}
|
|
@ -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
|
@ -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 signal’s value changes whether it is equal to the key value
|
||||
/// (as determined by [`PartialEq`].)
|
||||
///
|
||||
/// **You probably don’t 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 signal’s value changes whether the given function is true.
|
||||
///
|
||||
/// **You probably don’t 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 signal’s 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 signal’s value changes whether it is equal to the key value
|
||||
/// (as determined by [`PartialEq`].)
|
||||
///
|
||||
/// **You probably don’t 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);
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
@ -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 signal’s value, and notifies all subscribers that the signal’s 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];
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
/// signal’s 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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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));
|
|
@ -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 won’t 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(_)"),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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();
|
||||
}
|
Loading…
Reference in a new issue