Repository: leptos-rs/leptos
Pull request #1628 (2023-09-02 05:30:21)

feat: Add dynamically resolved attributes
This is a followup to #1619, everything seems to work as expected when I did some testing, however `cargo test` fails. I think the failures are just it not adjusting the snapshots for the new expected output (I'm new here and have no idea how to fix that, if anyone has any advice that'd be wonderful!).

gbj: Thanks for this. A couple things:
- Could you give an example of usage? I'm not sure exactly how this is supposed to work.
- Naming: I don't love the `DynAttrs`/`dyn_attrs`/`dyn:` naming. Maybe something like `additional_attributes` or just `attributes`, and `attr:` in the view? (Although I recognize the potential for name conflicts with other user-defined props here)
- I think the reason the tests are failing is that this line will *always* add at least `.dyn_attrs(vec![])` to every component
```rust
    let dyn_attrs = quote! { .dyn_attrs(vec![#(#dyn_attrs),*]) };
```
I'd suggest checking if `dyn_attrs.is_empty()` and just using `quote! {}` in that case so that this does nothing if you don't add additional attributes.
mrvillage: Agh! I keep forgetting to include examples sorry! I put one to show off the syntax below:
```rs
#[component]
pub fn Input(#[prop(dyn_attrs)] attrs: Vec<(&'static str, Attribute)>) -> impl IntoView {
    view! { <input type="text" {..attrs} /> }
}

<Input dyn:value="hello" dyn:label=label />
```

I'm happy to change the naming if you'd like, I decided to build the naming off the `dyn:` prefix to show that they're dynamic attributes, if you'd rather me used `attr:` and/or `additional_attributes` then let me know!
Also the `.is_empty` check seems to have helped, but it still seems to be having snapshot issues with generated code since it changed the format slightly, not sure what the correct way to address that is sorry.
gbj: Okay, so I went ahead and renamed to `#[prop(attrs)]` and `attr:`, while leaving the trait and method with the same name. And I think I should have fixed that test -- it was just a question of updating the error message to include `attrs` as a valid prop type, which is perfect.

I removed the import of `use leptos::DynAttr`, which was the other big diff in the snapshot test. If someone is using `use leptos::*`, it's imported automatically, and if not the compiler gives a useful error saying it should be imported. I'm open to putting this back in, or a different way of handling the need to import that trait, though.
gbj: Oh, I should add, here's a current example:
```rust
#[component]
pub fn App() -> impl IntoView {
    view! {
        <Input attr:value="hello" attr:label="foo" />
        <Input attr:type="number" attr:value="0" />
    }
}

#[component]
pub fn Input(
    #[prop(attrs)] attrs: Vec<(&'static str, Attribute)>,
) -> impl IntoView {
    view! {
        <input {..attrs} />
        <pre>{format!("{attrs2:#?}")}</pre>
    }
}
```

Patch:
diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md
index 07209adc..829880c2 100644
--- a/docs/book/src/SUMMARY.md
+++ b/docs/book/src/SUMMARY.md
@@ -47,3 +47,4 @@
 - [Deployment](./deployment.md)
 - [Appendix: How Does the Reactive System Work?](./appendix_reactive_graph.md)
 - [Appendix: Optimizing WASM Binary Size](./appendix_binary_size.md)
