diff --git a/examples/hackernews/hackernews-app/Cargo.toml b/examples/hackernews/hackernews-app/Cargo.toml index 17434a2..68b93d2 100644 --- a/examples/hackernews/hackernews-app/Cargo.toml +++ b/examples/hackernews/hackernews-app/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" [dependencies] anyhow = "1" console_log = "0.2" -leptos = { path = "../../../leptos", default-features = false } +leptos = { path = "../../../leptos", default-features = false, features = ["serde"] } leptos_meta = { path = "../../../meta", default-features = false } leptos_router = { path = "../../../router", default-features = false } log = "0.4" diff --git a/leptos_reactive/src/resource.rs b/leptos_reactive/src/resource.rs index 35331ea..f78a6d3 100644 --- a/leptos_reactive/src/resource.rs +++ b/leptos_reactive/src/resource.rs @@ -10,18 +10,24 @@ use std::{ }; use crate::{ - create_isomorphic_effect, create_memo, create_signal, queue_microtask, runtime::Runtime, - serialization::Serializable, spawn::spawn_local, use_context, Memo, ReadSignal, Scope, - ScopeProperty, SuspenseContext, WriteSignal, + create_effect, create_isomorphic_effect, create_memo, create_signal, queue_microtask, + runtime::Runtime, serialization::Serializable, spawn::spawn_local, use_context, Memo, + ReadSignal, Scope, ScopeProperty, SuspenseContext, WriteSignal, }; /// Creates [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. +/// 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. +/// `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()]. /// /// ``` /// # use leptos_reactive::*; @@ -73,6 +79,12 @@ where /// Creates a [Resource](crate::Resource) with the given initial value, which /// will only generate and run a [Future] using the `fetcher` when the `source` changes. +/// +/// 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_with_initial_value()]. pub fn create_resource_with_initial_value( cx: Scope, source: impl Fn() -> S + 'static, @@ -105,7 +117,7 @@ where suspense_contexts: Default::default(), }); - let id = cx.runtime.create_resource(Rc::clone(&r)); + let id = cx.runtime.create_serializable_resource(Rc::clone(&r)); create_isomorphic_effect(cx, { let r = Rc::clone(&r); @@ -124,11 +136,111 @@ where } } +/// 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]. +/// +/// ``` +/// # use leptos_reactive::*; +/// # create_scope(|cx| { +/// #[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 that will +/// let result = create_local_resource(cx, move || (), |_| setup_complicated_struct()); +/// # }).dispose(); +/// ``` +pub fn create_local_resource( + cx: Scope, + source: impl Fn() -> S + 'static, + fetcher: impl Fn(S) -> Fu + 'static, +) -> Resource +where + S: PartialEq + Debug + Clone + 'static, + T: Debug + Clone + 'static, + Fu: Future + 'static, +{ + let initial_value = None; + create_local_resource_with_initial_value(cx, source, fetcher, initial_value) +} + +/// Creates a _local_ [Resource](crate::Resource) with the given initial value, +/// which will only generate and run a [Future] using the `fetcher` when the +/// `source` changes. +/// +/// Unlike [create_resource_with_initial_value()], this [Future] will always run +/// on the local system and therefore its output type does not need to be +/// [Serializable]. +pub fn create_local_resource_with_initial_value( + cx: Scope, + source: impl Fn() -> S + 'static, + fetcher: impl Fn(S) -> Fu + 'static, + initial_value: Option, +) -> Resource +where + S: PartialEq + Debug + Clone + 'static, + T: Debug + Clone + 'static, + Fu: Future + 'static, +{ + let resolved = initial_value.is_some(); + let (value, set_value) = create_signal(cx, initial_value); + + let (loading, set_loading) = create_signal(cx, false); + + let fetcher = Rc::new(move |s| Box::pin(fetcher(s)) as Pin>>); + let source = create_memo(cx, move |_| source()); + + let r = Rc::new(ResourceState { + scope: cx, + value, + set_value, + loading, + set_loading, + source, + fetcher, + resolved: Rc::new(Cell::new(resolved)), + scheduled: Rc::new(Cell::new(false)), + suspense_contexts: Default::default(), + }); + + let id = cx.runtime.create_unserializable_resource(Rc::clone(&r)); + + create_effect(cx, { + let r = Rc::clone(&r); + // This is a local resource, so we're always going to handle it on the + // client + move |_| r.load(false) + }); + + cx.with_scope_property(|prop| prop.push(ScopeProperty::Resource(id))); + + Resource { + runtime: cx.runtime, + id, + source_ty: PhantomData, + out_ty: PhantomData, + } +} + #[cfg(not(feature = "hydrate"))] fn load_resource(_cx: Scope, _id: ResourceId, r: Rc>) where S: PartialEq + Debug + Clone + 'static, - T: Debug + Clone + Serializable + 'static, + T: Debug + Clone + 'static, { r.load(false) } @@ -143,6 +255,8 @@ where if let Some(ref mut context) = *cx.runtime.shared_context.borrow_mut() { if let Some(data) = context.resolved_resources.remove(&id) { + // The server already sent us the serialized resource value, so + // deserialize & set it now context.pending_resources.remove(&id); // no longer pending r.resolved.set(true); @@ -153,6 +267,9 @@ where // for reactivity _ = r.source.try_with(|n| n.clone()); } else if context.pending_resources.remove(&id) { + // We're still waiting for the resource, add a "resolver" closure so + // that it will be set as soon as the server sends the serialized + // value r.set_loading.update(|n| *n = true); let resolve = { @@ -174,7 +291,7 @@ where &wasm_bindgen::JsValue::from_str("__LEPTOS_RESOURCE_RESOLVERS"), ) .expect_throw("no __LEPTOS_RESOURCE_RESOLVERS found in the JS global scope"); - let id = serde_json::to_string(&id).expect_throw("could not deserialize Resource ID"); + let id = serde_json::to_string(&id).expect_throw("could not serialize Resource ID"); _ = js_sys::Reflect::set( &resource_resolvers, &wasm_bindgen::JsValue::from_str(&id), @@ -184,6 +301,8 @@ where // for reactivity _ = r.source.get(); } else { + // Server didn't mark the resource as pending, so load it on the + // client r.load(false); } } else { @@ -421,7 +540,12 @@ where } } -pub(crate) trait AnyResource { +pub(crate) enum AnyResource { + Unserializable(Rc), + Serializable(Rc), +} + +pub(crate) trait SerializableResource { fn as_any(&self) -> &dyn Any; #[cfg(feature = "ssr")] @@ -431,7 +555,7 @@ pub(crate) trait AnyResource { ) -> Pin>>; } -impl AnyResource for ResourceState +impl SerializableResource for ResourceState where S: Debug + Clone, T: Clone + Debug + Serializable, @@ -449,3 +573,17 @@ where Box::pin(fut) } } + +pub(crate) trait UnserializableResource { + fn as_any(&self) -> &dyn Any; +} + +impl UnserializableResource for ResourceState +where + S: Debug + Clone, + T: Clone + Debug, +{ + fn as_any(&self) -> &dyn Any { + self + } +} diff --git a/leptos_reactive/src/runtime.rs b/leptos_reactive/src/runtime.rs index 94174ee..65ffbe6 100644 --- a/leptos_reactive/src/runtime.rs +++ b/leptos_reactive/src/runtime.rs @@ -28,7 +28,7 @@ pub(crate) struct Runtime { pub signal_subscribers: RefCell>>>, pub effects: RefCell>>>, pub effect_sources: RefCell>>>, - pub resources: RefCell>>, + pub resources: RefCell>, } impl Debug for Runtime { @@ -147,12 +147,30 @@ impl Runtime { Memo(read) } - pub(crate) fn create_resource(&self, state: Rc>) -> ResourceId + pub(crate) fn create_unserializable_resource( + &self, + state: Rc>, + ) -> ResourceId + where + S: Debug + Clone + 'static, + T: Debug + Clone + 'static, + { + self.resources + .borrow_mut() + .insert(AnyResource::Unserializable(state)) + } + + pub(crate) fn create_serializable_resource( + &self, + state: Rc>, + ) -> ResourceId where S: Debug + Clone + 'static, T: Debug + Clone + Serializable + 'static, { - self.resources.borrow_mut().insert(state) + self.resources + .borrow_mut() + .insert(AnyResource::Serializable(state)) } #[cfg(all(feature = "hydrate", not(feature = "ssr")))] @@ -194,7 +212,13 @@ impl Runtime { let resources = self.resources.borrow(); let res = resources.get(id); if let Some(res) = res { - if let Some(n) = res.as_any().downcast_ref::>() { + let res_state = match res { + AnyResource::Unserializable(res) => res.as_any(), + AnyResource::Serializable(res) => res.as_any(), + } + .downcast_ref::>(); + + if let Some(n) = res_state { f(n) } else { panic!( @@ -225,7 +249,9 @@ impl Runtime { > { let f = futures::stream::futures_unordered::FuturesUnordered::new(); for (id, resource) in self.resources.borrow().iter() { - f.push(resource.to_serialization_resolver(id)); + if let AnyResource::Serializable(resource) = resource { + f.push(resource.to_serialization_resolver(id)); + } } f }