Repository: leptos-rs/leptos (rust)

Issue:
Cancelling a timeout or animation frame request leaks the associated closure
**Describe the bug**
`request_animation_frame_with_handle` and `set_timeout_with_handle` convert the Rust closure into a Javascript one with `Closure::once_into_js`.  This function cleans up the Rust side once it's called, but if it never is then it is [documented to leak](https://rustwasm.github.io/wasm-bindgen/api/wasm_bindgen/closure/struct.Closure.html#method.once_into_js).

**Leptos Dependencies**

Please copy and paste the Leptos dependencies and features from your `Cargo.toml`.

For example:
```toml
leptos = { version = "0.6.5", features = ["csr"] }
```

**To Reproduce**
Steps to reproduce the behavior:
1. Create a timeout or a request for animation frame which closes over something whose Drop impl is observable e.g., by logging.
2. Cancel that timeout or animation frame.
3. Don't observe the drop

**Expected behavior**
Cancelled timeouts to drop their closures.


Pull request:
Make it so that cancelled timeouts don't leak
Instead of using `Closure::once_into_js`, this uses `into_js_value`, which uses weak references to clean up the closure when Javascript no longer has need of it.

It would be nice to make this (and the similar interval function) drop the callback promptly when cancelled, but I don't think that's possible while keeping the handles Copy.

Fixes #2330

Patch:
diff --git a/Cargo.toml b/Cargo.toml
index cb10f4cd..e0a33375 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -25,7 +25,7 @@ members = [
 exclude = ["benchmarks", "examples"]
 
 [workspace.package]
-version = "0.6.5"
+version = "0.6.6"
 
 [workspace.dependencies]
 leptos = { path = "./leptos", version = "0.6.5" }
diff --git a/examples/tailwind_axum/package-lock.json b/examples/tailwind_axum/package-lock.json
index bd1d8ef3..416d14e4 100644
--- a/examples/tailwind_axum/package-lock.json
+++ b/examples/tailwind_axum/package-lock.json
@@ -9,7 +9,6 @@
       "version": "1.0.0",
       "license": "ISC",
       "dependencies": {
-        "preline": "^1.8.0",
         "tailwindcss": "^3.3.2"
       }
     },
@@ -104,15 +103,6 @@
         "node": ">= 8"
       }
     },
-    "node_modules/@popperjs/core": {
-      "version": "2.11.8",
-      "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
-      "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/popperjs"
-      }
-    },
     "node_modules/any-promise": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
@@ -699,14 +689,6 @@
       "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
       "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
     },