+- [Appendix: Some Small DX Improvements](./appending_dx.md)
diff --git a/docs/book/src/appendix_dx.md b/docs/book/src/appendix_dx.md
new file mode 100644
index 00000000..1010ed6d
--- /dev/null
+++ b/docs/book/src/appendix_dx.md
@@ -0,0 +1,49 @@
+# A Running List of Small Developer Experience Improvements
+
+## Autocompletion inside `#[component]` and `#[server]`
+
+Because of the nature of macros (they can expand from anything to anything, but only if the input is exactly correct at that instant) it can be hard for rust-analyzer to do proper autocompletion and other support.
+
+But you can tell rust-analyzer to ignore certain proc macros. For `#[component]` and `#[server]` especially, which annotate function bodies but don't actually transform anything inside the body of your function, this can be really helpful.
+
+Note that this means that rust-analyzer doesn't know about your component props, which may generate its own set of errors or warnings in the IDE.
+
+VSCode `settings.json`:
+
+```json
+"rust-analyzer.procMacro.ignored": {
+	"leptos_macro": [
+		"server",
+		"component"
+	],
+}
+```
+
+neovim with lspconfig:
+
+```lua
+require('lspconfig').rust_analyzer.setup {
+  -- Other Configs ...
+  settings = {
+    ["rust-analyzer"] = {
+      -- Other Settings ...
+      procMacro = {
+        ignored = {
+            leptos_macro = {
+                "server",
+                "component",
+            },
+        },
+      },
+    },
+  }
+}
+```
+
+Helix, in `.helix/languages.toml`:
+
+```toml
+[[language]]
+name = "rust"
+config = { procMacro = {ignored = {leptos_macro = ["component"]}}}
+```
diff --git a/docs/book/src/interlude_projecting_children.md b/docs/book/src/interlude_projecting_children.md
index 39209808..312e63e7 100644
--- a/docs/book/src/interlude_projecting_children.md
+++ b/docs/book/src/interlude_projecting_children.md
@@ -89,7 +89,7 @@ We can solve this problem by using the [`store_value`](https://docs.rs/leptos/la
 In this case, it’s really simple:
 
 ```rust
-pub fn LoggedIn<F, IV>(F, children: ChildrenFn) -> impl IntoView
+pub fn LoggedIn<F, IV>(fallback: F, children: ChildrenFn) -> impl IntoView
 where
     F: Fn() -> IV + 'static,
     IV: IntoView,
diff --git a/docs/book/src/router/16_routes.md b/docs/book/src/router/16_routes.md
index 927c668a..e84fbefa 100644
--- a/docs/book/src/router/16_routes.md
+++ b/docs/book/src/router/16_routes.md
@@ -101,3 +101,44 @@ Now if you navigate to `/` or to `/users` you’ll get the home page or the `<Us
 Note that you can define your routes in any order. The router scores each route to see how good a match it is, rather than simply trying to match them top to bottom.
 
 Simple enough?
+
+## Conditional Routes
+
+`leptos_router` is based on the assumption that you have one and only one `<Routes/>` component in your app. It uses this to generate routes on the server side, optimize route matching by caching calculated branches, and render your application.
+
+You should not conditionally render `<Routes/>` using another component like `<Show/>` or `<Suspense/>`.
+
+```rust
+// ❌ don't do this!
+view! {
+  <Show when=|| is_loaded() fallback=|| view! { <p>"Loading"</p> }>
+    <Routes>
+      <Route path="/" view=Home/>
+    </Routes>
+  </Show>
+}
+```
+
+Instead, you can use nested routing to render your `<Routes/>` once, and conditionally render the router outlet:
+
+```rust
+// ✅ do this instead!
+view! {
+  <Routes>
+    // parent route
+    <Route path="/" view=move || {
+      view! {
+        // only show the outlet if data have loaded
+        <Show when=|| is_loaded() fallback=|| view! { <p>"Loading"</p> }>
+          <Outlet/>
+        </Show>
+      }
+    }>
+      // nested child route
+      <Route path="/" view=Home/>
+    </Route>
+  </Routes>
+}
+```
+
+If this looks bizarre, don’t worry! The next section of the book is about this kind of nested routing.
diff --git a/docs/book/src/router/17_nested_routing.md b/docs/book/src/router/17_nested_routing.md
index 854ec7d8..340ebbe5 100644
--- a/docs/book/src/router/17_nested_routing.md
+++ b/docs/book/src/router/17_nested_routing.md
@@ -153,6 +153,43 @@ pub fn ContactList() -> impl IntoView {
 }
 ```
 
+## Refactoring Route Definitions
+
+You don’t need to define all your routes in one place if you don’t want to. You can refactor any `<Route/>` and its children out into a separate component.
+
+For example, you can refactor the example above to use two separate components:
+
+```rust
+#[component]
+fn App() -> impl IntoView {
+  view! {
+    <Router>
+      <Routes>
+        <Route path="/contacts" view=ContactList>
+          <ContactInfoRoutes/>
+          <Route path="" view=|| view! {
+            <p>"Select a contact to view more info."</p>
+          }/>
+        </Route>
+      </Routes>
+    </Router>
+  }
+}
+
+#[component(transparent)]
+fn ContactInfoRoutes() -> impl IntoView {
+  view! {
+    <Route path=":id" view=ContactInfo>
+      <Route path="" view=EmailAndPhone/>
+      <Route path="address" view=Address/>
+      <Route path="messages" view=Messages/>
+    </Route>
+  }
+}
+```
+
+This second component is a `#[component(transparent)]`, meaning it just returns its data, not a view: in this case, it's a [`RouteDefinition`](https://docs.rs/leptos_router/latest/leptos_router/struct.RouteDefinition.html) struct, which is what the `<Route/>` returns. As long as it is marked `#[component(transparent)]`, this sub-route can be defined wherever you want, and inserted as a component into your tree of route definitions.
+
 ## Nested Routing and Performance
 
 All of this is nice, conceptually, but again—what’s the big deal?
diff --git a/examples/hackernews/src/api.rs b/examples/hackernews/src/api.rs
index b30bbb3c..99c44c6e 100644
--- a/examples/hackernews/src/api.rs
+++ b/examples/hackernews/src/api.rs
@@ -17,8 +17,7 @@ where
     let abort_controller = web_sys::AbortController::new().ok();
     let abort_signal = abort_controller.as_ref().map(|a| a.signal());
 
-    // abort in-flight requests if the Scope is disposed
-    // i.e., if we've navigated away from this page
+    // abort in-flight requests if, e.g., we've navigated away from this page
     leptos::on_cleanup(move || {
         if let Some(abort_controller) = abort_controller {
             abort_controller.abort()
diff --git a/examples/hackernews_axum/src/api.rs b/examples/hackernews_axum/src/api.rs
index b30bbb3c..92d66937 100644
--- a/examples/hackernews_axum/src/api.rs
+++ b/examples/hackernews_axum/src/api.rs
@@ -17,8 +17,7 @@ where
     let abort_controller = web_sys::AbortController::new().ok();
     let abort_signal = abort_controller.as_ref().map(|a| a.signal());
 
-    // abort in-flight requests if the Scope is disposed
-    // i.e., if we've navigated away from this page
+    // abort in-flight requests if e.g., we've navigated away from this page
     leptos::on_cleanup(move || {
         if let Some(abort_controller) = abort_controller {
             abort_controller.abort()
diff --git a/examples/ssr_modes/src/app.rs b/examples/ssr_modes/src/app.rs
index e7dfb649..8d167d4f 100644
--- a/examples/ssr_modes/src/app.rs
+++ b/examples/ssr_modes/src/app.rs
@@ -88,9 +88,6 @@ fn Post() -> impl IntoView {
         }
     });
 
-    // this view needs to take the `Scope` from the `<Suspense/>`, not
-    // from the parent component, so we take that as an argument and
-    // pass it in under the `<Suspense/>` so that it is correct
     let post_view = move || {
         post.and_then(|post| {
             view! {
diff --git a/examples/ssr_modes_axum/src/app.rs b/examples/ssr_modes_axum/src/app.rs
index a34cb1dd..45923754 100644
--- a/examples/ssr_modes_axum/src/app.rs
+++ b/examples/ssr_modes_axum/src/app.rs
@@ -88,9 +88,6 @@ fn Post() -> impl IntoView {
         }
     });
 
-    // this view needs to take the `Scope` from the `<Suspense/>`, not
-    // from the parent component, so we take that as an argument and
-    // pass it in under the `<Suspense/>` so that it is correct
     let post_view = move || {
         post.and_then(|post| {
             view! {
@@ -123,7 +120,7 @@ fn Post() -> impl IntoView {
                     </div>
                 }
             }>
-                {post_view()}
+                {post_view}
             </ErrorBoundary>
         </Suspense>
     }
diff --git a/integrations/actix/src/lib.rs b/integrations/actix/src/lib.rs
index 6a4cef27..601ce645 100644
--- a/integrations/actix/src/lib.rs
+++ b/integrations/actix/src/lib.rs
@@ -56,25 +56,23 @@ impl ResponseParts {
     }
 }
 
-/// Adding this Struct to your Scope inside of a Server Fn or Elements will allow you to override details of the Response
-/// like StatusCode and add Headers/Cookies. Because Elements and Server Fns are lower in the tree than the Response generation
-/// code, it needs to be wrapped in an `Arc<RwLock<>>` so that it can be surfaced
+/// Allows you to override details of the HTTP response like the status code and add Headers/Cookies.
 #[derive(Debug, Clone, Default)]
 pub struct ResponseOptions(pub Arc<RwLock<ResponseParts>>);
 
 impl ResponseOptions {
-    /// A less boilerplatey way to overwrite the contents of `ResponseOptions` with a new `ResponseParts`
+    /// A simpler way to overwrite the contents of `ResponseOptions` with a new `ResponseParts`.
     pub fn overwrite(&self, parts: ResponseParts) {
         let mut writable = self.0.write();
         *writable = parts
     }
-    /// Set the status of the returned Response
+    /// Set the status of the returned Response.
     pub fn set_status(&self, status: StatusCode) {
         let mut writeable = self.0.write();
         let res_parts = &mut *writeable;
         res_parts.status = Some(status);
     }
-    /// Insert a header, overwriting any previous value with the same key
+    /// Insert a header, overwriting any previous value with the same key.
     pub fn insert_header(
         &self,
         key: header::HeaderName,
@@ -84,7 +82,7 @@ impl ResponseOptions {
         let res_parts = &mut *writeable;
         res_parts.headers.insert(key, value);
     }
-    /// Append a header, leaving any header with the same key intact
+    /// Append a header, leaving any header with the same key intact.
     pub fn append_header(
         &self,
         key: header::HeaderName,
diff --git a/integrations/axum/src/lib.rs b/integrations/axum/src/lib.rs
index 938ac504..b10f4dce 100644
--- a/integrations/axum/src/lib.rs
+++ b/integrations/axum/src/lib.rs
@@ -83,31 +83,29 @@ impl ResponseParts {
     }
 }
 
-/// Adding this Struct to your Scope inside of a Server Fn or Element will allow you to override details of the Response
-/// like status and add Headers/Cookies. Because Elements and Server Fns are lower in the tree than the Response generation
-/// code, it needs to be wrapped in an `Arc<RwLock<>>` so that it can be surfaced.
+/// Allows you to override details of the HTTP response like the status code and add Headers/Cookies.
 #[derive(Debug, Clone, Default)]
 pub struct ResponseOptions(pub Arc<RwLock<ResponseParts>>);
 
 impl ResponseOptions {
-    /// A less boilerplatey way to overwrite the contents of `ResponseOptions` with a new `ResponseParts`
+    /// A simpler way to overwrite the contents of `ResponseOptions` with a new `ResponseParts`.
     pub fn overwrite(&self, parts: ResponseParts) {
         let mut writable = self.0.write();
         *writable = parts
     }
-    /// Set the status of the returned Response
+    /// Set the status of the returned Response.
     pub fn set_status(&self, status: StatusCode) {
         let mut writeable = self.0.write();
         let res_parts = &mut *writeable;
         res_parts.status = Some(status);
     }
-    /// Insert a header, overwriting any previous value with the same key
+    /// Insert a header, overwriting any previous value with the same key.
     pub fn insert_header(&self, key: HeaderName, value: HeaderValue) {
         let mut writeable = self.0.write();
         let res_parts = &mut *writeable;
         res_parts.headers.insert(key, value);
     }
-    /// Append a header, leaving any header with the same key intact
+    /// Append a header, leaving any header with the same key intact.
     pub fn append_header(&self, key: HeaderName, value: HeaderValue) {
         let mut writeable = self.0.write();
         let res_parts = &mut *writeable;
@@ -1478,14 +1476,19 @@ impl ExtractorHelper {
         }
     }
 
-    pub async fn extract<F, T, U>(&self, f: F) -> Result<U, T::Rejection>
+    pub async fn extract<F, T, U, S>(
+        &self,
+        f: F,
+        s: S,
+    ) -> Result<U, T::Rejection>
     where
-        F: Extractor<T, U>,
-        T: std::fmt::Debug + Send + FromRequestParts<()> + 'static,
+        S: Sized,
+        F: Extractor<T, U, S>,
+        T: std::fmt::Debug + Send + FromRequestParts<S> + 'static,
         T::Rejection: std::fmt::Debug + Send + 'static,
     {
         let mut parts = self.parts.lock().await;
-        let data = T::from_request_parts(&mut parts, &()).await?;
+        let data = T::from_request_parts(&mut parts, &s).await?;
         Ok(f.call(data).await)
     }
 }
@@ -1518,39 +1521,74 @@ impl<B> From<Request<B>> for ExtractorHelper {
 /// }
 /// ```
 ///
-/// > Note: For now, the Axum `extract` function only supports extractors for
-/// which the state is `()`, i.e., you can't yet use it to extract `State(_)`.
-/// You can access `State(_)` by using a custom handler that extracts the state
-/// and then provides it via context.
-/// [Click here for an example](https://github.com/leptos-rs/leptos/blob/a5f73b441c079f9138102b3a7d8d4828f045448c/examples/session_auth_axum/src/main.rs#L91-L92).
+/// > This function only supports extractors for
+/// which the state is `()`. If the state is not `()`, use [`extract_with_state`].
 #[tracing::instrument(level = "trace", fields(error), skip_all)]
-pub async fn extract<T, U>(f: impl Extractor<T, U>) -> Result<U, T::Rejection>
+pub async fn extract<T, U>(
+    f: impl Extractor<T, U, ()>,
+) -> Result<U, T::Rejection>
 where
     T: std::fmt::Debug + Send + FromRequestParts<()> + 'static,
     T::Rejection: std::fmt::Debug + Send + 'static,
+{
+    extract_with_state((), f).await
+}
+
+/// A helper to make it easier to use Axum extractors in server functions. This takes
+/// a handler function and state as its arguments. The handler rules similar to Axum
+/// [handlers](https://docs.rs/axum/latest/axum/extract/index.html#intro): it is an async function
+/// whose arguments are “extractors.”
+///
+/// ```rust,ignore
+/// #[server(QueryExtract, "/api")]
+/// pub async fn query_extract() -> Result<String, ServerFnError> {
+///     use axum::{extract::Query, http::Method};
+///     use leptos_axum::extract;
+///     let state: ServerState = use_context::<crate::ServerState>()
+///          .ok_or(ServerFnError::ServerError("No server state".to_string()))?;
+///
+///     extract_with_state(&state, |method: Method, res: Query<MyQuery>| async move {
+///             format!("{method:?} and {}", res.q)
+///         },
+///     )
+///     .await
+///     .map_err(|e| ServerFnError::ServerError("Could not extract method and query...".to_string()))
+/// }
+/// ```
+#[tracing::instrument(level = "trace", fields(error), skip_all)]
+pub async fn extract_with_state<T, U, S>(
+    state: S,
+    f: impl Extractor<T, U, S>,
+) -> Result<U, T::Rejection>
+where
+    S: Sized,
+    T: std::fmt::Debug + Send + FromRequestParts<S> + 'static,
+    T::Rejection: std::fmt::Debug + Send + 'static,
 {
     use_context::<ExtractorHelper>()
         .expect(
             "should have had ExtractorHelper provided by the leptos_axum \
              integration",
         )
-        .extract(f)
+        .extract(f, state)
         .await
 }
 
-pub trait Extractor<T, U>
+pub trait Extractor<T, U, S>
 where
-    T: FromRequestParts<()>,
+    S: Sized,
+    T: FromRequestParts<S>,
 {
     fn call(self, args: T) -> Pin<Box<dyn Future<Output = U>>>;
 }
 
 macro_rules! factory_tuple ({ $($param:ident)* } => {
-    impl<Func, Fut, U, $($param,)*> Extractor<($($param,)*), U> for Func
+    impl<Func, Fut, U, S, $($param,)*> Extractor<($($param,)*), U, S> for Func
     where
-        $($param: FromRequestParts<()> + Send,)*
+        $($param: FromRequestParts<S> + Send,)*
         Func: FnOnce($($param),*) -> Fut + 'static,
         Fut: Future<Output = U> + 'static,
+        S: Sized + Send + Sync
     {
         #[inline]
         #[allow(non_snake_case)]
diff --git a/integrations/viz/src/lib.rs b/integrations/viz/src/lib.rs
index 087379f5..79d2d0c2 100644
--- a/integrations/viz/src/lib.rs
+++ b/integrations/viz/src/lib.rs
@@ -59,31 +59,29 @@ impl ResponseParts {
     }
 }
 
-/// Adding this Struct to your Scope inside of a Server Fn or Element will allow you to override details of the Response
-/// like status and add Headers/Cookies. Because Elements and Server Fns are lower in the tree than the Response generation
-/// code, it needs to be wrapped in an `Arc<RwLock<>>` so that it can be surfaced.
+/// Allows you to override details of the HTTP response like the status code and add Headers/Cookies.
 #[derive(Debug, Clone, Default)]
 pub struct ResponseOptions(pub Arc<RwLock<ResponseParts>>);
 
 impl ResponseOptions {
-    /// A less boilerplatey way to overwrite the contents of `ResponseOptions` with a new `ResponseParts`
+    /// A simpler way to overwrite the contents of `ResponseOptions` with a new `ResponseParts`.
     pub fn overwrite(&self, parts: ResponseParts) {
         let mut writable = self.0.write();
         *writable = parts
     }
-    /// Set the status of the returned Response
+    /// Set the status of the returned Response.
     pub fn set_status(&self, status: StatusCode) {
         let mut writeable = self.0.write();
         let res_parts = &mut *writeable;
         res_parts.status = Some(status);
     }
-    /// Insert a header, overwriting any previous value with the same key
+    /// Insert a header, overwriting any previous value with the same key.
     pub fn insert_header(&self, key: HeaderName, value: HeaderValue) {
         let mut writeable = self.0.write();
         let res_parts = &mut *writeable;
         res_parts.headers.insert(key, value);
     }
-    /// Append a header, leaving any header with the same key intact
+    /// Append a header, leaving any header with the same key intact.
     pub fn append_header(&self, key: HeaderName, value: HeaderValue) {
         let mut writeable = self.0.write();
         let res_parts = &mut *writeable;
diff --git a/leptos/Cargo.toml b/leptos/Cargo.toml
index 4717b491..f77286c9 100644
--- a/leptos/Cargo.toml
+++ b/leptos/Cargo.toml
@@ -19,8 +19,8 @@ leptos_config = { workspace = true }
 tracing = "0.1"
 typed-builder = "0.16"
 typed-builder-macro = "0.16"
-serde = { optional = true }
-serde_json = { optional = true }
+serde = { version = "1", optional = true }
+serde_json = { version = "1", optional = true }
 server_fn = { workspace = true }
 web-sys = { version = "0.3.63", optional = true }
 wasm-bindgen = { version = "0.2", optional = true }
diff --git a/leptos/src/lib.rs b/leptos/src/lib.rs
index 4ed2bf07..e2b242b2 100644
--- a/leptos/src/lib.rs
+++ b/leptos/src/lib.rs
@@ -265,6 +265,18 @@ pub trait Props {
     fn builder() -> Self::Builder;
 }
 
+#[doc(hidden)]
+pub trait DynAttrs {
+    fn dyn_attrs(self, _args: Vec<(&'static str, Attribute)>) -> Self
+    where
+        Self: Sized,
+    {
+        self
+    }
+}
+
+impl DynAttrs for () {}
+
 #[doc(hidden)]
 pub trait PropsOrNoPropsBuilder {
     type Builder;
diff --git a/leptos_dom/src/html.rs b/leptos_dom/src/html.rs
index a34783aa..d82e1e4e 100644
--- a/leptos_dom/src/html.rs
+++ b/leptos_dom/src/html.rs
@@ -34,6 +34,7 @@ cfg_if! {
         v
     }
 
+    #[allow(unused)]
     fn is_meta_tag() -> bool {
         IS_META.with(|m| m.get())
     }
@@ -196,6 +197,7 @@ impl Custom {
 
         #[cfg(all(target_arch = "wasm32", feature = "web"))]
         let element = if HydrationCtx::is_hydrating() && id.is_some() {
+            #[allow(unused)]
             let id = id.unwrap();
             #[cfg(feature = "hydrate")]
             if let Some(el) = crate::hydration::get_element(&id.to_string()) {
@@ -577,6 +579,20 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
         name: impl Into<Oco<'static, str>>,
         attr: impl IntoAttribute,
     ) -> Self {
+        #[cfg(all(debug_assertions, feature = "ssr"))]
+        {
+            if matches!(self.children, ElementChildren::Chunks(_)) {
+                let location = std::panic::Location::caller();
+                crate::warn!(
+                    "\n\nWARNING: At {location}, you call .attr() on an \
+                     HtmlElement<_> that was created with the `view!` macro. \
+                     The macro applies optimizations during SSR that prevent \
+                     calling this method successfully. You should not mix the \
+                     `view` macro and the builder syntax when using SSR.\n\n",
+                );
+            }
+        }
+
         let name = name.into();
 
         #[cfg(all(target_arch = "wasm32", feature = "web"))]
@@ -646,6 +662,20 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
         name: impl Into<Oco<'static, str>>,
         class: impl IntoClass,
     ) -> Self {
+        #[cfg(all(debug_assertions, feature = "ssr"))]
+        {
+            if matches!(self.children, ElementChildren::Chunks(_)) {
+                let location = std::panic::Location::caller();
+                crate::warn!(
+                    "\n\nWARNING: At {location}, you call .class() on an \
+                     HtmlElement<_> that was created with the `view!` macro. \
+                     The macro applies optimizations during SSR that prevent \
+                     calling this method successfully. You should not mix the \
+                     `view` macro and the builder syntax when using SSR.\n\n",
+                );
+            }
+        }
+
         let name = name.into();
 
         #[cfg(all(target_arch = "wasm32", feature = "web"))]
@@ -696,6 +726,20 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
     #[track_caller]
     #[inline(always)]
     pub fn classes(self, classes: impl Into<Oco<'static, str>>) -> Self {
+        #[cfg(all(debug_assertions, feature = "ssr"))]
+        {
+            if matches!(self.children, ElementChildren::Chunks(_)) {
+                let location = std::panic::Location::caller();
+                crate::warn!(
+                    "\n\nWARNING: At {location}, you call .classes() on an \
+                     HtmlElement<_> that was created with the `view!` macro. \
+                     The macro applies optimizations during SSR that prevent \
+                     calling this method successfully. You should not mix the \
+                     `view` macro and the builder syntax when using SSR.\n\n",
+                );
+            }
+        }
+
         self.classes_inner(&classes.into())
     }
 
@@ -709,6 +753,21 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
         I: IntoIterator<Item = C>,
         C: Into<Oco<'static, str>>,
     {
+        #[cfg(all(debug_assertions, feature = "ssr"))]
+        {
+            if matches!(self.children, ElementChildren::Chunks(_)) {
+                let location = std::panic::Location::caller();
+                crate::warn!(
+                    "\n\nWARNING: At {location}, you call .dyn_classes() on \
+                     an HtmlElement<_> that was created with the `view!` \
+                     macro. The macro applies optimizations during SSR that \
+                     prevent calling this method successfully. You should not \
+                     mix the `view` macro and the builder syntax when using \
+                     SSR.\n\n",
+                );
+            }
+        }
+
         #[cfg(all(target_arch = "wasm32", feature = "web"))]
         {
             use smallvec::SmallVec;
@@ -809,6 +868,20 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
         name: impl Into<Oco<'static, str>>,
         style: impl IntoStyle,
     ) -> Self {
+        #[cfg(all(debug_assertions, feature = "ssr"))]
+        {
+            if matches!(self.children, ElementChildren::Chunks(_)) {
+                let location = std::panic::Location::caller();
+                crate::warn!(
+                    "\n\nWARNING: At {location}, you call .style() on an \
+                     HtmlElement<_> that was created with the `view!` macro. \
+                     The macro applies optimizations during SSR that prevent \
+                     calling this method successfully. You should not mix the \
+                     `view` macro and the builder syntax when using SSR.\n\n",
+                );
+            }
+        }
+
         let name = name.into();
 
         #[cfg(all(target_arch = "wasm32", feature = "web"))]
@@ -1004,12 +1077,30 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
                 ElementChildren::Children(ref mut children) => {
                     children.push(child);
                 }
-                _ => {
-                    crate::debug_warn!(
-                        "Don’t call .child() on an HtmlElement if you’ve \
-                         already called .inner_html() or \
-                         HtmlElement::from_chunks()."
-                    );
+                ElementChildren::InnerHtml(_) => {
+                    #[cfg(debug_assertions)]
+                    {
+                        let location = std::panic::Location::caller();
+                        crate::debug_warn!(
+                            "At {location}, you call .child() on an HTML \
+                             element that already had inner_html provided. \
+                             This will have no effect."
+                        );
+                    }
+                }
+                ElementChildren::Chunks(_) => {
+                    #[cfg(debug_assertions)]
+                    {
+                        let location = std::panic::Location::caller();
+                        crate::debug_warn!(
+                            "\n\nWARNING: At {location}, you call .child() on \
+                             an HtmlElement<_> that was created with the \
+                             `view!` macro. The macro applies optimizations \
+                             during SSR that prevent calling this method \
+                             successfully. You should not mix the `view` \
+                             macro and the builder syntax when using SSR.\n\n"
+                        );
+                    }
                 }
             }
 
@@ -1239,8 +1330,8 @@ macro_rules! generate_html_tags {
 
 #[cfg(all(target_arch = "wasm32", feature = "web"))]
 fn create_leptos_element(
-    tag: &str,
-    id: Option<HydrationKey>,
+    #[allow(unused)] tag: &str,
+    #[allow(unused)] id: Option<HydrationKey>,
     clone_element: fn() -> web_sys::HtmlElement,
 ) -> web_sys::HtmlElement {
     #[cfg(not(debug_assertions))]
diff --git a/leptos_dom/src/lib.rs b/leptos_dom/src/lib.rs
index e17b1d2d..70237bc0 100644
--- a/leptos_dom/src/lib.rs
+++ b/leptos_dom/src/lib.rs
@@ -862,6 +862,7 @@ where
 
     cfg_if! {
       if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
+        crate::logging::console_log("mounting");
         mount_to(crate::document().body().expect("body element to exist"), f)
       } else {
         _ = f;
@@ -894,9 +895,9 @@ pub fn mount_to_with_stop_hydrating<F, N>(
             if stop_hydrating {
                 HydrationCtx::stop_hydrating();
             }
-            // TODO is this *ever* needed? unnecessarily remounts hydrated child
-            // parent.append_child(&node.get_mountable_node()).unwrap();
-            _ = parent;
+            if cfg!(feature = "csr") {
+                parent.append_child(&node.get_mountable_node()).unwrap();
+            }
             std::mem::forget(node);
       } else {
         _ = parent;
diff --git a/leptos_dom/src/math.rs b/leptos_dom/src/math.rs
index ec646e05..262ddee4 100644
--- a/leptos_dom/src/math.rs
+++ b/leptos_dom/src/math.rs
@@ -55,6 +55,7 @@ macro_rules! generate_math_tags {
 
         impl Default for [<$tag:camel $($second:camel $($third:camel)?)?>] {
           fn default() -> Self {
+            #[allow(unused)]
             let id = HydrationCtx::id();
 
             #[cfg(all(target_arch = "wasm32", feature = "hydrate"))]
diff --git a/leptos_dom/src/svg.rs b/leptos_dom/src/svg.rs
index 1daa10d4..e6098778 100644
--- a/leptos_dom/src/svg.rs
+++ b/leptos_dom/src/svg.rs
@@ -52,6 +52,7 @@ macro_rules! generate_svg_tags {
 
         impl Default for [<$tag:camel $($second:camel $($third:camel)?)?>] {
           fn default() -> Self {
+            #[allow(unused)]
             let id = HydrationCtx::id();
 
             #[cfg(all(target_arch = "wasm32", feature = "hydrate"))]
diff --git a/leptos_macro/src/component.rs b/leptos_macro/src/component.rs
index 9b3597c3..f92dd143 100644
--- a/leptos_macro/src/component.rs
+++ b/leptos_macro/src/component.rs
@@ -332,6 +332,39 @@ impl ToTokens for Model {
             }
         };
 
+        let count = props
+            .iter()
+            .filter(
+                |Prop {
+                     prop_opts: PropOpt { attrs, .. },
+                     ..
+                 }| *attrs,
+            )
+            .count();
+
+        let dyn_attrs_props = props
+            .iter()
+            .filter(
+                |Prop {
+                     prop_opts: PropOpt { attrs, .. },
+                     ..
+                 }| *attrs,
+            )
+            .enumerate()
+            .map(|(idx, Prop { name, .. })| {
+                let ident = &name.ident;
+                if idx < count - 1 {
+                    quote! {
+                        self.#ident = v.clone().into();
+                    }
+                } else {
+                    quote! {
+                        self.#ident = v.into();
+                    }
+                }
+            })
+            .collect::<TokenStream>();
+
         let body = quote! {
             #body
             #destructure_props
@@ -461,6 +494,13 @@ impl ToTokens for Model {
                 }
             }
 
+            impl #impl_generics ::leptos::DynAttrs for #props_name #generics #where_clause {
+                fn dyn_attrs(mut self, v: Vec<(&'static str, ::leptos::Attribute)>) -> Self {
+                    #dyn_attrs_props
+                    self
+                }
+            }
+
             #into_view
 
             #docs
@@ -699,6 +739,7 @@ struct PropOpt {
     #[attribute(example = "5 * 10")]
     default: Option<syn::Expr>,
     into: bool,
+    attrs: bool,
 }
 
 struct TypedBuilderOpts {
@@ -711,7 +752,7 @@ struct TypedBuilderOpts {
 impl TypedBuilderOpts {
     fn from_opts(opts: &PropOpt, is_ty_option: bool) -> Self {
         Self {
-            default: opts.optional || opts.optional_no_strip,
+            default: opts.optional || opts.optional_no_strip || opts.attrs,
             default_with_value: opts.default.clone(),
             strip_option: opts.strip_option || opts.optional && is_ty_option,
             into: opts.into,
diff --git a/leptos_macro/src/lib.rs b/leptos_macro/src/lib.rs
index e40d162f..59150cbc 100644
--- a/leptos_macro/src/lib.rs
+++ b/leptos_macro/src/lib.rs
@@ -404,8 +404,7 @@ pub fn template(tokens: TokenStream) -> TokenStream {
 ///
 /// The `#[component]` macro allows you to annotate plain Rust functions as components
 /// and use them within your Leptos [view](crate::view!) as if they were custom HTML elements. The
-/// component function takes a [Scope](https://docs.rs/leptos/latest/leptos/struct.Scope.html)
-/// and any number of other arguments. When you use the component somewhere else,
+/// component function takes any number of other arguments. When you use the component somewhere else,
 /// the names of its arguments are the names of the properties you use in the [view](crate::view!) macro.
 ///
 /// Every component function should have the return type `-> impl IntoView`.
@@ -535,7 +534,7 @@ pub fn template(tokens: TokenStream) -> TokenStream {
 /// ```
 ///
 /// 5. You can access the children passed into the component with the `children` property, which takes
-///    an argument of the type `Children`. This is an alias for `Box<dyn FnOnce(Scope) -> Fragment>`.
+///    an argument of the type `Children`. This is an alias for `Box<dyn FnOnce() -> Fragment>`.
 ///    If you need `children` to be a `Fn` or `FnMut`, you can use the `ChildrenFn` or `ChildrenFnMut`
 ///    type aliases.
 ///
@@ -807,10 +806,7 @@ pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
 /// ```
 ///
 /// The server function itself can take any number of arguments, each of which should be serializable
-/// and deserializable with `serde`. Optionally, its first argument can be a Leptos
-/// [Scope](https://docs.rs/leptos/latest/leptos/struct.Scope.html),
-/// which will be injected *on the server side.* This can be used to inject the raw HTTP request or other
-/// server-side context into the server function.
+/// and deserializable with `serde`.
 ///
 /// ```ignore
 /// # use leptos::*; use serde::{Serialize, Deserialize};
@@ -839,9 +835,8 @@ pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
 ///   form data using [`serde_qs`](https://docs.rs/serde_qs/latest/serde_qs/) or as `application/cbor`
 ///   using [`cbor`](https://docs.rs/cbor/latest/cbor/). **Note**: You should explicitly include `serde` with the
 ///   `derive` feature enabled in your `Cargo.toml`. You can do this by running `cargo add serde --features=derive`.
-/// - **The `Scope` comes from the server.** Optionally, the first argument of a server function
-///   can be a Leptos `Scope`. This scope can be used to inject dependencies like the HTTP request
-///   or response or other server-only dependencies, but it does *not* have access to reactive state that exists in the client.
+/// - **Context comes from the server.** Server functions are provided access to the HTTP request and other relevant
+///   server data via the server integrations, but they do *not* have access to reactive state that exists in the client.
 /// - Your server must be ready to handle the server functions at the API prefix you list. The easiest way to do this
 ///   is to use the `handle_server_fns` function from [`leptos_actix`](https://docs.rs/leptos_actix/latest/leptos_actix/fn.handle_server_fns.html)
 ///   or [`leptos_axum`](https://docs.rs/leptos_axum/latest/leptos_axum/fn.handle_server_fns.html).
diff --git a/leptos_macro/src/view/component_builder.rs b/leptos_macro/src/view/component_builder.rs
index 64f1a3b5..7d81ee88 100644
--- a/leptos_macro/src/view/component_builder.rs
+++ b/leptos_macro/src/view/component_builder.rs
@@ -33,6 +33,7 @@ pub(crate) fn component_to_tokens(
             !attr.key.to_string().starts_with("bind:")
                 && !attr.key.to_string().starts_with("clone:")
                 && !attr.key.to_string().starts_with("on:")
+                && !attr.key.to_string().starts_with("attr:")
         })
         .map(|attr| {
             let name = &attr.key;
@@ -70,6 +71,7 @@ pub(crate) fn component_to_tokens(
         .collect::<Vec<_>>();
 
     let events = attrs
+        .clone()
         .filter(|attr| attr.key.to_string().starts_with("on:"))
         .map(|attr| {
             let (event_type, handler) = event_from_attribute_node(attr, true);
@@ -80,6 +82,24 @@ pub(crate) fn component_to_tokens(
         })
         .collect::<Vec<_>>();
 
+    let dyn_attrs = attrs
+        .filter(|attr| attr.key.to_string().starts_with("attr:"))
+        .filter_map(|attr| {
+            let name = &attr.key.to_string();
+            let name = name.strip_prefix("attr:");
+            let value = attr.value().map(|v| {
+                quote! { #v }
+            })?;
+            Some(quote! { (#name, #value.into_attribute()) })
+        })
+        .collect::<Vec<_>>();
+
+    let dyn_attrs = if dyn_attrs.is_empty() {
+        quote! {}
+    } else {
+        quote! { .dyn_attrs(vec![#(#dyn_attrs),*]) }
+    };
+
     let mut slots = HashMap::new();
     let children = if node.children.is_empty() {
         quote! {}
@@ -163,6 +183,7 @@ pub(crate) fn component_to_tokens(
                 #(#slots)*
                 #children
                 .build()
+                #dyn_attrs
         )
     };
 
diff --git a/leptos_reactive/src/memo.rs b/leptos_reactive/src/memo.rs
index 024c7315..160e6aab 100644
--- a/leptos_reactive/src/memo.rs
+++ b/leptos_reactive/src/memo.rs
@@ -169,7 +169,7 @@ 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();
diff --git a/router/src/components/routes.rs b/router/src/components/routes.rs
index cbf1b504..aa995dbd 100644
--- a/router/src/components/routes.rs
+++ b/router/src/components/routes.rs
@@ -20,6 +20,49 @@ use std::{
 /// You should locate the `<Routes/>` component wherever on the page you want the routes to appear.
 ///
 /// **Note:** Your application should only include one `<Routes/>` or `<AnimatedRoutes/>` component.
+///
+/// You should not conditionally render `<Routes/>` using another component like `<Show/>` or `<Suspense/>`.
+///
+/// ```rust
+/// # use leptos::*;
+/// # use leptos_router::*;
+/// # if false {
+/// // ❌ don't do this!
+/// view! {
+///   <Show when=|| 1 == 2 fallback=|| view! { <p>"Loading"</p> }>
+///     <Routes>
+///       <Route path="/" view=|| "Home"/>
+///     </Routes>
+///   </Show>
+/// }
+/// # ;}
+/// ```
+///
+/// Instead, you can use nested routing to render your `<Routes/>` once, and conditionally render the router outlet:
+///
+/// ```rust
+/// # use leptos::*;
+/// # use leptos_router::*;
+/// # if false {
+/// // ✅ do this instead!
+/// view! {
+///   <Routes>
+///     // parent route
+///     <Route path="/" view=move || {
+///       view! {
+///         // only show the outlet if data have loaded
+///         <Show when=|| 1 == 2 fallback=|| view! { <p>"Loading"</p> }>
+///           <Outlet/>
+///         </Show>
+///       }
+///     }>
+///       // nested child route
+///       <Route path="/" view=|| "Home"/>
+///     </Route>
+///   </Routes>
+/// }
+/// # ;}
+/// ```
 #[cfg_attr(
     any(debug_assertions, feature = "ssr"),
     tracing::instrument(level = "info", skip_all,)
@@ -230,6 +273,18 @@ thread_local! {
 impl Branches {
     pub fn initialize(base: &str, children: Fragment) {
         BRANCHES.with(|branches| {
+            #[cfg(debug_assertions)]
+            {
+                if cfg!(any(feature = "csr", feature = "hydrate"))
+                    && !branches.borrow().is_empty()
+                {
+                    leptos::logging::warn!(
+                        "You should only render the <Routes/> component once \
+                         in your app. Please see the docs at https://docs.rs/leptos_router/latest/leptos_router/fn.Routes.html."
+                    );
+                }
+            }
+
             let mut current = branches.borrow_mut();
             if !current.contains_key(base) {
                 let mut branches = Vec::new();


Test patch:
diff --git a/leptos_macro/tests/ui/component.stderr b/leptos_macro/tests/ui/component.stderr
index e629ee97..37563cda 100644
--- a/leptos_macro/tests/ui/component.stderr
+++ b/leptos_macro/tests/ui/component.stderr
@@ -44,7 +44,7 @@ error: return type is incorrect
   |
   = help: return signature must be `-> impl IntoView`
 
-error: supported fields are `optional`, `optional_no_strip`, `strip_option`, `default` and `into`
+error: supported fields are `optional`, `optional_no_strip`, `strip_option`, `default`, `into` and `attrs`
   --> tests/ui/component.rs:10:31
    |
 10 | fn unknown_prop_option(#[prop(hello)] test: bool) -> impl IntoView {
diff --git a/leptos_macro/tests/ui/component_absolute.stderr b/leptos_macro/tests/ui/component_absolute.stderr
index 18c84daa..8975dec7 100644
--- a/leptos_macro/tests/ui/component_absolute.stderr
+++ b/leptos_macro/tests/ui/component_absolute.stderr
@@ -6,7 +6,7 @@ error: return type is incorrect
   |
   = help: return signature must be `-> impl IntoView`
 
-error: supported fields are `optional`, `optional_no_strip`, `strip_option`, `default` and `into`
+error: supported fields are `optional`, `optional_no_strip`, `strip_option`, `default`, `into` and `attrs`
  --> tests/ui/component_absolute.rs:5:31
   |
 5 | fn unknown_prop_option(#[prop(hello)] test: bool) -> impl ::leptos::IntoView {