Merge pull request #1579 from leptos-rs/rusty

feat: start adding some Rustier interfaces for reactive types
This commit is contained in:
Greg Johnston 2023-09-04 13:23:18 -04:00 committed by GitHub
commit 70e1ad41e2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 433 additions and 17 deletions

View file

@ -3,7 +3,7 @@ use leptos::{ev, html::*, *};
/// A simple counter view.
// A component is really just a function call: it runs once to create the DOM and reactive system
pub fn counter(initial_value: i32, step: u32) -> impl IntoView {
let (count, set_count) = create_signal(Count::new(initial_value, step));
let count = RwSignal::new(Count::new(initial_value, step));
// the function name is the same as the HTML tag name
div()
@ -17,18 +17,14 @@ pub fn counter(initial_value: i32, step: u32) -> impl IntoView {
// typed events found in leptos::ev
// 1) prevent typos in event names
// 2) allow for correct type inference in callbacks
.on(ev::click, move |_| set_count.update(|count| count.clear()))
.on(ev::click, move |_| count.update(Count::clear))
.child("Clear"),
button()
.on(ev::click, move |_| {
set_count.update(|count| count.decrease())
})
.on(ev::click, move |_| count.update(Count::decrease))
.child("-1"),
span().child(("Value: ", move || count.get().value(), "!")),
button()
.on(ev::click, move |_| {
set_count.update(|count| count.increase())
})
.on(ev::click, move |_| count.update(Count::increase))
.child("+1"),
))
}

View file