-    "node_modules/preline": {
-      "version": "1.8.0",
-      "resolved": "https://registry.npmjs.org/preline/-/preline-1.8.0.tgz",
-      "integrity": "sha512-guttn86Fc/+AbvN9oKcr2z3zU7DL3Q5dl7nhcR4nTi5F02LXQc7WIYwgIXMR97kymCs52feiju6glXO3dUIpvA==",
-      "dependencies": {
-        "@popperjs/core": "^2.11.2"
-      }
-    },
     "node_modules/queue-microtask": {
       "version": "1.2.3",
       "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
diff --git a/examples/tailwind_axum/src/app.rs b/examples/tailwind_axum/src/app.rs
index 37e3b8f1..932096e0 100644
--- a/examples/tailwind_axum/src/app.rs
+++ b/examples/tailwind_axum/src/app.rs
@@ -7,8 +7,7 @@ pub fn App() -> impl IntoView {
     provide_meta_context();
 
     view! {
-
-        <Stylesheet id="leptos" href="/pkg/tailwind_axum.css"/>
+        <Stylesheet id="leptos" href="/pkg/leptos_tailwind.css"/>
         <Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
         <Router>
             <Routes>
diff --git a/examples/tailwind_csr/src/app.rs b/examples/tailwind_csr/src/app.rs
index fbbfb76a..c375cde8 100644
--- a/examples/tailwind_csr/src/app.rs
+++ b/examples/tailwind_csr/src/app.rs
@@ -8,7 +8,7 @@ pub fn App() -> impl IntoView {
 
     view! {
 
-        <Stylesheet id="leptos" href="/pkg/tailwind.css"/>
+        <Stylesheet id="leptos" href="/style/output.css"/>
         <Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
         <Router>
             <Routes>
diff --git a/leptos/Cargo.toml b/leptos/Cargo.toml
index 2e718b88..d56d618f 100644
--- a/leptos/Cargo.toml
+++ b/leptos/Cargo.toml
@@ -15,7 +15,7 @@ leptos_macro = { workspace = true }
 leptos_reactive = { workspace = true }
 leptos_server = { workspace = true }
 leptos_config = { workspace = true }
-leptos-spin-macro = { git = "https://github.com/fermyon/leptos-spin", optional = true}
+leptos-spin-macro = { version="0.1", optional = true}
 tracing = "0.1"
 typed-builder = "0.18"
 typed-builder-macro = "0.18"
diff --git a/leptos_dom/src/helpers.rs b/leptos_dom/src/helpers.rs
index 3dbb269b..99fe8a4e 100644
--- a/leptos_dom/src/helpers.rs
+++ b/leptos_dom/src/helpers.rs
@@ -103,6 +103,25 @@ pub fn request_animation_frame(cb: impl FnOnce() + 'static) {
     _ = request_animation_frame_with_handle(cb);
 }
 
+// Closure::once_into_js only frees the callback when it's actually
+// called, so this instead uses into_js_value, which can be freed by
+// the host JS engine's GC if it supports weak references (which all
+// modern brower engines do).  The way this works is that the provided
+// callback's captured data is dropped immediately after being called,
+// as before, but it leaves behind a small stub closure rust-side that
+// will be freed "eventually" by the JS GC.  If the function is never
+// called (e.g., it's a cancelled timeout or animation frame callback)
+// then it will also be freed eventually.
+fn closure_once(cb: impl FnOnce() + 'static) -> JsValue {
+    let mut wrapped_cb: Option<Box<dyn FnOnce()>> = Some(Box::new(cb));
+    let closure = Closure::new(move || {
+        if let Some(cb) = wrapped_cb.take() {
+            cb()
+        }
+    });
+    closure.into_js_value()
+}
+
 /// Runs the given function between the next repaint using
 /// [`Window.requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame),
 /// returning a cancelable handle.
@@ -128,7 +147,7 @@ pub fn request_animation_frame_with_handle(
             .map(AnimationFrameRequestHandle)
     }
 
-    raf(Closure::once_into_js(cb))
+    raf(closure_once(cb))
 }
 
 /// Handle that is generated by [request_idle_callback_with_handle] and can be
@@ -237,7 +256,7 @@ pub fn set_timeout_with_handle(
             .map(TimeoutHandle)
     }
 
-    st(Closure::once_into_js(cb), duration)
+    st(closure_once(cb), duration)
 }
 
 /// "Debounce" a callback function. This will cause it to wait for a period of `delay`
diff --git a/leptos_macro/src/lib.rs b/leptos_macro/src/lib.rs
index dc949643..6beebf32 100644
--- a/leptos_macro/src/lib.rs
+++ b/leptos_macro/src/lib.rs
@@ -883,6 +883,11 @@ pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
 ///     - `"GetCbor"`: `GET` request with URL-encoded arguments and CBOR response
 /// - `req` and `res` specify the HTTP request and response types to be used on the server (these
 ///   should usually only be necessary if you are integrating with a server other than Actix/Axum)
+/// - `impl_from`: specifies whether to implement trait `From` for server function's type or not.
+///   By default, if a server function only has one argument, the macro automatically implements the `From` trait
+///   to convert from the argument type to the server function type, and vice versa, allowing you to convert
+///   between them easily. Setting `impl_from` to `false` disables this, which can be necessary for argument types
+///   for which this would create a conflicting implementation. (defaults to `true`)
 ///
 /// ```rust,ignore
 /// #[server(
@@ -891,6 +896,7 @@ pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
 ///   endpoint = "my_fn",
 ///   input = Cbor,
 ///   output = Json
+///   impl_from = true
 /// )]
 /// pub async fn my_wacky_server_fn(input: Vec<String>) -> Result<usize, ServerFnError> {
 ///   todo!()
diff --git a/leptos_reactive/src/memo.rs b/leptos_reactive/src/memo.rs
index 858305a8..760a38d2 100644
--- a/leptos_reactive/src/memo.rs
+++ b/leptos_reactive/src/memo.rs
@@ -86,7 +86,88 @@ pub fn create_memo<T>(f: impl Fn(Option<&T>) -> T + 'static) -> Memo<T>
 where
     T: PartialEq + 'static,
 {
-    Runtime::current().create_memo(f)
+    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: PartialEq + 'static,
+{
+    Runtime::current().create_owning_memo(f)
 }
 
 /// An efficient derived reactive value based on other reactive values.
@@ -216,6 +297,65 @@ impl<T> Memo<T> {
     {
         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: PartialEq + 'static,
+    {
+        create_owning_memo(f)
+    }
 }
 
 impl<T> Clone for Memo<T>
@@ -524,8 +664,8 @@ impl_get_fn_traits![Memo];
 
 pub(crate) struct MemoState<T, F>
 where
-    T: PartialEq + 'static,
-    F: Fn(Option<&T>) -> T,
+    T: 'static,
+    F: Fn(Option<T>) -> (T, bool),
 {
     pub f: F,
     pub t: PhantomData<T>,
@@ -535,8 +675,8 @@ where
 
 impl<T, F> AnyComputation for MemoState<T, F>
 where
-    T: PartialEq + 'static,
-    F: Fn(Option<&T>) -> T,
+    T: 'static,
+    F: Fn(Option<T>) -> (T, bool),
 {
     #[cfg_attr(
         any(debug_assertions, feature = "ssr"),
@@ -551,24 +691,16 @@ where
         )
     )]
     fn run(&self, value: Rc<RefCell<dyn Any>>) -> bool {
-        let (new_value, is_different) = {
-            let value = value.borrow();
-            let curr_value = value
-                .downcast_ref::<Option<T>>()
-                .expect("to downcast memo value");
-
-            // run the effect
-            let new_value = (self.f)(curr_value.as_ref());
-            let is_different = curr_value.as_ref() != Some(&new_value);
-            (new_value, is_different)
-        };
-        if is_different {
-            let mut value = value.borrow_mut();
-            let curr_value = value
-                .downcast_mut::<Option<T>>()
-                .expect("to downcast memo value");
-            *curr_value = Some(new_value);
-        }
+        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
     }
diff --git a/leptos_reactive/src/runtime.rs b/leptos_reactive/src/runtime.rs
index 61deaf8c..2798fd61 100644
--- a/leptos_reactive/src/runtime.rs
+++ b/leptos_reactive/src/runtime.rs
@@ -1201,12 +1201,12 @@ impl RuntimeId {
 
     #[track_caller]
     #[inline(always)]
-    pub(crate) fn create_memo<T>(
+    pub(crate) fn create_owning_memo<T>(
         self,
-        f: impl Fn(Option<&T>) -> T + 'static,
+        f: impl Fn(Option<T>) -> (T, bool) + 'static,
     ) -> Memo<T>
     where
-        T: PartialEq + Any + 'static,
+        T: 'static,
     {
         Memo {
             id: self.create_concrete_memo(
diff --git a/meta/Cargo.toml b/meta/Cargo.toml
index 22b5d665..2392026e 100644
--- a/meta/Cargo.toml
+++ b/meta/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "leptos_meta"
-version = "0.6.5"
+version = "0.6.6"
 edition = "2021"
 authors = ["Greg Johnston"]
 license = "MIT"
diff --git a/router/Cargo.toml b/router/Cargo.toml
index 581ed123..10014559 100644
--- a/router/Cargo.toml
+++ b/router/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "leptos_router"
-version = "0.6.5"
+version = "0.6.6"
 edition = "2021"
 authors = ["Greg Johnston"]
 license = "MIT"
diff --git a/router/src/components/form.rs b/router/src/components/form.rs
index b128ac83..98e90408 100644
--- a/router/src/components/form.rs
+++ b/router/src/components/form.rs
@@ -1,6 +1,6 @@
 use crate::{
-    hooks::has_router, use_navigate, use_resolved_path, NavigateOptions,
-    ToHref, Url,
+    hooks::has_router, resolve_redirect_url, use_navigate, use_resolved_path,
+    NavigateOptions, ToHref, Url,
 };
 use leptos::{
     html::form,
@@ -447,8 +447,10 @@ where
 {
     let has_router = has_router();
     if !has_router {
-        _ = server_fn::redirect::set_redirect_hook(|path: &str| {
-            _ = window().location().set_href(path);
+        _ = server_fn::redirect::set_redirect_hook(|loc: &str| {
+            if let Some(url) = resolve_redirect_url(loc) {
+                _ = window().location().set_href(&url.href());
+            }
         });
     }
     let action_url = if let Some(url) = action.url() {
@@ -545,8 +547,10 @@ where
 {
     let has_router = has_router();
     if !has_router {
-        _ = server_fn::redirect::set_redirect_hook(|path: &str| {
-            _ = window().location().set_href(path);
+        _ = server_fn::redirect::set_redirect_hook(|loc: &str| {
+            if let Some(url) = resolve_redirect_url(loc) {
+                _ = window().location().set_href(&url.href());
+            }
         });
     }
     let action_url = if let Some(url) = action.url() {
diff --git a/router/src/components/router.rs b/router/src/components/router.rs
index 55056a7b..5ec2e04c 100644
--- a/router/src/components/router.rs
+++ b/router/src/components/router.rs
@@ -1,7 +1,7 @@
 use crate::{
-    create_location, matching::resolve_path, scroll_to_el, use_location,
-    use_navigate, Branch, History, Location, LocationChange, RouteContext,
-    RouterIntegrationContext, State,
+    create_location, matching::resolve_path, resolve_redirect_url,
+    scroll_to_el, use_location, use_navigate, Branch, History, Location,
+    LocationChange, RouteContext, RouterIntegrationContext, State,
 };
 #[cfg(not(feature = "ssr"))]
 use crate::{unescape, Url};
@@ -24,6 +24,7 @@ use std::{
 use thiserror::Error;
 #[cfg(not(feature = "ssr"))]
 use wasm_bindgen::JsCast;
+use wasm_bindgen::UnwrapThrowExt;
 
 static GLOBAL_ROUTERS_COUNT: AtomicUsize = AtomicUsize::new(0);
 
@@ -56,15 +57,24 @@ pub fn Router(
     // set server function redirect hook
     let navigate = use_navigate();
     let navigate = SendWrapper::new(navigate);
-    let router_hook = Box::new(move |path: &str| {
-        let path = path.to_string();
-        // delay by a tick here, so that the Action updates *before* the redirect
-        request_animation_frame({
+    let router_hook = Box::new(move |loc: &str| {
+        let Some(url) = resolve_redirect_url(loc) else {
+            return; // resolve_redirect_url() already logs an error
+        };
+        let current_origin =
+            leptos_dom::helpers::location().origin().unwrap_throw();
+        if url.origin() == current_origin {
             let navigate = navigate.clone();
-            move || {
-                navigate(&path, Default::default());
-            }
-        });
+            // delay by a tick here, so that the Action updates *before* the redirect
+            request_animation_frame(move || {
+                navigate(&url.pathname(), Default::default());
+            });
+            // Use set_href() if the conditions for client-side navigation were not satisfied
+        } else if let Err(e) =
+            leptos_dom::helpers::location().set_href(&url.href())
+        {
+            leptos::logging::error!("Failed to redirect: {e:#?}");
+        }
     }) as RedirectHook;
     _ = server_fn::redirect::set_redirect_hook(router_hook);
 
diff --git a/router/src/hooks.rs b/router/src/hooks.rs
index d9a9fc12..0c53689a 100644
--- a/router/src/hooks.rs
+++ b/router/src/hooks.rs
@@ -3,8 +3,8 @@ use crate::{
     RouterContext,
 };
 use leptos::{
-    create_memo, request_animation_frame, signal_prelude::*, use_context, Memo,
-    Oco,
+    create_memo, request_animation_frame, signal_prelude::*, use_context,
+    window, Memo, Oco,
 };
 use std::{rc::Rc, str::FromStr};
 
@@ -216,3 +216,28 @@ pub(crate) fn use_is_back_navigation() -> ReadSignal<bool> {
     let router = use_router();
     router.inner.is_back.read_only()
 }
+
+/// Resolves a redirect location to an (absolute) URL.
+pub(crate) fn resolve_redirect_url(loc: &str) -> Option<web_sys::Url> {
+    let origin = match window().location().origin() {
+        Ok(origin) => origin,
+        Err(e) => {
+            leptos::logging::error!("Failed to get origin: {:#?}", e);
+            return None;
+        }
+    };
+
+    // TODO: Use server function's URL as base instead.
+    let base = origin;
+
+    match web_sys::Url::new_with_base(loc, &base) {
+        Ok(url) => Some(url),
+        Err(e) => {
+            leptos::logging::error!(
+                "Invalid redirect location: {}",
+                e.as_string().unwrap_or_default(),
+            );
+            None
+        }
+    }
+}
diff --git a/server_fn/src/redirect.rs b/server_fn/src/redirect.rs
index aa279d39..e20968a1 100644
--- a/server_fn/src/redirect.rs
+++ b/server_fn/src/redirect.rs
@@ -25,9 +25,9 @@ pub fn set_redirect_hook(
     REDIRECT_HOOK.set(Box::new(hook))
 }
 
-/// Calls the hook that has been set by [`set_redirect_hook`] to redirect to `path`.
-pub fn call_redirect_hook(path: &str) {
+/// Calls the hook that has been set by [`set_redirect_hook`] to redirect to `loc`.
+pub fn call_redirect_hook(loc: &str) {
     if let Some(hook) = REDIRECT_HOOK.get() {
-        hook(path)
+        hook(loc)
     }
 }
diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs
index 613987aa..8560d555 100644
--- a/server_fn_macro/src/lib.rs
+++ b/server_fn_macro/src/lib.rs
@@ -119,6 +119,7 @@ pub fn server_macro_impl(
         res_ty,
         client,
         custom_wrapper,
+        impl_from,
     } = args;
     let prefix = prefix.unwrap_or_else(|| Literal::string(default_path));
     let fn_path = fn_path.unwrap_or_else(|| Literal::string(""));
@@ -206,8 +207,11 @@ pub fn server_macro_impl(
         FnArg::Receiver(_) => None,
         FnArg::Typed(t) => Some((&t.pat, &t.ty)),
     });
-    let from_impl =
-        (body.inputs.len() == 1 && first_field.is_some()).then(|| {
+    let impl_from = impl_from.map(|v| v.value).unwrap_or(true);
+    let from_impl = (body.inputs.len() == 1
+        && first_field.is_some()
+        && impl_from)
+        .then(|| {
             let field = first_field.unwrap();
             let (name, ty) = field;
             quote! {
@@ -676,6 +680,7 @@ struct ServerFnArgs {
     client: Option<Type>,
     custom_wrapper: Option<Path>,
     builtin_encoding: bool,
+    impl_from: Option<LitBool>,
 }
 
 impl Parse for ServerFnArgs {
@@ -693,6 +698,7 @@ impl Parse for ServerFnArgs {
         let mut res_ty: Option<Type> = None;
         let mut client: Option<Type> = None;
         let mut custom_wrapper: Option<Path> = None;
+        let mut impl_from: Option<LitBool> = None;
 
         let mut use_key_and_value = false;
         let mut arg_pos = 0;
@@ -800,6 +806,14 @@ impl Parse for ServerFnArgs {
                             ));
                         }
                         custom_wrapper = Some(stream.parse()?);
+                    } else if key == "impl_from" {
+                        if impl_from.is_some() {
+                            return Err(syn::Error::new(
+                                key.span(),
+                                "keyword argument repeated: `impl_from`",
+                            ));
+                        }
+                        impl_from = Some(stream.parse()?);
                     } else {
                         return Err(lookahead.error());
                     }
@@ -895,6 +909,7 @@ impl Parse for ServerFnArgs {
             res_ty,
             client,
             custom_wrapper,
+            impl_from,
         })
     }
 }


Test patch:
diff --git a/leptos_reactive/tests/memo.rs b/leptos_reactive/tests/memo.rs
index 35eb8d9a..0d072cd0 100644
--- a/leptos_reactive/tests/memo.rs
+++ b/leptos_reactive/tests/memo.rs
@@ -212,3 +212,84 @@ fn dynamic_dependencies() {
 
     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_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);
+
+    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();
+}