@ -57,7 +57,7 @@ use std::{any::Any, cell::RefCell, marker::PhantomData, rc::Rc};
)]
#[track_caller]
#[inline(always)]
pub fn create_effect<T>(f: impl Fn(Option<T>) -> T + 'static) -> Effect
pub fn create_effect<T>(f: impl Fn(Option<T>) -> T + 'static) -> Effect<T>
where
T: 'static,
{
@ -69,15 +69,127 @@ where
_ = with_runtime( |runtime| {
runtime.update_if_necessary(id);
});
Effect { id }
Effect { id, ty: PhantomData }
} else {
// clear warnings
_ = f;
Effect::default()
Effect { id: Default::default(), ty: PhantomData }
}
}
}
impl<T> Effect<T>
where
T: 'static,
{
/// Effects run a certain chunk of code whenever the signals they depend on change.
/// `create_effect` immediately runs the given function once, tracks its dependence
/// on any signal values read within it, and reruns the function whenever the value
/// of a dependency changes.
///
/// Effects are intended to run *side-effects* of the system, not to synchronize state
/// *within* the system. In other words: don't write to signals within effects.
/// (If you need to define a signal that depends on the value of other signals, use a
/// derived signal or [`create_memo`](crate::create_memo)).
///
/// The effect function is called with an argument containing whatever value it returned
/// the last time it ran. On the initial run, this is `None`.
///
/// By default, effects **do not run on the server**. This means you can call browser-specific
/// APIs within the effect function without causing issues. If you need an effect to run on
/// the server, use [`create_isomorphic_effect`].
/// ```
/// # use leptos_reactive::*;
/// # use log::*;
/// # let runtime = create_runtime();
/// let a = RwSignal::new(0);
/// let b = RwSignal::new(0);
///
/// // ✅ use effects to interact between reactive state and the outside world
/// Effect::new(move |_| {
/// // immediately prints "Value: 0" and subscribes to `a`
/// log::debug!("Value: {}", a.get());
/// });
///
/// a.set(1);
/// // ✅ because it's subscribed to `a`, the effect reruns and prints "Value: 1"
///
/// // ❌ don't use effects to synchronize state within the reactive system
/// Effect::new(move |_| {
/// // this technically works but can cause unnecessary re-renders
/// // and easily lead to problems like infinite loops
/// b.set(a.get() + 1);
/// });
/// # if !cfg!(feature = "ssr") {
/// # assert_eq!(b.get(), 2);
/// # }
/// # runtime.dispose();
/// ```
#[track_caller]
#[inline(always)]
pub fn new(f: impl Fn(Option<T>) -> T + 'static) -> Self {
create_effect(f)
}
/// Creates an effect; unlike effects created by [`create_effect`], isomorphic effects will run on
/// the server as well as the client.
/// ```
/// # use leptos_reactive::*;
/// # use log::*;
/// # let runtime = create_runtime();
/// let a = RwSignal::new(0);
/// let b = RwSignal::new(0);
///
/// // ✅ use effects to interact between reactive state and the outside world
/// Effect::new_isomorphic(move |_| {
/// // immediately prints "Value: 0" and subscribes to `a`
/// log::debug!("Value: {}", a.get());
/// });
///
/// a.set(1);
/// // ✅ because it's subscribed to `a`, the effect reruns and prints "Value: 1"
///
/// // ❌ don't use effects to synchronize state within the reactive system
/// Effect::new_isomorphic(move |_| {
/// // this technically works but can cause unnecessary re-renders
/// // and easily lead to problems like infinite loops
/// b.set(a.get() + 1);
/// });
/// # assert_eq!(b.get(), 2);
/// # runtime.dispose();
#[track_caller]
#[inline(always)]
pub fn new_isomorphic(f: impl Fn(Option<T>) -> T + 'static) -> Self {
create_isomorphic_effect(f)
}
/// Applies the given closure to the most recent value of the effect.
///
/// Because effect functions can return values, each time an effect runs it
/// consumes its previous value. This allows an effect to store additional state
/// (like a DOM node, a timeout handle, or a type that implements `Drop`) and
/// keep it alive across multiple runs.
///
/// This method allows access to the effects value outside the effect function.
/// The next time a signal change causes the effect to run, it will receive the
/// mutated value.
pub fn with_value_mut<U>(
&self,
f: impl FnOnce(&mut Option<T>) -> U,
) -> Option<U> {
with_runtime(|runtime| {
let nodes = runtime.nodes.borrow();
let node = nodes.get(self.id)?;
let value = node.value.clone()?;
let mut value = value.borrow_mut();
let value = value.downcast_mut()?;
Some(f(value))
})
.ok()
.flatten()
}
}
/// Creates an effect; unlike effects created by [`create_effect`], isomorphic effects will run on
/// the server as well as the client.
/// ```
@ -118,7 +230,7 @@ where
#[inline(always)]
pub fn create_isomorphic_effect<T>(
f: impl Fn(Option<T>) -> T + 'static,
) -> Effect
) -> Effect<T>
where
T: 'static,
{
@ -128,7 +240,10 @@ where
_ = with_runtime(|runtime| {
runtime.update_if_necessary(id);
});
Effect { id }
Effect {
id,
ty: PhantomData,
}
}
#[doc(hidden)]
@ -152,17 +267,18 @@ where
/// 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 {
pub struct Effect<T> {
pub(crate) id: NodeId,
ty: PhantomData<T>,
}
impl From<Effect> for Disposer {
fn from(effect: Effect) -> Self {
impl<T> From<Effect<T>> for Disposer {
fn from(effect: Effect<T>) -> Self {
Disposer(effect.id)
}
}
impl SignalDispose for Effect {
impl<T> SignalDispose for Effect<T> {
fn dispose(self) {
drop(Disposer::from(self));
}

View file

@ -165,6 +165,58 @@ where
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)
}
}
impl<T> Clone for Memo<T>
where
T: 'static,

View file

@ -904,6 +904,139 @@ where
pub(crate) defined_at: &'static std::panic::Location<'static>,
}
impl<S, T> Resource<S, T>
where
S: 'static,
T: 'static,
{
/// Creates a [`Resource`](crate::Resource), which is a signal that reflects the
/// current state of an asynchronous task, allowing you to integrate `async`
/// [`Future`]s into the synchronous reactive system.
///
/// Takes a `fetcher` function that generates a [`Future`] when called and a
/// `source` signal that provides the argument for the `fetcher`. Whenever the
/// value of the `source` changes, a new [`Future`] will be created and run.
///
/// When server-side rendering is used, the server will handle running the
/// [`Future`] and will stream the result to the client. This process requires the
/// output type of the Future to be [`Serializable`]. If your output cannot be
/// serialized, or you just want to make sure the [`Future`] runs locally, use
/// [`create_local_resource()`].
///
/// This is identical with [`create_resource`].
///
/// ```
/// # use leptos_reactive::*;
/// # let runtime = create_runtime();
/// // any old async function; maybe this is calling a REST API or something
/// async fn fetch_cat_picture_urls(how_many: i32) -> Vec<String> {
/// // pretend we're fetching cat pics
/// vec![how_many.to_string()]
/// }
///
/// // a signal that controls how many cat pics we want
/// let (how_many_cats, set_how_many_cats) = create_signal(1);
///
/// // create a resource that will refetch whenever `how_many_cats` changes
/// # // `csr`, `hydrate`, and `ssr` all have issues here
/// # // because we're not running in a browser or in Tokio. Let's just ignore it.
/// # if false {
/// let cats = Resource::new(move || how_many_cats.get(), fetch_cat_picture_urls);
///
/// // when we read the signal, it contains either
/// // 1) None (if the Future isn't ready yet) or
/// // 2) Some(T) (if the future's already resolved)
/// assert_eq!(cats.read(), Some(vec!["1".to_string()]));
///
/// // when the signal's value changes, the `Resource` will generate and run a new `Future`
/// set_how_many_cats.set(2);
/// assert_eq!(cats.read(), Some(vec!["2".to_string()]));
/// # }
/// # runtime.dispose();
/// ```
#[inline(always)]
#[track_caller]
pub fn new<Fu>(
source: impl Fn() -> S + 'static,
fetcher: impl Fn(S) -> Fu + 'static,
) -> Resource<S, T>
where
S: PartialEq + Clone + 'static,
T: Serializable + 'static,
Fu: Future<Output = T> + 'static,
{
create_resource(source, fetcher)
}
/// Creates a _local_ [`Resource`](crate::Resource), which is a signal that
/// reflects the current state of an asynchronous task, allowing you to
/// integrate `async` [`Future`]s into the synchronous reactive system.
///
/// Takes a `fetcher` function that generates a [`Future`] when called and a
/// `source` signal that provides the argument for the `fetcher`. Whenever the
/// value of the `source` changes, a new [`Future`] will be created and run.
///
/// Unlike [`create_resource()`], this [`Future`] is always run on the local system
/// and therefore it's result type does not need to be [`Serializable`].
///
/// This is identical with [`create_local_resource`].
///
/// ```
/// # use leptos_reactive::*;
/// # let runtime = create_runtime();
/// #[derive(Debug, Clone)] // doesn't implement Serialize, Deserialize
/// struct ComplicatedUnserializableStruct {
/// // something here that can't be serialized
/// }
/// // any old async function; maybe this is calling a REST API or something
/// async fn setup_complicated_struct() -> ComplicatedUnserializableStruct {
/// // do some work
/// ComplicatedUnserializableStruct {}
/// }
///
/// // create the resource; it will run but not be serialized
/// # if cfg!(not(any(feature = "csr", feature = "hydrate"))) {
/// let result =
/// create_local_resource(move || (), |_| setup_complicated_struct());
/// # }
/// # runtime.dispose();
/// ```
#[inline(always)]
#[track_caller]
pub fn local<Fu>(
source: impl Fn() -> S + 'static,
fetcher: impl Fn(S) -> Fu + 'static,
) -> Resource<S, T>
where
S: PartialEq + Clone + 'static,
T: 'static,
Fu: Future<Output = T> + 'static,
{
let initial_value = None;
create_local_resource_with_initial_value(source, fetcher, initial_value)
}
}
impl<T> Resource<(), T>
where
T: 'static,
{
/// Creates a resource that will only load once, and will not respond
/// to any reactive changes, including changes in any reactive variables
/// read in its fetcher.
///
/// This identical to `create_resource(|| (), move |_| fetcher())`.
#[inline(always)]
#[track_caller]
pub fn once<Fu>(fetcher: impl Fn() -> Fu + 'static) -> Resource<(), T>
where
T: Serializable + 'static,
Fu: Future<Output = T> + 'static,
{
create_resource(|| (), move |_| fetcher())
}
}
// Resources
slotmap::new_key_type! {
/// Unique ID assigned to a [`Resource`](crate::Resource).

View file

@ -124,6 +124,50 @@ impl<T> Selector<T>
where
T: PartialEq + Eq + Clone + Hash + 'static,
{
/// Creates a conditional signal that only notifies subscribers when a change
/// in the source signals value changes whether it is equal to the key value
/// (as determined by [`PartialEq`].)
///
/// **You probably dont need this,** but it can be a very useful optimization
/// in certain situations (e.g., “set the class `selected` if `selected() == this_row_index`)
/// because it reduces them from `O(n)` to `O(1)`.
///
/// ```
/// # use leptos_reactive::*;
/// # use std::rc::Rc;
/// # use std::cell::RefCell;
/// # let runtime = create_runtime();
/// let a = RwSignal::new(0);
/// let is_selected = Selector::new(move || a.get());
/// let total_notifications = Rc::new(RefCell::new(0));
/// let not = Rc::clone(&total_notifications);
/// create_isomorphic_effect({
/// let is_selected = is_selected.clone();
/// move |_| {
/// if is_selected.selected(5) {
/// *not.borrow_mut() += 1;
/// }
/// }
/// });
///
/// assert_eq!(is_selected.selected(5), false);
/// assert_eq!(*total_notifications.borrow(), 0);
/// a.set(5);
/// assert_eq!(is_selected.selected(5), true);
/// assert_eq!(*total_notifications.borrow(), 1);
/// a.set(5);
/// assert_eq!(is_selected.selected(5), true);
/// assert_eq!(*total_notifications.borrow(), 1);
/// a.set(4);
/// assert_eq!(is_selected.selected(5), false);
/// # runtime.dispose()
/// ```
#[inline(always)]
#[track_caller]
pub fn new(source: impl Fn() -> T + Clone + 'static) -> Self {
create_selector_with_fn(source, PartialEq::eq)
}
/// Reactively checks whether the given key is selected.
pub fn selected(&self, key: T) -> bool {
let owner = self.owner;

View file

@ -1790,6 +1790,35 @@ impl<T> SignalDispose for RwSignal<T> {
}
impl<T> RwSignal<T> {
/// Creates a reactive signal with the getter and setter unified in one value.
/// You may prefer this style, or it may be easier to pass around in a context
/// or as a function argument.
///
/// This is identical to [`create_rw_signal`].
/// ```
/// # use leptos_reactive::*;
/// # let runtime = create_runtime();
/// let count = RwSignal::new(0);
///
/// // ✅ set the value
/// count.set(1);
/// assert_eq!(count.get(), 1);
///
/// // ❌ you can call the getter within the setter
/// // count.set(count.get() + 1);
///
/// // ✅ however, it's more efficient to use .update() and mutate the value in place
/// count.update(|count: &mut i32| *count += 1);
/// assert_eq!(count.get(), 2);
/// # runtime.dispose();
/// #
/// ```
#[inline(always)]
#[track_caller]
pub fn new(value: T) -> Self {
create_rw_signal(value)
}
/// Returns a read-only handle to the signal.
///
/// Useful if you're trying to give read access to another component but ensure that it can't write

View file

@ -308,4 +308,50 @@ where
}
}
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));