From 96b169a2ff7c0b23f2f6623767c083f846555a6d Mon Sep 17 00:00:00 2001 From: Greg Johnston Date: Sat, 30 Nov 2024 12:25:14 -0500 Subject: [PATCH] porting book to 0.7 (#133) --- src/01_introduction.md | 2 +- src/15_global_state.md | 320 ++------------------- src/SUMMARY.md | 2 +- src/appendix_life_cycle.md | 38 ++- src/appendix_reactive_graph.md | 37 +-- src/async/10_resources.md | 125 ++++---- src/async/11_suspense.md | 90 ++++-- src/async/12_transition.md | 43 +-- src/async/13_actions.md | 47 +-- src/async/README.md | 6 +- src/deployment/ssr.md | 2 +- src/getting_started/README.md | 40 +-- src/getting_started/leptos_dx.md | 4 +- src/interlude_projecting_children.md | 93 +++--- src/interlude_styling.md | 32 +-- src/islands.md | 82 +++--- src/metadata.md | 27 +- src/progressive_enhancement/README.md | 12 +- src/progressive_enhancement/action_form.md | 54 ++-- src/reactivity/14_create_effect.md | 166 ++++------- src/reactivity/interlude_functions.md | 24 +- src/reactivity/working_with_signals.md | 129 ++++----- src/router/16_routes.md | 127 +++----- src/router/17_nested_routing.md | 152 +++++----- src/router/18_params_and_queries.md | 38 ++- src/router/19_a.md | 8 +- src/router/20_form.md | 57 ++-- src/server/25_server_functions.md | 79 +---- src/server/26_extractors.md | 16 +- src/server/27_response.md | 51 ++-- src/ssr/21_cargo_leptos.md | 3 - src/ssr/22_life_cycle.md | 2 +- src/ssr/23_ssr_modes.md | 60 ++-- src/ssr/24_hydration_bugs.md | 19 +- src/ssr/README.md | 11 +- src/testing.md | 93 +++--- src/view/01_basic_component.md | 140 +++++---- src/view/02_dynamic_attributes.md | 83 +++--- src/view/03_components.md | 118 +++++--- src/view/04_iteration.md | 57 ++-- src/view/04b_iteration.md | 69 ++--- src/view/05_forms.md | 64 ++--- src/view/06_control_flow.md | 91 +++--- src/view/07_errors.md | 59 ++-- src/view/08_parent_child.md | 116 ++------ src/view/09_component_children.md | 47 +-- src/view/builder.md | 54 ++-- src/web_sys.md | 188 ++++-------- 48 files changed, 1328 insertions(+), 1849 deletions(-) diff --git a/src/01_introduction.md b/src/01_introduction.md index 4030712..ed5e340 100644 --- a/src/01_introduction.md +++ b/src/01_introduction.md @@ -15,6 +15,6 @@ to other frameworks like React (JavaScript), Svelte (JavaScript), Yew (Rust), an Dioxus (Rust), so knowledge of one of those frameworks may also make it easier to understand Leptos. -You can find more detailed docs for each part of the API at [Docs.rs](https://docs.rs/leptos/latest/leptos/). +You can find more detailed docs for each part of the API at [Docs.rs](https://docs.rs/leptos/0.7.0-gamma3/leptos/). > The source code for the book is available [here](https://github.com/leptos-rs/book). PRs for typos or clarification are always welcome. diff --git a/src/15_global_state.md b/src/15_global_state.md index 0017e19..1f39e67 100644 --- a/src/15_global_state.md +++ b/src/15_global_state.md @@ -8,7 +8,7 @@ The three best approaches to global state are 1. Using the router to drive global state via the URL 2. Passing signals through context -3. Creating a global state struct and creating lenses into it with `create_slice` +3. Creating a global state struct using stores ## Option #1: URL as Global State @@ -32,7 +32,7 @@ all its children and descendants using `provide_context`. fn App() -> impl IntoView { // here we create a signal in the root that can be consumed // anywhere in the app. - let (count, set_count) = create_signal(0); + let (count, set_count) = signal(0); // we'll pass the setter to specific components, // but provide the count itself to the whole app via context provide_context(count); @@ -62,7 +62,7 @@ fn FancyMath() -> impl IntoView { let count = use_context::>() // we know we just provided this in the parent component .expect("there to be a `count` signal provided"); - let is_even = move || count() & 1 == 0; + let is_even = move || count.get() & 1 == 0; view! {
@@ -79,334 +79,58 @@ fn FancyMath() -> impl IntoView { } ``` -Note that this same pattern can be applied to more complex state. If you have multiple fields you want to update independently, you can do that by providing some struct of signals: +## Option #3: Create a Global State Store -```rust -#[derive(Copy, Clone, Debug)] -struct GlobalState { - count: RwSignal, - name: RwSignal -} - -impl GlobalState { - pub fn new() -> Self { - Self { - count: create_rw_signal(0), - name: create_rw_signal("Bob".to_string()) - } - } -} - -#[component] -fn App() -> impl IntoView { - provide_context(GlobalState::new()); +Stores are a new reactive primitive, available in Leptos 0.7 through the accompanying `reactive_stores` crate. (This crate is shipped separately for now so we can continue to develop it without requiring a version change to the whole framework.) - // etc. -} -``` +Stores allow you to wrap an entire struct, and reactively read from and update individual fields without tracking changes to other fields. -## Option #3: Create a Global State Struct and Slices - -You may find it cumbersome to wrap each field of a structure in a separate signal like this. In some cases, it can be useful to create a plain struct with non-reactive fields, and then wrap that in a signal. +They are used by adding `#[derive(Store)]` onto a struct. (You can `use reactive_stores::Store;` to import the macro.) This creates an extension trait with a getter for each field of the struct, when the struct is wrapped in a `Store<_>`. ```rust -#[derive(Copy, Clone, Debug, Default)] +#[derive(Clone, Debug, Default, Store)] struct GlobalState { count: i32, - name: String + name: String, } +``` + +This creates a trait named `GlobalStateStoreFields` which adds with methods `count` and `name` to a `Store`. Each method returns a reactive store *field*. +```rust #[component] fn App() -> impl IntoView { - provide_context(create_rw_signal(GlobalState::default())); + provide_context(Store::new(GlobalState::default())); // etc. } -``` -But there’s a problem: because our whole state is wrapped in one signal, updating the value of one field will cause reactive updates in parts of the UI that only depend on the other. - -```rust -let state = expect_context::>(); -view! { - -

{move || state.with(|state| state.name.clone())}

-} -``` - -In this example, clicking the button will cause the text inside `

` to be updated, cloning `state.name` again! Because signals are the atomic unit of reactivity, updating any field of the signal triggers updates to everything that depends on the signal. - -There’s a better way. You can take fine-grained, reactive slices by using [`create_memo`](https://docs.rs/leptos/latest/leptos/fn.create_memo.html) or [`create_slice`](https://docs.rs/leptos/latest/leptos/fn.create_slice.html) (which uses `create_memo` but also provides a setter). “Memoizing” a value means creating a new reactive value which will only update when it changes. “Memoizing a slice” means creating a new reactive value which will only update when some field of the state struct updates. - -Here, instead of reading from the state signal directly, we create “slices” of that state with fine-grained updates via `create_slice`. Each slice signal only updates when the particular piece of the larger struct it accesses updates. This means you can create a single root signal, and then take independent, fine-grained slices of it in different components, each of which can update without notifying the others of changes. - -```rust /// A component that updates the count in the global state. #[component] fn GlobalStateCounter() -> impl IntoView { - let state = expect_context::>(); + let state = expect_context::>(); - // `create_slice` lets us create a "lens" into the data - let (count, set_count) = create_slice( - - // we take a slice *from* `state` - state, - // our getter returns a "slice" of the data - |state| state.count, - // our setter describes how to mutate that slice, given a new value - |state, n| state.count = n, - ); + // this gives us reactive access to the `count` field only + let count = state.count(); view! {


- "Count is: " {count} + "Count is: " {move || count.get()}
} } ``` -Clicking this button only updates `state.count`, so if we create another slice -somewhere else that only takes `state.name`, clicking the button won’t cause -that other slice to update. This allows you to combine the benefits of a top-down +Clicking this button only updates `state.count`. If we read from `state.name` somewhere else, +click the button won’t notify it. This allows you to combine the benefits of a top-down data flow and of fine-grained reactive updates. -> **Note**: There are some significant drawbacks to this approach. Both signals and memos need to own their values, so a memo will need to clone the field’s value on every change. The most natural way to manage state in a framework like Leptos is always to provide signals that are as locally-scoped and fine-grained as they can be, not to hoist everything up into global state. But when you _do_ need some kind of global state, `create_slice` can be a useful tool. - -```admonish sandbox title="Live example" collapsible=true - -[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/15-global-state-0-5-8c2ff6?file=%2Fsrc%2Fmain.rs%3A1%2C2) - - - - - -``` - -
-CodeSandbox Source - -```rust -use leptos::*; - -// So far, we've only been working with local state in components -// We've only seen how to communicate between parent and child components -// But there are also more general ways to manage global state -// -// The three best approaches to global state are -// 1. Using the router to drive global state via the URL -// 2. Passing signals through context -// 3. Creating a global state struct and creating lenses into it with `create_slice` -// -// Option #1: URL as Global State -// The next few sections of the tutorial will be about the router. -// So for now, we'll just look at options #2 and #3. - -// Option #2: Pass Signals through Context -// -// In virtual DOM libraries like React, using the Context API to manage global -// state is a bad idea: because the entire app exists in a tree, changing -// some value provided high up in the tree can cause the whole app to render. -// -// In fine-grained reactive libraries like Leptos, this is simply not the case. -// You can create a signal in the root of your app and pass it down to other -// components using provide_context(). Changing it will only cause rerendering -// in the specific places it is actually used, not the whole app. -#[component] -fn Option2() -> impl IntoView { - // here we create a signal in the root that can be consumed - // anywhere in the app. - let (count, set_count) = create_signal(0); - // we'll pass the setter to specific components, - // but provide the count itself to the whole app via context - provide_context(count); - - view! { -

"Option 2: Passing Signals"

- // SetterButton is allowed to modify the count - - // These consumers can only read from it - // But we could give them write access by passing `set_count` if we wanted -
- - -
- } -} - -/// A button that increments our global counter. -#[component] -fn SetterButton(set_count: WriteSignal) -> impl IntoView { - view! { -
- -
- } -} - -/// A component that does some "fancy" math with the global count -#[component] -fn FancyMath() -> impl IntoView { - // here we consume the global count signal with `use_context` - let count = use_context::>() - // we know we just provided this in the parent component - .expect("there to be a `count` signal provided"); - let is_even = move || count() & 1 == 0; - - view! { -
- "The number " - {count} - {move || if is_even() { - " is" - } else { - " is not" - }} - " even." -
- } -} - -/// A component that shows a list of items generated from the global count. -#[component] -fn ListItems() -> impl IntoView { - // again, consume the global count signal with `use_context` - let count = use_context::>().expect("there to be a `count` signal provided"); - - let squares = move || { - (0..count()) - .map(|n| view! {
  • {n}"2" " is " {n * n}
  • }) - .collect::>() - }; - - view! { -
    -
      {squares}
    -
    - } -} - -// Option #3: Create a Global State Struct -// -// You can use this approach to build a single global data structure -// that holds the state for your whole app, and then access it by -// taking fine-grained slices using `create_slice` or `create_memo`, -// so that changing one part of the state doesn't cause parts of your -// app that depend on other parts of the state to change. - -#[derive(Default, Clone, Debug)] -struct GlobalState { - count: u32, - name: String, -} - -#[component] -fn Option3() -> impl IntoView { - // we'll provide a single signal that holds the whole state - // each component will be responsible for creating its own "lens" into it - let state = create_rw_signal(GlobalState::default()); - provide_context(state); - - view! { -

    "Option 3: Passing Signals"

    -
    -

    "Current Global State"

    -
    -                {move || {
    -                    format!("{:#?}", state.get())
    -                }}
    -            
    -
    -
    - - -
    - } -} - -/// A component that updates the count in the global state. -#[component] -fn GlobalStateCounter() -> impl IntoView { - let state = use_context::>().expect("state to have been provided"); - - // `create_slice` lets us create a "lens" into the data - let (count, set_count) = create_slice( - - // we take a slice *from* `state` - state, - // our getter returns a "slice" of the data - |state| state.count, - // our setter describes how to mutate that slice, given a new value - |state, n| state.count = n, - ); - - view! { -
    - -
    - "Count is: " {count} -
    - } -} - -/// A component that updates the count in the global state. -#[component] -fn GlobalStateInput() -> impl IntoView { - let state = use_context::>().expect("state to have been provided"); - - // this slice is completely independent of the `count` slice - // that we created in the other component - // neither of them will cause the other to rerun - let (name, set_name) = create_slice( - // we take a slice *from* `state` - state, - // our getter returns a "slice" of the data - |state| state.name.clone(), - // our setter describes how to mutate that slice, given a new value - |state, n| state.name = n, - ); - - view! { -
    - -
    - "Name is: " {name} -
    - } -} -// This `main` function is the entry point into the app -// It just mounts our component to the -// Because we defined it as `fn App`, we can now use it in a -// template as -fn main() { - leptos::mount_to_body(|| view! { }) -} -``` - -
    - +Check out the [`stores` example](https://github.com/leptos-rs/leptos/blob/main/examples/stores/src/lib.rs) in the repo for a more extensive example. diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 86996ff..71868bf 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -18,7 +18,7 @@ - [No Macros: The View Builder Syntax](./view/builder.md) - [Reactivity](./reactivity/README.md) - [Working with Signals](./reactivity/working_with_signals.md) - - [Responding to Changes with `create_effect`](./reactivity/14_create_effect.md) + - [Responding to Changes with Effects](./reactivity/14_create_effect.md) - [Interlude: Reactivity and Functions](./reactivity/interlude_functions.md) - [Testing](./testing.md) - [Async](./async/README.md) diff --git a/src/appendix_life_cycle.md b/src/appendix_life_cycle.md index 3ad33ce..ec6c40c 100644 --- a/src/appendix_life_cycle.md +++ b/src/appendix_life_cycle.md @@ -13,26 +13,26 @@ Consider the following simple Leptos app: ```rust use leptos::logging::log; -use leptos::*; +use leptos::prelude::*; #[component] pub fn App() -> impl IntoView { - let (count, set_count) = create_signal(0); + let (count, set_count) = signal(0); view! { - - {move || if count() % 2 == 0 { - view! {

    "Even numbers are fine."

    }.into_view() + + {move || if count.get() % 2 == 0 { + view! {

    "Even numbers are fine."

    }.into_any() } else { - view! { }.into_view() + view! { }.into_any() }} } } #[component] pub fn InnerComponent(count: ReadSignal) -> impl IntoView { - create_effect(move |_| { - log!("count is odd and is {}", count()); + Effect::new(move |_| { + log!("count is odd and is {}", count.get()); }); view! { @@ -98,20 +98,20 @@ This means that when your application is rendered, it creates a tree of nested e let button = /* render the

    - "stable"": " {move || stable.get()} + "stable"": " {move || stable.get().as_deref().copied()}

    "count"": " {count} @@ -132,13 +136,12 @@ fn App() -> impl IntoView { "async_value"": " {async_result}
    - {is_loading}

    } } fn main() { - leptos::mount_to_body(App) + leptos::mount::mount_to_body(App) } ``` diff --git a/src/async/11_suspense.md b/src/async/11_suspense.md index c991be2..6893c3d 100644 --- a/src/async/11_suspense.md +++ b/src/async/11_suspense.md @@ -3,8 +3,8 @@ In the previous chapter, we showed how you can create a simple loading screen to show some fallback while a resource is loading. ```rust -let (count, set_count) = create_signal(0); -let once = create_resource(count, |count| async move { load_a(count).await }); +let (count, set_count) = signal(0); +let once = Resource::new(move || count.get(), |count| async move { load_a(count).await }); view! {

    "My Data"

    @@ -18,10 +18,10 @@ view! { But what if we have two resources, and want to wait for both of them? ```rust -let (count, set_count) = create_signal(0); -let (count2, set_count2) = create_signal(0); -let a = create_resource(count, |count| async move { load_a(count).await }); -let b = create_resource(count2, |count| async move { load_b(count).await }); +let (count, set_count) = signal(0); +let (count2, set_count2) = signal(0); +let a = Resource::new(move || count.get(), |count| async move { load_a(count).await }); +let b = Resource::new(move || count2.get(), |count| async move { load_b(count).await }); view! {

    "My Data"

    @@ -37,13 +37,13 @@ view! { That’s not _so_ bad, but it’s kind of annoying. What if we could invert the flow of control? -The [``](https://docs.rs/leptos/latest/leptos/fn.Suspense.html) component lets us do exactly that. You give it a `fallback` prop and children, one or more of which usually involves reading from a resource. Reading from a resource “under” a `` (i.e., in one of its children) registers that resource with the ``. If it’s still waiting for resources to load, it shows the `fallback`. When they’ve all loaded, it shows the children. +The [``](https://docs.rs/leptos/0.7.0-gamma3/leptos/suspense/fn.Suspense.html) component lets us do exactly that. You give it a `fallback` prop and children, one or more of which usually involves reading from a resource. Reading from a resource “under” a `` (i.e., in one of its children) registers that resource with the ``. If it’s still waiting for resources to load, it shows the `fallback`. When they’ve all loaded, it shows the children. ```rust -let (count, set_count) = create_signal(0); -let (count2, set_count2) = create_signal(0); -let a = create_resource(count, |count| async move { load_a(count).await }); -let b = create_resource(count2, |count| async move { load_b(count).await }); +let (count, set_count) = signal(0); +let (count2, set_count2) = signal(0); +let a = Resource::new(count, |count| async move { load_a(count).await }); +let b = Resource::new(count2, |count| async move { load_b(count).await }); view! {

    "My Data"

    @@ -69,9 +69,34 @@ Every time one of the resources is reloading, the `"Loading..."` fallback will s This inversion of the flow of control makes it easier to add or remove individual resources, as you don’t need to handle the matching yourself. It also unlocks some massive performance improvements during server-side rendering, which we’ll talk about during a later chapter. +Using `` also gives us access to a useful way to directly `.await` resources, allowing us to remove a level of nesting, above. The `Suspend` type lets us create a renderable `Future` which can be used in the view: + +```rust +view! { +

    "My Data"

    + "Loading..."

    } + > +

    "My Data"

    + {move || Suspend::new(async move { + let a = a.await; + let b = b.await; + view! { +

    "A"

    + +

    "B"

    + + } + })} +
    +} +``` + +`Suspend` allows us to avoid null-checking each resource, and removes some additional complexity from the code. + ## `` -If you’re simply trying to wait for some `Future` to resolve before rendering, you may find the `` component helpful in reducing boilerplate. `` essentially combines a resource with the source argument `|| ()` with a `` with no fallback. +If you’re simply trying to wait for some `Future` to resolve before rendering, you may find the `` component helpful in reducing boilerplate. `` essentially combines a `OnceResource` with a `` with no fallback. In other words: @@ -87,7 +112,7 @@ async fn fetch_monkeys(monkey: i32) -> i32 { view! { @@ -99,14 +124,14 @@ view! { ```admonish sandbox title="Live example" collapsible=true -[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/11-suspense-0-5-qzpgqs?file=%2Fsrc%2Fmain.rs%3A1%2C1) +[Click to open CodeSandbox.](https://codesandbox.io/p/devbox/11-suspense-0-7-sr2srk?file=%2Fsrc%2Fmain.rs%3A1%2C1-55%2C1) ``` @@ -116,7 +141,7 @@ view! { ```rust use gloo_timers::future::TimeoutFuture; -use leptos::*; +use leptos::prelude::*; async fn important_api_call(name: String) -> String { TimeoutFuture::new(1_000).await; @@ -124,24 +149,33 @@ async fn important_api_call(name: String) -> String { } #[component] -fn App() -> impl IntoView { - let (name, set_name) = create_signal("Bill".to_string()); +pub fn App() -> impl IntoView { + let (name, set_name) = signal("Bill".to_string()); // this will reload every time `name` changes - let async_data = create_resource( - - name, - |name| async move { important_api_call(name).await }, - ); + let async_data = LocalResource::new(move || important_api_call(name.get())); view! {

    "name:" {name}

    + "Loading..."

    } + > + // Suspend allows you use to an async block in the view +

    + "Your shouting name is " + {move || Suspend::new(async move { + async_data.await + })} +

    +
    impl IntoView { // the children will be rendered once initially, // and then whenever any resources has been resolved

    - "Your shouting name is " - {move || async_data.get()} + "Which should be the same as... " + {move || async_data.get().as_deref().map(ToString::to_string)}

    } } fn main() { - leptos::mount_to_body(App) + leptos::mount::mount_to_body(App) } ``` diff --git a/src/async/12_transition.md b/src/async/12_transition.md index f62202e..0cc7a2f 100644 --- a/src/async/12_transition.md +++ b/src/async/12_transition.md @@ -1,6 +1,6 @@ # `` -You’ll notice in the `` example that if you keep reloading the data, it keeps flickering back to `"Loading..."`. Sometimes this is fine. For other times, there’s [``](https://docs.rs/leptos/latest/leptos/fn.Transition.html). +You’ll notice in the `` example that if you keep reloading the data, it keeps flickering back to `"Loading..."`. Sometimes this is fine. For other times, there’s [``](https://docs.rs/leptos/0.7.0-gamma3/leptos/suspense/fn.Transition.html). `` behaves exactly the same as ``, but instead of falling back every time, it only shows the fallback the first time. On all subsequent loads, it continues showing the old data until the new data are ready. This can be really handy to prevent the flickering effect, and to allow users to continue interacting with your application. @@ -8,14 +8,14 @@ This example shows how you can create a simple tabbed contact list with ` Please enable JavaScript to view examples. ``` @@ -25,7 +25,7 @@ This example shows how you can create a simple tabbed contact list with ` String { TimeoutFuture::new(1_000).await; @@ -40,52 +40,57 @@ async fn important_api_call(id: usize) -> String { #[component] fn App() -> impl IntoView { - let (tab, set_tab) = create_signal(0); + let (tab, set_tab) = signal(0); + let (pending, set_pending) = signal(false); // this will reload every time `tab` changes - let user_data = create_resource(tab, |tab| async move { important_api_call(tab).await }); + let user_data = LocalResource::new(move || important_api_call(tab.get())); view! {
    - {move || if user_data.loading().get() { - "Loading..." +
    +

    + {move || if pending.get() { + "Hang on..." } else { - "" + "Ready." }} -

    +

    "Loading..."

    } + fallback=move || view! {

    "Loading initial data..."

    } + // this will be set to `true` whenever the transition is ongoing + set_pending >

    - {move || user_data.read()} + {move || user_data.read().as_deref().map(ToString::to_string)}

    } } fn main() { - leptos::mount_to_body(App) + leptos::mount::mount_to_body(App) } ``` diff --git a/src/async/13_actions.md b/src/async/13_actions.md index ff55c6e..e62df7a 100644 --- a/src/async/13_actions.md +++ b/src/async/13_actions.md @@ -2,11 +2,11 @@ We’ve talked about how to load `async` data with resources. Resources immediately load data and work closely with `` and `` components to show whether data is loading in your app. But what if you just want to call some arbitrary `async` function and keep track of what it’s doing? -Well, you could always use [`spawn_local`](https://docs.rs/leptos/latest/leptos/fn.spawn_local.html). This allows you to just spawn an `async` task in a synchronous environment by handing the `Future` off to the browser (or, on the server, Tokio or whatever other runtime you’re using). But how do you know if it’s still pending? Well, you could just set a signal to show whether it’s loading, and another one to show the result... +Well, you could always use [`spawn_local`](https://docs.rs/leptos/0.7.0-gamma3/leptos/task/fn.spawn_local.html). This allows you to just spawn an `async` task in a synchronous environment by handing the `Future` off to the browser (or, on the server, Tokio or whatever other runtime you’re using). But how do you know if it’s still pending? Well, you could just set a signal to show whether it’s loading, and another one to show the result... -All of this is true. Or you could use the final `async` primitive: [`create_action`](https://docs.rs/leptos/latest/leptos/fn.create_action.html). +All of this is true. Or you could use the final `async` primitive: [`Action`](https://docs.rs/leptos/0.7.0-gamma3/leptos/reactive/actions/struct.Action.html). -Actions and resources seem similar, but they represent fundamentally different things. If you’re trying to load data by running an `async` function, either once or when some other value changes, you probably want to use `create_resource`. If you’re trying to occasionally run an `async` function in response to something like a user clicking a button, you probably want to use `create_action`. +Actions and resources seem similar, but they represent fundamentally different things. If you’re trying to load data by running an `async` function, either once or when some other value changes, you probably want to use a resource. If you’re trying to occasionally run an `async` function in response to something like a user clicking a button, you probably want to use an `Action`. Say we have some `async` function we want to run. @@ -16,22 +16,22 @@ async fn add_todo_request(new_title: &str) -> Uuid { } ``` -`create_action` takes an `async` function that takes a reference to a single argument, which you could think of as its “input type.” +`Action::new()` takes an `async` function that takes a reference to a single argument, which you could think of as its “input type.” > The input is always a single type. If you want to pass in multiple arguments, you can do it with a struct or tuple. > > ```rust > // if there's a single argument, just use that -> let action1 = create_action(|input: &String| { +> let action1 = Action::new(|input: &String| { > let input = input.clone(); > async move { todo!() } > }); > > // if there are no arguments, use the unit type `()` -> let action2 = create_action(|input: &()| async { todo!() }); +> let action2 = Action::new(|input: &()| async { todo!() }); > > // if there are multiple arguments, use a tuple -> let action3 = create_action( +> let action3 = Action::new( > |input: &(usize, String)| async { todo!() } > ); > ``` @@ -41,7 +41,7 @@ async fn add_todo_request(new_title: &str) -> Uuid { So in this case, all we need to do to create an action is ```rust -let add_todo_action = create_action(|input: &String| { +let add_todo_action = Action::new(|input: &String| { let input = input.to_owned(); async move { add_todo_request(&input).await } }); @@ -66,7 +66,7 @@ let todo_id = add_todo_action.value(); // RwSignal> This makes it easy to track the current state of your request, show a loading indicator, or do “optimistic UI” based on the assumption that the submission will succeed. ```rust -let input_ref = create_node_ref::(); +let input_ref = NodeRef::::new(); view! {
    "Add Todo"
    // use our loading state -

    {move || pending().then("Loading...")}

    +

    {move || pending.get().then_some("Loading...")}

    } ``` -Now, there’s a chance this all seems a little over-complicated, or maybe too restricted. I wanted to include actions here, alongside resources, as the missing piece of the puzzle. In a real Leptos app, you’ll actually most often use actions alongside server functions, [`create_server_action`](https://docs.rs/leptos/latest/leptos/fn.create_server_action.html), and the [``](https://docs.rs/leptos_router/latest/leptos_router/fn.ActionForm.html) component to create really powerful progressively-enhanced forms. So if this primitive seems useless to you... Don’t worry! Maybe it will make sense later. (Or check out our [`todo_app_sqlite`](https://github.com/leptos-rs/leptos/blob/main/examples/todo_app_sqlite/src/todo.rs) example now.) +Now, there’s a chance this all seems a little over-complicated, or maybe too restricted. I wanted to include actions here, alongside resources, as the missing piece of the puzzle. In a real Leptos app, you’ll actually most often use actions alongside server functions, [`ServerAction`](https://docs.rs/leptos/0.7.0-gamma3/leptos/server/struct.ServerAction.html), and the [``](https://docs.rs/leptos/0.7.0-gamma3/leptos/form/fn.ActionForm.html) component to create really powerful progressively-enhanced forms. So if this primitive seems useless to you... Don’t worry! Maybe it will make sense later. (Or check out our [`todo_app_sqlite`](https://github.com/leptos-rs/leptos/blob/main/examples/todo_app_sqlite/src/todo.rs) example now.) ```admonish sandbox title="Live example" collapsible=true -[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/13-actions-0-5-8xk35v?file=%2Fsrc%2Fmain.rs%3A1%2C1) +[Click to open CodeSandbox.](https://codesandbox.io/p/devbox/13-action-0-7-g73rl9?file=%2Fsrc%2Fmain.rs) ``` @@ -110,7 +110,7 @@ Now, there’s a chance this all seems a little over-complicated, or maybe too r ```rust use gloo_timers::future::TimeoutFuture; -use leptos::{html::Input, *}; +use leptos::{html::Input, prelude::*}; use uuid::Uuid; // Here we define an async function @@ -120,16 +120,17 @@ use uuid::Uuid; async fn add_todo(text: &str) -> Uuid { _ = text; // fake a one-second delay - TimeoutFuture::new(1_000).await; + // SendWrapper allows us to use this !Send browser API; don't worry about it + send_wrapper::SendWrapper::new(TimeoutFuture::new(1_000)).await; // pretend this is a post ID or something Uuid::new_v4() } #[component] -fn App() -> impl IntoView { +pub fn App() -> impl IntoView { // an action takes an async function with single argument // it can be a simple type, a struct, or () - let add_todo = create_action(|input: &String| { + let add_todo = Action::new(|input: &String| { // the input is a reference, but we need the Future to own it // this is important: we need to clone and move into the Future // so it has a 'static lifetime @@ -143,7 +144,7 @@ fn App() -> impl IntoView { let pending = add_todo.pending(); let todo_id = add_todo.value(); - let input_ref = create_node_ref::(); + let input_ref = NodeRef::::new(); view! {
    impl IntoView {
    -

    {move || pending().then(|| "Loading...")}

    +

    {move || pending.get().then_some("Loading...")}

    "Submitted: " - {move || format!("{:#?}", submitted())} + {move || format!("{:#?}", submitted.get())}

    "Pending: " - {move || format!("{:#?}", pending())} + {move || format!("{:#?}", pending.get())}

    "Todo ID: " - {move || format!("{:#?}", todo_id())} + {move || format!("{:#?}", todo_id.get())}

    } } fn main() { - leptos::mount_to_body(App) + leptos::mount::mount_to_body(App) } ``` diff --git a/src/async/README.md b/src/async/README.md index 1520d1a..96f15be 100644 --- a/src/async/README.md +++ b/src/async/README.md @@ -4,6 +4,8 @@ So far we’ve only been working with synchronous user interfaces: You provide s the app immediately processes it and updates the interface. This is great, but is a tiny subset of what web applications do. In particular, most web apps have to deal with some kind of asynchronous data loading, usually loading something from an API. -Asynchronous data is notoriously hard to integrate with the synchronous parts of your code. Leptos provides a cross-platform [`spawn_local`](https://docs.rs/leptos/latest/leptos/fn.spawn_local.html) function that makes it easy to run a `Future`, but there’s much more to it than that. +Asynchronous data is notoriously hard to integrate with the synchronous parts of your code because of problems of “function coloring.” -In this chapter, we’ll see how Leptos helps smooth out that process for you. +In the following chapters, we’ll see a few reactive primitives for working with async data. But it’s important to note at the very beginning: If you just want to do some asynchronous work, Leptos provides a cross-platform [`spawn_local`](https://docs.rs/leptos/0.7.0-gamma3/leptos/task/fn.spawn_local.html) function that makes it easy to run a `Future`. If one of the primitives that’s discussd in the rest of this section doesn’t seem to do what you want, consider combining `spawn_local` with setting a signal. + +While the primitives to come are very useful, and even necessary in some cases, people sometimes run into situations in which they really just need to spawn a task and wait for it to finish before doing something else. Use `spawn_local` in those situations! diff --git a/src/deployment/ssr.md b/src/deployment/ssr.md index 56caa73..c12a5b0 100644 --- a/src/deployment/ssr.md +++ b/src/deployment/ssr.md @@ -222,7 +222,7 @@ The other factor to bear in mind is the 'cold-start' time for functions as a ser ### Deno & Cloudflare Workers -Currently, Leptos-Axum supports running in Javascript-hosted WebAssembly runtimes such as Deno, Cloudflare Workers, etc. This option requires some changes to the setup of your source code (for example, in `Cargo.toml` you must define your app using `crate-type = ["cdylib"]` and the "wasm" feature must be enabled for `leptos_axum`). [The Leptos HackerNews JS-fetch example](https://github.com/leptos-rs/leptos/tree/leptos_0.6/examples/hackernews_js_fetch) demonstrates the required modifications and shows how to run an app in the Deno runtime. Additionally, the [`leptos_axum` crate docs](https://docs.rs/leptos_axum/latest/leptos_axum/#js-fetch-integration) are a helpful reference when setting up your own `Cargo.toml` file for JS-hosted WASM runtimes. +Currently, Leptos-Axum supports running in Javascript-hosted WebAssembly runtimes such as Deno, Cloudflare Workers, etc. This option requires some changes to the setup of your source code (for example, in `Cargo.toml` you must define your app using `crate-type = ["cdylib"]` and the "wasm" feature must be enabled for `leptos_axum`). [The Leptos HackerNews JS-fetch example](https://github.com/leptos-rs/leptos/tree/leptos_0.6/examples/hackernews_js_fetch) demonstrates the required modifications and shows how to run an app in the Deno runtime. Additionally, the [`leptos_axum` crate docs](https://docs.rs/leptos_axum/0.7.0-gamma3/leptos_axum/#js-fetch-integration) are a helpful reference when setting up your own `Cargo.toml` file for JS-hosted WASM runtimes. While the initial setup for JS-hosted WASM runtimes is not onerous, the more important restriction to keep in mind is that since your app will be compiled to WebAssembly (`wasm32-unknown-unknown`) on the server as well as the client, you must ensure that the crates you use in your app are all WASM-compatible; this may or may not be a deal-breaker depending on your app's requirements, as not all crates in the Rust ecosystem have WASM support. diff --git a/src/getting_started/README.md b/src/getting_started/README.md index 162390c..93ed8de 100644 --- a/src/getting_started/README.md +++ b/src/getting_started/README.md @@ -3,15 +3,14 @@ There are two basic paths to getting started with Leptos: 1. **Client-side rendering (CSR) with [Trunk](https://trunkrs.dev/)** - a great option if you just want to make a snappy website with Leptos, or work with a pre-existing server or API. -In CSR mode, Trunk compiles your Leptos app to WebAssembly (WASM) and runs it in the browser like a typical Javascript single-page app (SPA). The advantages of Leptos CSR include faster build times and a quicker iterative development cycle, as well as a simpler mental model and more options for deploying your app. CSR apps do come with some disadvantages: initial load times for your end users are slower compared to a server-side rendering approach, and the usual SEO challenges that come along with using a JS single-page app model apply to Leptos CSR apps as well. Also note that, under the hood, an auto-generated snippet of JS is used to load the Leptos WASM bundle, so JS *must* be enabled on the client device for your CSR app to display properly. As with all software engineering, there are trade-offs here you'll need to consider. + In CSR mode, Trunk compiles your Leptos app to WebAssembly (WASM) and runs it in the browser like a typical Javascript single-page app (SPA). The advantages of Leptos CSR include faster build times and a quicker iterative development cycle, as well as a simpler mental model and more options for deploying your app. CSR apps do come with some disadvantages: initial load times for your end users are slower compared to a server-side rendering approach, and the usual SEO challenges that come along with using a JS single-page app model apply to Leptos CSR apps as well. Also note that, under the hood, an auto-generated snippet of JS is used to load the Leptos WASM bundle, so JS _must_ be enabled on the client device for your CSR app to display properly. As with all software engineering, there are trade-offs here you'll need to consider. 2. **Full-stack, server-side rendering (SSR) with [`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos)** - SSR is a great option for building CRUD-style websites and custom web apps if you want Rust powering both your frontend and backend. -With the Leptos SSR option, your app is rendered to HTML on the server and sent down to the browser; then, WebAssembly is used to instrument the HTML so your app becomes interactive - this process is called 'hydration'. On the server side, Leptos SSR apps integrate closely with your choice of either [Actix-web](https://docs.rs/leptos_actix/latest/leptos_actix/index.html) or [Axum](https://docs.rs/leptos_axum/latest/leptos_axum/index.html) server libraries, so you can leverage those communities' crates to help build out your Leptos server. -The advantages of taking the SSR route with Leptos include helping you get the best initial load times and optimal SEO scores for your web app. SSR apps can also dramatically simplify working across the server/client boundary via a Leptos feature called "server functions", which lets you transparently call functions on the server from your client code (more on this feature later). Full-stack SSR isn't all rainbows and butterflies, though - disadvantages include a slower developer iteration loop (because you need to recompile both the server and client when making Rust code changes), as well as some added complexity that comes along with hydration. + With the Leptos SSR option, your app is rendered to HTML on the server and sent down to the browser; then, WebAssembly is used to instrument the HTML so your app becomes interactive - this process is called 'hydration'. On the server side, Leptos SSR apps integrate closely with your choice of either [Actix-web](https://docs.rs/leptos_actix/0.7.0-gamma3/leptos_actix/) or [Axum](https://docs.rs/leptos_axum/0.7.0-gamma3/leptos_axum/) server libraries, so you can leverage those communities' crates to help build out your Leptos server. + The advantages of taking the SSR route with Leptos include helping you get the best initial load times and optimal SEO scores for your web app. SSR apps can also dramatically simplify working across the server/client boundary via a Leptos feature called "server functions", which lets you transparently call functions on the server from your client code (more on this feature later). Full-stack SSR isn't all rainbows and butterflies, though - disadvantages include a slower developer iteration loop (because you need to recompile both the server and client when making Rust code changes), as well as some added complexity that comes along with hydration. By the end of the book, you should have a good idea of which trade-offs to make and which route to take - CSR or SSR - depending on your project's requirements. - In Part 1 of this book, we'll start with client-side rendering Leptos sites and building reactive UIs using `Trunk` to serve our JS and WASM bundle to the browser. We’ll introduce `cargo-leptos` in Part 2 of this book, which is all about working with the full power of Leptos in its full-stack, SSR mode. @@ -45,37 +44,10 @@ cargo init leptos-tutorial `cd` into your new `leptos-tutorial` project and add `leptos` as a dependency -```bash -cargo add leptos --features=csr,nightly -``` - -Or you can leave off `nightly` if you're using stable Rust - ```bash cargo add leptos --features=csr ``` -> Using `nightly` Rust, and the `nightly` feature in Leptos enables the function-call syntax for signal getters and setters that is used in most of this book. -> -> To use nightly Rust, you can either opt into nightly for all your Rust projects by running -> -> ```bash -> rustup toolchain install nightly -> rustup default nightly -> ``` -> -> or only for this project -> -> ```bash -> rustup toolchain install nightly -> cd -> rustup override set nightly -> ``` -> -> [See here for more details.](https://doc.rust-lang.org/book/appendix-07-nightly-rust.html) -> -> If you’d rather use stable Rust with Leptos, you can do that too. In the guide and examples, you’ll just use the [`ReadSignal::get()`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html#impl-SignalGet%3CT%3E-for-ReadSignal%3CT%3E) and [`WriteSignal::set()`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html#impl-SignalGet%3CT%3E-for-ReadSignal%3CT%3E) methods instead of calling signal getters and setters as functions. - Make sure you've added the `wasm32-unknown-unknown` target so that Rust can compile your code to WebAssembly to run in the browser. ```bash @@ -95,10 +67,10 @@ Create a simple `index.html` in the root of the `leptos-tutorial` directory And add a simple “Hello, world!” to your `main.rs` ```rust -use leptos::*; +use leptos::prelude::*; fn main() { - mount_to_body(|| view! {

    "Hello, world!"

    }) + leptos::mount::mount_to_body(|| view! {

    "Hello, world!"

    }) } ``` @@ -126,4 +98,4 @@ simply use `trunk serve` and open a browser tab manually. --- -Now before we get started building your first real UI's with Leptos, there are a couple of things you might want to know to help make your experience with Leptos just a little bit easier. +Now before we get started building your first real applications with Leptos, there are a couple of things you might want to know to help make your experience with Leptos just a little bit easier. diff --git a/src/getting_started/leptos_dx.md b/src/getting_started/leptos_dx.md index 1a224a9..e8d71dc 100644 --- a/src/getting_started/leptos_dx.md +++ b/src/getting_started/leptos_dx.md @@ -23,8 +23,10 @@ Because of the nature of macros (they can expand from anything to anything, but If you run into issues using these macros in your editor, you can explicitly tell rust-analyzer to ignore certain proc macros. For the `#[server]` macro especially, which annotates function bodies but doesn't actually transform anything inside the body of your function, this can be really helpful. -Starting in Leptos version 0.5.3, rust-analyzer support was added for the `#[component]` macro, but if you run into issues, you may want to add `#[component]` to the macro ignore list as well (see below). +```admonish note + Starting in Leptos version 0.5.3, rust-analyzer support was added for the `#[component]` macro, but if you run into issues, you may want to add `#[component]` to the macro ignore list as well (see below). 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`: diff --git a/src/interlude_projecting_children.md b/src/interlude_projecting_children.md index bd46ec7..205f229 100644 --- a/src/interlude_projecting_children.md +++ b/src/interlude_projecting_children.md @@ -7,40 +7,38 @@ As you build components you may occasionally find yourself wanting to “project Consider the following: ```rust -pub fn LoggedIn(fallback: F, children: ChildrenFn) -> impl IntoView +pub fn NestedShow(fallback: F, children: ChildrenFn) -> impl IntoView where - F: Fn() -> IV + 'static, - IV: IntoView, + F: Fn() -> IV + Send + Sync + 'static, + IV: IntoView + 'static, { view! { - - {children()} - - + {children()} + + } } ``` -This is pretty straightforward: when the user is logged in, we want to show `children`. If the user is not logged in, we want to show `fallback`. And while we’re waiting to find out, we just render `()`, i.e., nothing. +This is pretty straightforward: if the inner condition is `true`, we want to show `children`. If not, we want to show `fallback`. And if the outer condition is `false`, we just render `()`, i.e., nothing. -In other words, we want to pass the children of `` _through_ the `` component to become the children of the ``. This is what I mean by “projection.” +In other words, we want to pass the children of `` _through_ the outer `` component to become the children of the inner ``. This is what I mean by “projection.” This won’t compile. ``` -error[E0507]: cannot move out of `fallback`, a captured variable in an `Fn` closure -error[E0507]: cannot move out of `children`, a captured variable in an `Fn` closure +error[E0525]: expected a closure that implements the `Fn` trait, but this closure only implements `FnOnce` ``` -The problem here is that both `` and `` need to be able to construct their `children` multiple times. The first time you construct ``’s children, it would take ownership of `fallback` and `children` to move them into the invocation of ``, but then they're not available for future `` children construction. +Each `` needs to be able to construct its `children` multiple times. The first time you construct the outer ``’s children, it takes `fallback` and `children` to move them into the invocation of the inner ``, but then they're not available for future outer-`` children construction. ## The Details @@ -49,29 +47,26 @@ The problem here is that both `` and `` need to be able to con If you want to really understand the issue here, it may help to look at the expanded `view` macro. Here’s a cleaned-up version: ```rust -Suspense( - ::leptos::component_props_builder(&Suspense) +Show( + ShowProps::builder() + .when(|| todo!()) .fallback(|| ()) .children({ - // fallback and children are moved into this closure - Box::new(move || { - { - // fallback and children captured here - leptos::Fragment::lazy(|| { - vec![ - (Show( - ::leptos::component_props_builder(&Show) - .when(|| true) - // but fallback is moved into Show here - .fallback(fallback) - // and children is moved into Show here - .children(children) - .build(), + // children and fallback are moved into a closure here + ::leptos::children::ToChildren::to_children(move || { + Show( + ShowProps::builder() + .when(|| todo!()) + // fallback is consumed here + .fallback(fallback) + .children({ + // children is captured here + ::leptos::children::ToChildren::to_children( + move || children(), ) - .into_view()), - ] - }) - } + }) + .build(), + ) }) }) .build(), @@ -84,29 +79,33 @@ All components own their props; so the `` in this case can’t be called However, both `` and `` take `ChildrenFn`, i.e., their `children` should implement the `Fn` type so they can be called multiple times with only an immutable reference. This means we don’t need to own `children` or `fallback`; we just need to be able to pass `'static` references to them. -We can solve this problem by using the [`store_value`](https://docs.rs/leptos/latest/leptos/fn.store_value.html) primitive. This essentially stores a value in the reactive system, handing ownership off to the framework in exchange for a reference that is, like signals, `Copy` and `'static`, which we can access or modify through certain methods. +We can solve this problem by using the [`StoredValue`](https://docs.rs/leptos/0.7.0-gamma3/leptos/reactive/owner/struct.StoredValue.html) primitive. This essentially stores a value in the reactive system, handing ownership off to the framework in exchange for a reference that is, like signals, `Copy` and `'static`, which we can access or modify through certain methods. In this case, it’s really simple: ```rust -pub fn LoggedIn(fallback: F, children: ChildrenFn) -> impl IntoView +pub fn NestedShow(fallback: F, children: ChildrenFn) -> impl IntoView where - F: Fn() -> IV + 'static, - IV: IntoView, + F: Fn() -> IV + Send + Sync + 'static, + IV: IntoView + 'static, { - let fallback = store_value(fallback); - let children = store_value(children); + let fallback = StoredValue::new(fallback); + let children = StoredValue::new(children); + view! { - - {children.with_value(|children| children())} + {children.read_value()()} - + } } ``` @@ -115,7 +114,7 @@ At the top level, we store both `fallback` and `children` in the reactive scope ## A Final Note -Note that this works because `` and `` only need an immutable reference to their children (which `.with_value` can give it), not ownership. +Note that this works because `` only needs an immutable reference to their children (which `.read_value` can give), not ownership. In other cases, you may need to project owned props through a function that takes `ChildrenFn` and therefore needs to be called more than once. In this case, you may find the `clone:` helper in the`view` macro helpful. diff --git a/src/interlude_styling.md b/src/interlude_styling.md index 9d4f9bf..ce01574 100644 --- a/src/interlude_styling.md +++ b/src/interlude_styling.md @@ -15,7 +15,7 @@ This allows you to write components like this: ```rust #[component] fn Home() -> impl IntoView { - let (count, set_count) = create_signal(0); + let (count, set_count) = signal(0); view! {
    @@ -23,12 +23,12 @@ fn Home() -> impl IntoView {

    "Tailwind will scan your Rust files for Tailwind class names and compile them into a CSS file."

    @@ -36,7 +36,7 @@ fn Home() -> impl IntoView { } ``` -It can be a little complicated to set up the Tailwind integration at first, but you can check out our two examples of how to use Tailwind with a [client-side-rendered `trunk` application](https://github.com/leptos-rs/leptos/tree/leptos_0.6/examples/tailwind_csr) or with a [server-rendered `cargo-leptos` application](https://github.com/leptos-rs/leptos/tree/leptos_0.6/examples/tailwind_actix). `cargo-leptos` also has some [built-in Tailwind support](https://github.com/leptos-rs/cargo-leptos#site-parameters) that you can use as an alternative to Tailwind’s CLI. +It can be a little complicated to set up the Tailwind integration at first, but you can check out our two examples of how to use Tailwind with a [client-side-rendered `trunk` application](https://github.com/leptos-rs/leptos/tree/main/examples/tailwind_csr) or with a [server-rendered `cargo-leptos` application](https://github.com/leptos-rs/leptos/tree/main/examples/tailwind_actix). `cargo-leptos` also has some [built-in Tailwind support](https://github.com/leptos-rs/cargo-leptos#site-parameters) that you can use as an alternative to Tailwind’s CLI. ## Stylers: Compile-time CSS Extraction @@ -110,28 +110,6 @@ You can edit the CSS directly without causing a Rust recompile. } ``` -## Styled: Runtime CSS Scoping - -[Styled](https://github.com/eboody/styled) is a runtime scoped CSS library that integrates well with Leptos. It lets you declare scoped CSS in the body of your component function, and then applies those styles at runtime. - -```rust -use styled::style; - -#[component] -pub fn MyComponent() -> impl IntoView { - let styles = style!( - div { - background-color: red; - color: white; - } - ); - - styled::view! { styles, -
    "This text should be red with white text."
    - } -} -``` - ## Contributions Welcome Leptos has no opinions on how you style your website or app, but we’re very happy to provide support to any tools you’re trying to create to make it easier. If you’re working on a CSS or styling approach that you’d like to add to this list, please let us know! diff --git a/src/islands.md b/src/islands.md index 8467db0..aff5ab1 100644 --- a/src/islands.md +++ b/src/islands.md @@ -1,6 +1,6 @@ # Guide: Islands -Leptos 0.5 introduces the new `experimental-islands` feature. This guide will walk through the islands feature and core concepts, while implementing a demo app using the islands architecture. +Leptos 0.5 introduced the new `islands` feature. This guide will walk through the islands feature and core concepts, while implementing a demo app using the islands architecture. ## The Islands Architecture @@ -28,10 +28,10 @@ The phrase “islands architecture” has emerged recently to describe the appro Let’s start with a fresh `cargo-leptos` app: ```bash -cargo leptos new --git leptos-rs/start +cargo leptos new --git leptos-rs/start-axum ``` -> I’m using Actix because I like it. Feel free to use Axum; there should be approximately no server-specific differences in this guide. +> There should be no real differences between Actix and Axum in this example. I’m just going to run @@ -41,22 +41,24 @@ cargo leptos build in the background while I fire up my editor and keep writing. -The first thing I’ll do is to add the `experimental-islands` feature in my `Cargo.toml`. I need to add this to both `leptos` and `leptos_actix`: +The first thing I’ll do is to add the `islands` feature in my `Cargo.toml`. I only need to add this to the `leptos` crate. ```toml -leptos = { version = "0.5", features = ["nightly", "experimental-islands"] } -leptos_actix = { version = "0.5", optional = true, features = [ - "experimental-islands", -] } +leptos = { version = "0.7", features = ["islands"] } ``` -Next I’m going to modify the `hydrate` function exported from `src/lib.rs`. I’m going to remove the line that calls `leptos::mount_to_body(App)` and replace it with +Next I’m going to modify the `hydrate` function exported from `src/lib.rs`. I’m going to remove the line that calls `leptos::mount::mount_to_body(App)` and replace it with ```rust -leptos::leptos_dom::HydrationCtx::stop_hydrating(); +leptos::mount::hydrate_islands(); ``` -Each “island” we create will actually act as its own entrypoint, so our `hydrate()` function just says “okay, hydration’s done now.” +Rather than running the whole application and hydrating the view that it creates, this will hydrate each individual island, in order. + +In `app.rs`, in the `shell` functions, we’ll also need to add `islands=true` to the `HydrationScripts` component: +```rust + +``` Okay, now fire up your `cargo leptos watch` and go to [`http://localhost:3000`](http://localhost:3000) (or wherever). @@ -76,7 +78,7 @@ However, this can cause issues if you are using a workspace setup. We use `wasm- Nothing happens because we’ve just totally inverted the mental model of our app. Rather than being interactive by default and hydrating everything, the app is now plain HTML by default, and we need to opt into interactivity. -This has a big effect on WASM binary sizes: if I compile in release mode, this app is a measly 24kb of WASM (uncompressed), compared to 355kb in non-islands mode. (355kb is quite large for a “Hello, world!” It’s really just all the code related to client-side routing, which isn’t being used in the demo.) +This has a big effect on WASM binary sizes: if I compile in release mode, this app is a measly 24kb of WASM (uncompressed), compared to 274kb in non-islands mode. (274kb is quite large for a “Hello, world!” It’s really just all the code related to client-side routing, which isn’t being used in the demo.) When we click the button, nothing happens, because our whole page is static. @@ -90,8 +92,8 @@ Here was the non-interactive version: #[component] fn HomePage() -> impl IntoView { // Creates a reactive value to update the button - let (count, set_count) = create_signal(0); - let on_click = move |_| set_count.update(|count| *count += 1); + let count = RwSignal::new(0); + let on_click = move |_| *count.write() += 1; view! {

    "Welcome to Leptos!"

    @@ -106,8 +108,8 @@ Here’s the interactive version: #[island] fn HomePage() -> impl IntoView { // Creates a reactive value to update the button - let (count, set_count) = create_signal(0); - let on_click = move |_| set_count.update(|count| *count += 1); + let count = RwSignal::new(0); + let on_click = move |_| *count.write() += 1; view! {

    "Welcome to Leptos!"

    @@ -123,16 +125,13 @@ The `#[island]` macro works exactly like the `#[component]` macro, except that i If you open up the source for the page now, you’ll see that your `HomePage` island has been rendered as a special `` HTML element which specifies which component should be used to hydrate it: ```html - -

    Welcome to Leptos!

    - + +

    Welcome to Leptos!

    +
    ``` -The typical Leptos hydration keys and markers are only present inside the island, only the island is hydrated. +Only code for what's inside this `` is compiled to WASM, only only that code runs when hydrating. ## Using Islands Effectively @@ -150,8 +149,8 @@ fn HomePage() -> impl IntoView { #[island] fn Counter() -> impl IntoView { // Creates a reactive value to update the button - let (count, set_count) = create_signal(0); - let on_click = move |_| set_count.update(|count| *count += 1); + let (count, set_count) = signal(0); + let on_click = move |_| *set_count.write() += 1; view! { @@ -169,11 +168,13 @@ So, this 50% reduction in WASM binary size is nice. But really, what’s the poi The point comes when you combine two key facts: -1. Code inside `#[component]` functions now _only_ runs on the server. +1. Code inside `#[component]` functions now _only_ runs on the server, unless you use it in an island.\* 2. Children and props can be passed from the server to islands, without being included in the WASM binary. This means you can run server-only code directly in the body of a component, and pass it directly into the children. Certain tasks that take a complex blend of server functions and Suspense in fully-hydrated apps can be done inline in islands. +> \* This “unless you use it in an island” is important. It is *not* the case that `#[component]` components only run on the server. Rather, they are “shared components” that are only compiled into the WASM binary if they’re used in the body of an `#[island]`. But if you don’t use them in an island, they won’t run in the browser. + We’re going to rely on a third fact in the rest of this demo: 3. Context can be passed between otherwise-independent islands. @@ -237,11 +238,17 @@ fn HomePage() -> impl IntoView { If you take a look in the DOM inspector, you’ll see the island is now something like ```html - + +
    + + + + +
    +
    ``` Our `labels` prop is getting serialized to JSON and stored in an HTML attribute so it can be used to hydrate the island. @@ -335,14 +342,14 @@ We’ll modify `Tabs` to create a simple `selected` signal. We provide the read ```rust #[island] fn Tabs(labels: Vec, children: Children) -> impl IntoView { - let (selected, set_selected) = create_signal(0); + let (selected, set_selected) = signal(0); provide_context(selected); let buttons = labels .into_iter() .enumerate() .map(|(index, label)| view! { - }) @@ -360,7 +367,7 @@ fn Tab(index: usize, children: Children) -> impl IntoView {
    That’s why in `HomePage`, I made `let tabs = move ||` a function, and called it like `{tabs()}`: creating the tabs lazily this way meant that the `Tabs` island would already have provided the `selected` context by the time each `Tab` went looking for it. -Our complete tabs demo is about 220kb uncompressed: not the smallest demo in the world, but still about a third smaller than the counter button! Just for kicks, I built the same demo without islands mode, using `#[server]` functions and `Suspense`. and it was 429kb. So again, this was about a 50% savings in binary size. And this app includes quite minimal server-only content: remember that as we add additional server-only components and pages, this 220 will not grow. +Our complete tabs demo is about 200kb uncompressed: not the smallest demo in the world, but still significantly smaller than the “Hello, world” using client side routing that we started with! Just for kicks, I built the same demo without islands mode, using `#[server]` functions and `Suspense`. and it was over 400kb. So again, this was about a 50% savings in binary size. And this app includes quite minimal server-only content: remember that as we add additional server-only components and pages, this 200kb will not grow. ## Overview This demo may seem pretty basic. It is. But there are a number of immediate takeaways: - **50% WASM binary size reduction**, which means measurable improvements in time to interactivity and initial load times for clients. -- **Reduced HTML page size.** This one is less obvious, but it’s true and important: HTML generated from `#[component]`s doesn’t need all the hydration IDs and other boilerplate added. - **Reduced data serialization costs.** Creating a resource and reading it on the client means you need to serialize the data, so it can be used for hydration. If you’ve also read that data to create HTML in a `Suspense`, you end up with “double data,” i.e., the same exact data is both rendered to HTML and serialized as JSON, increasing the size of responses, and therefore slowing them down. - **Easily use server-only APIs** inside a `#[component]` as if it were a normal, native Rust function running on the server—which, in islands mode, it is! - **Reduced `#[server]`/`create_resource`/`Suspense` boilerplate** for loading server data. ## Future Exploration -The `experimental-islands` feature included in 0.5 reflects work at the cutting edge of what frontend web frameworks are exploring right now. As it stands, our islands approach is very similar to Astro (before its recent View Transitions support): it allows you to build a traditional server-rendered, multi-page app and pretty seamlessly integrate islands of interactivity. +The `islands` feature reflects work at the cutting edge of what frontend web frameworks are exploring right now. As it stands, our islands approach is very similar to Astro (before its recent View Transitions support): it allows you to build a traditional server-rendered, multi-page app and pretty seamlessly integrate islands of interactivity. There are some small improvements that will be easy to add. For example, we can do something very much like Astro's View Transitions approach: @@ -402,7 +408,7 @@ There are other, larger architectural changes that I’m [not sold on yet](https ## Additional Information -Check out the [islands PR](https://github.com/leptos-rs/leptos/pull/1660), [roadmap](https://github.com/leptos-rs/leptos/issues/1830), and [Hackernews demo](https://github.com/leptos-rs/leptos/tree/leptos_0.6/examples/hackernews_islands_axum) for additional discussion. +Check out the [`islands` example](https://github.com/leptos-rs/leptos/blob/main/examples/islands/src/app.rs), [roadmap](https://github.com/leptos-rs/leptos/issues/1830), and [Hackernews demo](https://github.com/leptos-rs/leptos/tree/leptos_0.6/examples/hackernews_islands_axum) for additional discussion. ## Demo Code diff --git a/src/metadata.md b/src/metadata.md index 45fdd00..7be78a5 100644 --- a/src/metadata.md +++ b/src/metadata.md @@ -4,39 +4,38 @@ So far, everything we’ve rendered has been inside the `` of the HTML doc However, there are plenty of occasions where you might want to update something inside the `` of the document using the same reactive primitives and component patterns you use for your UI. -That’s where the [`leptos_meta`](https://docs.rs/leptos_meta/latest/leptos_meta/) package comes in. +That’s where the [`leptos_meta`](https://docs.rs/leptos_meta/0.7.0-gamma3/leptos_meta/) package comes in. ## Metadata Components `leptos_meta` provides special components that let you inject data from inside components anywhere in your application into the ``: -[``](https://docs.rs/leptos_meta/latest/leptos_meta/fn.Title.html) allows you to set the document’s title from any component. It also takes a `formatter` function that can be used to apply the same format to the title set by other pages. So, for example, if you put `<Title formatter=|text| format!("{text} — My Awesome Site")/>` in your `<App/>` component, and then `<Title text="Page 1"/>` and `<Title text="Page 2"/>` on your routes, you’ll get `Page 1 — My Awesome Site` and `Page 2 — My Awesome Site`. +[`<Title/>`](https://docs.rs/leptos_meta/0.7.0-gamma3/leptos_meta/fn.Title.html) allows you to set the document’s title from any component. It also takes a `formatter` function that can be used to apply the same format to the title set by other pages. So, for example, if you put `<Title formatter=|text| format!("{text} — My Awesome Site")/>` in your `<App/>` component, and then `<Title text="Page 1"/>` and `<Title text="Page 2"/>` on your routes, you’ll get `Page 1 — My Awesome Site` and `Page 2 — My Awesome Site`. -[`<Link/>`](https://docs.rs/leptos_meta/latest/leptos_meta/fn.Link.html) takes the standard attributes of the `<link>` element. +[`<Link/>`](https://docs.rs/leptos_meta/0.7.0-gamma3/leptos_meta/fn.Link.html) injects a `<link>` element into the `<head>`. -[`<Stylesheet/>`](https://docs.rs/leptos_meta/latest/leptos_meta/fn.Stylesheet.html) creates a `<link rel="stylesheet">` with the `href` you give. +[`<Stylesheet/>`](https://docs.rs/leptos_meta/0.7.0-gamma3/leptos_meta/fn.Stylesheet.html) creates a `<link rel="stylesheet">` with the `href` you give. -[`<Style/>`](https://docs.rs/leptos_meta/latest/leptos_meta/fn.Style.html) creates a `<style>` with the children you pass in (usually a string). You can use this to import some custom CSS from another file at compile time `<Style>{include_str!("my_route.css")}</Style>`. +[`<Style/>`](https://docs.rs/leptos_meta/0.7.0-gamma3/leptos_meta/fn.Style.html) creates a `<style>` with the children you pass in (usually a string). You can use this to import some custom CSS from another file at compile time `<Style>{include_str!("my_route.css")}</Style>`. -[`<Meta/>`](https://docs.rs/leptos_meta/latest/leptos_meta/fn.Meta.html) lets you set `<meta>` tags with descriptions and other metadata. +[`<Meta/>`](https://docs.rs/leptos_meta/0.7.0-gamma3/leptos_meta/fn.Meta.html) lets you set `<meta>` tags with descriptions and other metadata. ## `<Script/>` and `<script>` -`leptos_meta` also provides a [`<Script/>`](https://docs.rs/leptos_meta/latest/leptos_meta/fn.Script.html) component, and it’s worth pausing here for a second. All of the other components we’ve considered inject `<head>`-only elements in the `<head>`. But a `<script>` can also be included in the body. +`leptos_meta` also provides a [`<Script/>`](https://docs.rs/leptos_meta/0.7.0-gamma3/leptos_meta/fn.Script.html) component, and it’s worth pausing here for a second. All of the other components we’ve considered inject `<head>`-only elements in the `<head>`. But a `<script>` can also be included in the body. There’s a very simple way to determine whether you should use a capital-S `<Script/>` component or a lowercase-s `<script>` element: the `<Script/>` component will be rendered in the `<head>`, and the `<script>` element will be rendered wherever in the `<body>` of your user interface you put it in, alongside other normal HTML elements. These cause JavaScript to load and run at different times, so use whichever is appropriate to your needs. ## `<Body/>` and `<Html/>` -There are even a couple elements designed to make semantic HTML and styling easier. [`<Html/>`](https://docs.rs/leptos_meta/latest/leptos_meta/fn.Html.html) lets you set the `lang` and `dir` on your `<html>` tag from your application code. `<Html/>` and [`<Body/>`](https://docs.rs/leptos_meta/latest/leptos_meta/fn.Body.html) both have `class` props that let you set their respective `class` attributes, which is sometimes needed by CSS frameworks for styling. - -`<Body/>` and `<Html/>` both also have `attributes` props which can be used to set any number of additional attributes on them via the `attr:` syntax: +There are even a couple elements designed to make semantic HTML and styling easier. `<Body/>` and `<Html/>` are designed to allow you to add arbitrary attributes to the `<html>` and `<body>` tags on your page. You can add any number of attributes using the usual Leptos syntax after the spread operator (`{..}`) and those will be added directly to the appropriate element. ```rust <Html - lang="he" - dir="rtl" - attr:data-theme="dark" + {..} + lang="he" + dir="rtl" + data-theme="dark" /> ``` @@ -46,4 +45,4 @@ Now, some of this is useful in any scenario, but some of it is especially import This is exactly what `leptos_meta` is for. And in fact, during server rendering, this is exactly what it does: collect all the `<head>` content you’ve declared by using its components throughout your application, and then inject it into the actual `<head>`. -But I’m getting ahead of myself. We haven’t actually talked about server-side rendering yet. The next chapter will talk about integrating with JavaScript libraries. Then we’ll wrap up the discussion of the client side, and move onto server side rendering. +But I’m getting ahead of myself. We haven’t actually talked about server-side rendering yet. The next chapter will talk about integrating with JavaScript libraries. Then we’ll wrap up the discussion of the client side, and move onto server side rendering. diff --git a/src/progressive_enhancement/README.md b/src/progressive_enhancement/README.md index 8a992a5..d0ee8c1 100644 --- a/src/progressive_enhancement/README.md +++ b/src/progressive_enhancement/README.md @@ -1,12 +1,13 @@ # Progressive Enhancement (and Graceful Degradation) -I’ve been driving around Boston for about fifteen years. If you don’t know Boston, let me tell you: Massachusetts has some of the most aggressive drivers(and pedestrians!) in the world. I’ve learned to practice what’s sometimes called “defensive driving”: assuming that someone’s about to swerve in front of you at an intersection when you have the right of way, preparing for a pedestrian to cross into the street at any moment, and driving accordingly. +I’ve been driving around Boston for about fifteen years. If you don’t know Boston, let me tell you: Massachusetts has some of the most aggressive drivers (and pedestrians!) in the world. I’ve learned to practice what’s sometimes called “defensive driving”: assuming that someone’s about to swerve in front of you at an intersection when you have the right of way, preparing for a pedestrian to cross into the street at any moment, and driving accordingly. “Progressive enhancement” is the “defensive driving” of web design. Or really, that’s “graceful degradation,” although they’re two sides of the same coin, or the same process, from two different directions. **Progressive enhancement**, in this context, means beginning with a simple HTML site or application that works for any user who arrives at your page, and gradually enhancing it with layers of additional features: CSS for styling, JavaScript for interactivity, WebAssembly for Rust-powered interactivity; using particular Web APIs for a richer experience if they’re available and as needed. -**Graceful degradation** means handling failure gracefully when parts of that stack of enhancement *aren’t* available. Here are some sources of failure your users might encounter in your app: +**Graceful degradation** means handling failure gracefully when parts of that stack of enhancement _aren’t_ available. Here are some sources of failure your users might encounter in your app: + - Their browser doesn’t support WebAssembly because it needs to be updated. - Their browser can’t support WebAssembly because browser updates are limited to newer OS versions, which can’t be installed on the device. (Looking at you, Apple.) - They have WASM turned off for security or privacy reasons. @@ -16,7 +17,7 @@ I’ve been driving around Boston for about fifteen years. If you don’t know B - They stepped onto a subway car after loading the initial page and subsequent navigations can’t load data. - ... and so on. -How much of your app still works if one of these holds true? Two of them? Three? +How much of your app still works if one of these holds true? Two of them? Three? If the answer is something like “95%... okay, then 90%... okay, then 75%,” that’s graceful degradation. If the answer is “my app shows a blank screen unless everything works correctly,” that’s... rapid unscheduled disassembly. @@ -27,10 +28,11 @@ Luckily, we’ve got some tools to help. ## Defensive Design There are a few practices that can help your apps degrade more gracefully: + 1. **Server-side rendering.** Without SSR, your app simply doesn’t work without both JS and WASM loading. In some cases this may be appropriate (think internal apps gated behind a login) but in others it’s simply broken. 2. **Native HTML elements.** Use HTML elements that do the things that you want, without additional code: `<a>` for navigation (including to hashes within the page), `<details>` for an accordion, `<form>` to persist information in the URL, etc. 3. **URL-driven state.** The more of your global state is stored in the URL (as a route param or part of the query string), the more of the page can be generated during server rendering and updated by an `<a>` or a `<form>`, which means that not only navigations but state changes can work without JS/WASM. -4. **[`SsrMode::PartiallyBlocked` or `SsrMode::InOrder`](https://docs.rs/leptos_router/latest/leptos_router/enum.SsrMode.html).** Out-of-order streaming requires a small amount of inline JS, but can fail if 1) the connection is broken halfway through the response or 2) the client’s device doesn’t support JS. Async streaming will give a complete HTML page, but only after all resources load. In-order streaming begins showing pieces of the page sooner, in top-down order. “Partially-blocked” SSR builds on out-of-order streaming by replacing `<Suspense/>` fragments that read from blocking resources on the server. This adds marginally to the initial response time (because of the `O(n)` string replacement work), in exchange for a more complete initial HTML response. This can be a good choice for situations in which there’s a clear distinction between “more important” and “less important” content, e.g., blog post vs. comments, or product info vs. reviews. If you choose to block on all the content, you’ve essentially recreated async rendering. +4. **[`SsrMode::PartiallyBlocked` or `SsrMode::InOrder`](https://docs.rs/leptos_router/0.7.0-gamma3/leptos_router/enum.SsrMode.html).** Out-of-order streaming requires a small amount of inline JS, but can fail if 1) the connection is broken halfway through the response or 2) the client’s device doesn’t support JS. Async streaming will give a complete HTML page, but only after all resources load. In-order streaming begins showing pieces of the page sooner, in top-down order. “Partially-blocked” SSR builds on out-of-order streaming by replacing `<Suspense/>` fragments that read from blocking resources on the server. This adds marginally to the initial response time (because of the `O(n)` string replacement work), in exchange for a more complete initial HTML response. This can be a good choice for situations in which there’s a clear distinction between “more important” and “less important” content, e.g., blog post vs. comments, or product info vs. reviews. If you choose to block on all the content, you’ve essentially recreated async rendering. 5. **Leaning on `<form>`s.** There’s been a bit of a `<form>` renaissance recently, and it’s no surprise. The ability of a `<form>` to manage complicated `POST` or `GET` requests in an easily-enhanced way makes it a powerful tool for graceful degradation. The example in [the `<Form/>` chapter](../router/20_form.md), for example, would work fine with no JS/WASM: because it uses a `<form method="GET">` to persist state in the URL, it works with pure HTML by making normal HTTP requests and then progressively enhances to use client-side navigations instead. -There’s one final feature of the framework that we haven’t seen yet, and which builds on this characteristic of forms to build powerful applications: the `<ActionForm/>`. \ No newline at end of file +There’s one final feature of the framework that we haven’t seen yet, and which builds on this characteristic of forms to build powerful applications: the `<ActionForm/>`. diff --git a/src/progressive_enhancement/action_form.md b/src/progressive_enhancement/action_form.md index 25cf4d9..e43e83e 100644 --- a/src/progressive_enhancement/action_form.md +++ b/src/progressive_enhancement/action_form.md @@ -1,50 +1,50 @@ # `<ActionForm/>` -[`<ActionForm/>`](https://docs.rs/leptos_router/latest/leptos_router/fn.ActionForm.html) is a specialized `<Form/>` that takes a server action, and automatically dispatches it on form submission. This allows you to call a server function directly from a `<form>`, even without JS/WASM. +[`<ActionForm/>`](https://docs.rs/leptos/0.7.0-gamma3/leptos/form/fn.ActionForm.html) is a specialized `<Form/>` that takes a server action, and automatically dispatches it on form submission. This allows you to call a server function directly from a `<form>`, even without JS/WASM. The process is simple: -1. Define a server function using the [`#[server]` macro](https://docs.rs/leptos/latest/leptos/attr.server.html) (see [Server Functions](../server/25_server_functions.md).) -2. Create an action using [`create_server_action`](https://docs.rs/leptos/latest/leptos/fn.create_server_action.html), specifying the type of the server function you’ve defined. +1. Define a server function using the [`#[server]` macro](https://docs.rs/leptos/0.7.0-gamma3/leptos/attr.server.html) (see [Server Functions](../server/25_server_functions.md).) +2. Create an action using [`ServerAction::new()`](https://docs.rs/leptos/0.7.0-gamma3/leptos/server/struct.ServerAction.html), specifying the type of the server function you’ve defined. 3. Create an `<ActionForm/>`, providing the server action in the `action` prop. 4. Pass the named arguments to the server function as form fields with the same names. > **Note:** `<ActionForm/>` only works with the default URL-encoded `POST` encoding for server functions, to ensure graceful degradation/correct behavior as an HTML form. ```rust -#[server(AddTodo, "/api")] +#[server] pub async fn add_todo(title: String) -> Result<(), ServerFnError> { todo!() } #[component] fn AddTodo() -> impl IntoView { - let add_todo = create_server_action::<AddTodo>(); - // holds the latest *returned* value from the server - let value = add_todo.value(); - // check if the server has returned an error - let has_error = move || value.with(|val| matches!(val, Some(Err(_)))); - - view! { - <ActionForm action=add_todo> - <label> - "Add a Todo" - // `title` matches the `title` argument to `add_todo` - <input type="text" name="title"/> - </label> - <input type="submit" value="Add"/> - </ActionForm> - } + let add_todo = ServerAction::<AddTodo>::new(); + // holds the latest *returned* value from the server + let value = add_todo.value(); + // check if the server has returned an error + let has_error = move || value.with(|val| matches!(val, Some(Err(_)))); + + view! { + <ActionForm action=add_todo> + <label> + "Add a Todo" + // `title` matches the `title` argument to `add_todo` + <input type="text" name="title"/> + </label> + <input type="submit" value="Add"/> + </ActionForm> + } } ``` -It’s really that easy. With JS/WASM, your form will submit without a page reload, storing its most recent submission in the `.input()` signal of the action, its pending status in `.pending()`, and so on. (See the [`Action`](https://docs.rs/leptos/latest/leptos/struct.Action.html) docs for a refresher, if you need.) Without JS/WASM, your form will submit with a page reload. If you call a `redirect` function (from `leptos_axum` or `leptos_actix`) it will redirect to the correct page. By default, it will redirect back to the page you’re currently on. The power of HTML, HTTP, and isomorphic rendering mean that your `<ActionForm/>` simply works, even with no JS/WASM. +It’s really that easy. With JS/WASM, your form will submit without a page reload, storing its most recent submission in the `.input()` signal of the action, its pending status in `.pending()`, and so on. (See the [`Action`](https://docs.rs/leptos/0.7.0-gamma3/leptos/reactive/actions/struct.Action.html) docs for a refresher, if you need.) Without JS/WASM, your form will submit with a page reload. If you call a `redirect` function (from `leptos_axum` or `leptos_actix`) it will redirect to the correct page. By default, it will redirect back to the page you’re currently on. The power of HTML, HTTP, and isomorphic rendering mean that your `<ActionForm/>` simply works, even with no JS/WASM. ## Client-Side Validation Because the `<ActionForm/>` is just a `<form>`, it fires a `submit` event. You can use either HTML validation, or your own client-side validation logic in an `on:submit`. Just call `ev.prevent_default()` to prevent submission. -The [`FromFormData`](https://docs.rs/leptos_router/latest/leptos_router/trait.FromFormData.html) trait can be helpful here, for attempting to parse your server function’s data type from the submitted form. +The [`FromFormData`](https://docs.rs/leptos/0.7.0-gamma3/leptos/form/trait.FromFormData.html) trait can be helpful here, for attempting to parse your server function’s data type from the submitted form. ```rust let on_submit = move |ev| { @@ -62,9 +62,6 @@ let on_submit = move |ev| { Server function arguments that are structs with nested serializable fields should make use of indexing notation of `serde_qs`. ```rust -use leptos::*; -use leptos_router::*; - #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] struct HeftyData { first_name: String, @@ -73,7 +70,7 @@ struct HeftyData { #[component] fn ComplexInput() -> impl IntoView { - let submit = Action::<VeryImportantFn, _>::server(); + let submit = ServerAction::<VeryImportantFn>::new(); view! { <ActionForm action=submit> @@ -89,12 +86,9 @@ fn ComplexInput() -> impl IntoView { } #[server] -async fn very_important_fn( - hefty_arg: HeftyData, -) -> Result<(), ServerFnError> { +async fn very_important_fn(hefty_arg: HeftyData) -> Result<(), ServerFnError> { assert_eq!(hefty_arg.first_name.as_str(), "leptos"); assert_eq!(hefty_arg.last_name.as_str(), "closures-everywhere"); Ok(()) } - ``` diff --git a/src/reactivity/14_create_effect.md b/src/reactivity/14_create_effect.md index 8f86a58..6adafee 100644 --- a/src/reactivity/14_create_effect.md +++ b/src/reactivity/14_create_effect.md @@ -1,26 +1,26 @@ -# Responding to Changes with `create_effect` +# Responding to Changes with Effects We’ve made it this far without having mentioned half of the reactive system: effects. Reactivity works in two halves: updating individual reactive values (“signals”) notifies the pieces of code that depend on them (“effects”) that they need to run again. These two halves of the reactive system are inter-dependent. Without effects, signals can change within the reactive system but never be observed in a way that interacts with the outside world. Without signals, effects run once but never again, as there’s no observable value to subscribe to. Effects are quite literally “side effects” of the reactive system: they exist to synchronize the reactive system with the non-reactive world outside it. -Hidden behind the whole reactive DOM renderer that we’ve seen so far is a function called `create_effect`. +The renderer uses effects to update parts of the DOM in response to changes in signals. You can create your own effects to synchronize the reactive system with the outside world in other ways. -[`create_effect`](https://docs.rs/leptos_reactive/latest/leptos_reactive/fn.create_effect.html) takes a function as its argument. It immediately runs the function. If you access any reactive signal inside that function, it registers the fact that the effect depends on that signal with the reactive runtime. Whenever one of the signals that the effect depends on changes, the effect runs again. +[`Effect::new`](https://docs.rs/leptos/0.7.0-gamma3/leptos/reactive/effect/struct.Effect.html) takes a function as its argument. It runs this function on the next “tick” of the reactive system. (So for example, if you use it in a component, it will run just _after_ that component has been rendered.) If you access any reactive signal inside that function, it registers the fact that the effect depends on that signal. Whenever one of the signals that the effect depends on changes, the effect runs again. ```rust -let (a, set_a) = create_signal(0); -let (b, set_b) = create_signal(0); +let (a, set_a) = signal(0); +let (b, set_b) = signal(0); -create_effect(move |_| { +Effect::new(move |_| { // immediately prints "Value: 0" and subscribes to `a` - log::debug!("Value: {}", a()); + log::debug!("Value: {}", a.get()); }); ``` 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`](https://docs.rs/leptos_reactive/latest/leptos_reactive/fn.create_isomorphic_effect.html). +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 [`Effect::new_isomorphic`](https://docs.rs/leptos/0.7.0-gamma3/leptos/reactive/effect/struct.Effect.html#method.new_isomorphic). ## Auto-tracking and Dynamic Dependencies @@ -42,18 +42,18 @@ While they’re not a “zero-cost abstraction” in the most technical sense— Imagine that I’m creating some kind of chat software, and I want people to be able to display their full name, or just their first name, and to notify the server whenever their name changes: ```rust -let (first, set_first) = create_signal(String::new()); -let (last, set_last) = create_signal(String::new()); -let (use_last, set_use_last) = create_signal(true); +let (first, set_first) = signal(String::new()); +let (last, set_last) = signal(String::new()); +let (use_last, set_use_last) = signal(true); // this will add the name to the log // any time one of the source signals changes -create_effect(move |_| { +Effect::new(move |_| { log( - if use_last() { - format!("{} {}", first(), last()) + if use_last.get() { + format!("{} {}", first.get(), last.get()) } else { - first() + first.get() }, ) }); @@ -61,23 +61,23 @@ create_effect(move |_| { If `use_last` is `true`, effect should rerun whenever `first`, `last`, or `use_last` changes. But if I toggle `use_last` to `false`, a change in `last` will never cause the full name to change. In fact, `last` will be removed from the dependency list until `use_last` toggles again. This saves us from sending multiple unnecessary requests to the API if I change `last` multiple times while `use_last` is still `false`. -## To `create_effect`, or not to `create_effect`? +## To create an effect, or not to create an effect? Effects are intended to synchronize the reactive system with the non-reactive world outside, not to synchronize between different reactive values. In other words: using an effect to read a value from one signal and set it in another is always sub-optimal. -If you need to define a signal that depends on the value of other signals, use a derived signal or [`create_memo`](https://docs.rs/leptos_reactive/latest/leptos_reactive/fn.create_memo.html). Writing to a signal inside an effect isn’t the end of the world, and it won’t cause your computer to light on fire, but a derived signal or memo is always better—not only because the dataflow is clear, but because the performance is better. +If you need to define a signal that depends on the value of other signals, use a derived signal or a [`Memo`](https://docs.rs/leptos/0.7.0-gamma3/leptos/reactive/computed/struct.Memo.html). Writing to a signal inside an effect isn’t the end of the world, and it won’t cause your computer to light on fire, but a derived signal or memo is always better—not only because the dataflow is clear, but because the performance is better. ```rust -let (a, set_a) = create_signal(0); +let (a, set_a) = signal(0); // ⚠️ not great -let (b, set_b) = create_signal(0); -create_effect(move |_| { - set_b(a() * 2); +let (b, set_b) = signal(0); +Effect::new(move |_| { + set_b.set(a.get() * 2); }); // ✅ woo-hoo! -let b = move || a() * 2; +let b = move || a.get() * 2; ``` If you need to synchronize some reactive value with the non-reactive world outside—like a web API, the console, the filesystem, or the DOM—writing to a signal in an effect is a fine way to do that. In many cases, though, you’ll find that you’re really writing to a signal inside an event listener or something else, not inside an effect. In these cases, you should check out [`leptos-use`](https://leptos-use.rs/) to see if it already provides a reactive wrapping primitive to do that! @@ -89,7 +89,7 @@ If you need to synchronize some reactive value with the non-reactive world outsi We’ve managed to get this far without mentioning effects because they’re built into the Leptos DOM renderer. We’ve seen that you can create a signal and pass it into the `view` macro, and it will update the relevant DOM node whenever the signal changes: ```rust -let (count, set_count) = create_signal(0); +let (count, set_count) = signal(0); view! { <p>{count}</p> @@ -99,16 +99,16 @@ view! { This works because the framework essentially creates an effect wrapping this update. You can imagine Leptos translating this view into something like this: ```rust -let (count, set_count) = create_signal(0); +let (count, set_count) = signal(0); // create a DOM element let document = leptos::document(); let p = document.create_element("p").unwrap(); // create an effect to reactively update the text -create_effect(move |prev_value| { +Effect::new(move |prev_value| { // first, access the signal’s value and convert it to a string - let text = count().to_string(); + let text = count.get().to_string(); // if this is different from the previous value, update the node if prev_value != Some(text) { @@ -122,43 +122,40 @@ create_effect(move |prev_value| { Every time `count` is updated, this effect will rerun. This is what allows reactive, fine-grained updates to the DOM. -## Explicit, Cancelable Tracking with `watch` +## Explicit Tracking with `Effect::watch()` -In addition to `create_effect`, Leptos provides a [`watch`](https://docs.rs/leptos_reactive/latest/leptos_reactive/fn.watch.html) function, which can be used for two main purposes: +In addition to `Effect::new()`, Leptos provides an [`Effect::watch()`](https://docs.rs/leptos/0.7.0-gamma3/leptos/reactive/effect/struct.Effect.html#method.watch) function, which can be used to separate racking and responding to changes by explicitly passing in a set of values to track. -1. Separating tracking and responding to changes by explicitly passing in a set of values to track. -2. Canceling tracking by calling a stop function. - -Like `create_resource`, `watch` takes a first argument, which is reactively tracked, and a second, which is not. Whenever a reactive value in its `deps` argument is changed, the `callback` is run. `watch` returns a function that can be called to stop tracking the dependencies. +`watch` takes a first argument, which is reactively tracked, and a second, which is not. Whenever a reactive value in its `deps` argument is changed, the `callback` is run. `watch` returns an `Effect`, which can be called with `.stop()` to stop tracking the dependencies. ```rust -let (num, set_num) = create_signal(0); +let (num, set_num) = signal(0); -let stop = watch( +let effect = Effect::watch( move || num.get(), move |num, prev_num, _| { - log::debug!("Number: {}; Prev: {:?}", num, prev_num); + leptos::logging::log!("Number: {}; Prev: {:?}", num, prev_num); }, false, ); set_num.set(1); // > "Number: 1; Prev: Some(0)" -stop(); // stop watching +effect.stop(); // stop watching set_num.set(2); // (nothing happens) ``` ```admonish sandbox title="Live example" collapsible=true -[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/14-effect-0-5-d6hkch?file=%2Fsrc%2Fmain.rs%3A1%2C1) +[Click to open CodeSandbox.](https://codesandbox.io/p/devbox/14-effect-0-7-fxpy2d?file=%2Fsrc%2Fmain.rs%3A21%2C28&workspaceId=478437f3-1f86-4b1e-b665-5c27a31451fb) <noscript> Please enable JavaScript to view examples. </noscript> <template> - <iframe src="https://codesandbox.io/p/sandbox/14-effect-0-5-d6hkch?file=%2Fsrc%2Fmain.rs%3A1%2C1" width="100%" height="1000px" style="max-height: 100vh"></iframe> + <iframe src="https://codesandbox.io/p/devbox/14-effect-0-7-fxpy2d?file=%2Fsrc%2Fmain.rs%3A21%2C28&workspaceId=478437f3-1f86-4b1e-b665-5c27a31451fb" width="100%" height="1000px" style="max-height: 100vh"></iframe> </template> ``` @@ -168,7 +165,7 @@ set_num.set(2); // (nothing happens) ```rust use leptos::html::Input; -use leptos::*; +use leptos::prelude::*; #[derive(Copy, Clone)] struct LogContext(RwSignal<Vec<String>>); @@ -177,8 +174,8 @@ struct LogContext(RwSignal<Vec<String>>); fn App() -> impl IntoView { // Just making a visible log here // You can ignore this... - let log = create_rw_signal::<Vec<String>>(vec![]); - let logged = move || log().join("\n"); + let log = RwSignal::<Vec<String>>::new(vec![]); + let logged = move || log.get().join("\n"); // the newtype pattern isn't *necessary* here but is a good practice // it avoids confusion with other possible future `RwSignal<Vec<String>>` contexts @@ -193,17 +190,19 @@ fn App() -> impl IntoView { #[component] fn CreateAnEffect() -> impl IntoView { - let (first, set_first) = create_signal(String::new()); - let (last, set_last) = create_signal(String::new()); - let (use_last, set_use_last) = create_signal(true); + let (first, set_first) = signal(String::new()); + let (last, set_last) = signal(String::new()); + let (use_last, set_use_last) = signal(true); // this will add the name to the log // any time one of the source signals changes - create_effect(move |_| { - log(if use_last() { - with!(|first, last| format!("{first} {last}")) + Effect::new(move |_| { + log(if use_last.get() { + let first = first.read(); + let last = last.read(); + format!("{first} {last}") } else { - first() + first.get() }) }); @@ -219,7 +218,7 @@ fn CreateAnEffect() -> impl IntoView { type="text" name="first" prop:value=first - on:change=move |ev| set_first(event_target_value(&ev)) + on:change:target=move |ev| set_first.set(ev.target().value()) /> </label> <label> @@ -228,7 +227,7 @@ fn CreateAnEffect() -> impl IntoView { type="text" name="last" prop:value=last - on:change=move |ev| set_last(event_target_value(&ev)) + on:change:target=move |ev| set_last.set(ev.target().value()) /> </label> <label> @@ -237,7 +236,7 @@ fn CreateAnEffect() -> impl IntoView { type="checkbox" name="use_last" prop:checked=use_last - on:change=move |ev| set_use_last(event_target_checked(&ev)) + on:change:target=move |ev| set_use_last.set(ev.target().checked()) /> </label> </form> @@ -246,9 +245,9 @@ fn CreateAnEffect() -> impl IntoView { #[component] fn ManualVersion() -> impl IntoView { - let first = create_node_ref::<Input>(); - let last = create_node_ref::<Input>(); - let use_last = create_node_ref::<Input>(); + let first = NodeRef::<Input>::new(); + let last = NodeRef::<Input>::new(); + let use_last = NodeRef::<Input>::new(); let mut prev_name = String::new(); let on_change = move |_| { @@ -280,70 +279,13 @@ fn ManualVersion() -> impl IntoView { } } -#[component] -fn EffectVsDerivedSignal() -> impl IntoView { - let (my_value, set_my_value) = create_signal(String::new()); - // Don't do this. - /*let (my_optional_value, set_optional_my_value) = create_signal(Option::<String>::None); - - create_effect(move |_| { - if !my_value.get().is_empty() { - set_optional_my_value(Some(my_value.get())); - } else { - set_optional_my_value(None); - } - });*/ - - // Do this - let my_optional_value = - move || (!my_value.with(String::is_empty)).then(|| Some(my_value.get())); - - view! { - <input prop:value=my_value on:input=move |ev| set_my_value(event_target_value(&ev))/> - - <p> - <code>"my_optional_value"</code> - " is " - <code> - <Show when=move || my_optional_value().is_some() fallback=|| view! { "None" }> - "Some(\"" - {my_optional_value().unwrap()} - "\")" - </Show> - </code> - </p> - } -} - -#[component] -pub fn Show<F, W, IV>( - /// The components Show wraps - children: Box<dyn Fn() -> Fragment>, - /// A closure that returns a bool that determines whether this thing runs - when: W, - /// A closure that returns what gets rendered if the when statement is false - fallback: F, -) -> impl IntoView -where - W: Fn() -> bool + 'static, - F: Fn() -> IV + 'static, - IV: IntoView, -{ - let memoized_when = create_memo(move |_| when()); - - move || match memoized_when.get() { - true => children().into_view(), - false => fallback().into_view(), - } -} - fn log(msg: impl std::fmt::Display) { let log = use_context::<LogContext>().unwrap().0; log.update(|log| log.push(msg.to_string())); } fn main() { - leptos::mount_to_body(App) + leptos::mount::mount_to_body(App) } ``` diff --git a/src/reactivity/interlude_functions.md b/src/reactivity/interlude_functions.md index 0091c01..11f21dd 100644 --- a/src/reactivity/interlude_functions.md +++ b/src/reactivity/interlude_functions.md @@ -6,11 +6,11 @@ application. It sometimes looks a little silly: ```rust // a signal holds a value, and can be updated -let (count, set_count) = create_signal(0); +let (count, set_count) = signal(0); // a derived signal is a function that accesses other signals -let double_count = move || count() * 2; -let count_is_odd = move || count() & 1 == 1; +let double_count = move || count.get() * 2; +let count_is_odd = move || count.get() & 1 == 1; let text = move || if count_is_odd() { "odd" } else { @@ -19,7 +19,7 @@ let text = move || if count_is_odd() { // an effect automatically tracks the signals it depends on // and reruns when they change -create_effect(move |_| { +Effect::new(move |_| { logging::log!("text = {}", text()); }); @@ -54,9 +54,9 @@ Take our typical `<SimpleCounter/>` example in its simplest form: ```rust #[component] pub fn SimpleCounter() -> impl IntoView { - let (value, set_value) = create_signal(0); + let (value, set_value) = signal(0); - let increment = move |_| set_value.update(|value| *value += 1); + let increment = move |_| *set_value.write() += 1; view! { <button on:click=increment> @@ -68,9 +68,17 @@ pub fn SimpleCounter() -> impl IntoView { The `SimpleCounter` function itself runs once. The `value` signal is created once. The framework hands off the `increment` function to the browser as an event listener. When you click the button, the browser calls `increment`, which updates `value` via `set_value`. And that updates the single text node represented in our view by `{value}`. -Closures are key to reactivity. They provide the framework with the ability to rerun the smallest possible unit of your application in response to a change. +Functions are key to reactivity. They provide the framework with the ability to rerun the smallest possible unit of your application in response to a change. So remember two things: 1. Your component function is a setup function, not a render function: it only runs once. -2. For values in your view template to be reactive, they must be functions: either signals (which implement the `Fn` traits) or closures. +2. For values in your view template to be reactive, they must be reactive functions: either signals or closures that capture and read from signals. + +```admonish note +This is actually the primary difference between the stable and nightly versions of Leptos. As you may know, using the nightly compiler and the `nightly` feature allows you to call a signal directly, as a function: so, `value()` instead of `value.get()`. + +But this isn’t just syntax sugar. It allows for an extremely consistent semantic model: Reactive things are functions. Signals are accessed by calling functions. To say “give me a signal as an argument” you can take anything that `impl Fn() -> T`. And this function-based interface makes no distinction between signals, memos, and derived signals: any of them can be accessed by calling them as functions. + +Unfortunately implementing the `Fn` traits on arbitrary structs like signals requires nightly Rust, although this particular feature has mostly just languished and is not likely to change (or be stabilized) any time soon. Many people avoid nightly, for one reason or another. So, over time we’ve moved the defaults for things like documentation toward stable. Unfortunately, this makes the simple mental model of “signals are functions” a bit less straightforward. +``` diff --git a/src/reactivity/working_with_signals.md b/src/reactivity/working_with_signals.md index 5be2496..762a44d 100644 --- a/src/reactivity/working_with_signals.md +++ b/src/reactivity/working_with_signals.md @@ -1,97 +1,84 @@ # Working with Signals -So far we’ve used some simple examples of [`create_signal`](https://docs.rs/leptos/latest/leptos/fn.create_signal.html), which returns a [`ReadSignal`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html) getter and a [`WriteSignal`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html) setter. +So far we’ve used some simple examples of using [`signal`](https://docs.rs/leptos/0.7.0-gamma3/leptos/reactive/signal/fn.signal.html), which returns a [`ReadSignal`](https://docs.rs/leptos/0.7.0-gamma3/leptos/reactive/signal/struct.ReadSignal.html) getter and a [`WriteSignal`](https://docs.rs/leptos/0.7.0-gamma3/leptos/reactive/signal/struct.WriteSignal.html) setter. ## Getting and Setting -There are four basic signal operations: +There are a few basic signal operations: -1. [`.get()`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html#impl-SignalGet%3CT%3E-for-ReadSignal%3CT%3E) clones the current value of the signal and tracks any future changes to the value reactively. -2. [`.with()`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html#impl-SignalWith%3CT%3E-for-ReadSignal%3CT%3E) takes a function, which receives the current value of the signal by reference (`&T`), and tracks any future changes. -3. [`.set()`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html#impl-SignalSet%3CT%3E-for-WriteSignal%3CT%3E) replaces the current value of the signal and notifies any subscribers that they need to update. -4. [`.update()`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html#impl-SignalUpdate%3CT%3E-for-WriteSignal%3CT%3E) takes a function, which receives a mutable reference to the current value of the signal (`&mut T`), and notifies any subscribers that they need to update. (`.update()` doesn’t return the value returned by the closure, but you can use [`.try_update()`](https://docs.rs/leptos/latest/leptos/trait.SignalUpdate.html#tymethod.try_update) if you need to; for example, if you’re removing an item from a `Vec<_>` and want the removed item.) +### Getting -Calling a `ReadSignal` as a function is syntax sugar for `.get()`. Calling a `WriteSignal` as a function is syntax sugar for `.set()`. So +1. [`.read()`](https://docs.rs/leptos/0.7.0-gamma3/leptos/reactive/signal/struct.ReadSignal.html#impl-Read-for-T) returns a read guard which dereferences to the value of the signal, and tracks any future changes to the value of the signal reactively. Note that you cannot update the value of the signal until this guard is dropped, or it will cause a runtime error. +1. [`.with()`](https://docs.rs/leptos/0.7.0-gamma3/leptos/reactive/signal/struct.ReadSignal.html#impl-With-for-T) takes a function, which receives the current value of the signal by reference (`&T`), and tracks the signal. +1. [`.get()`](https://docs.rs/leptos/0.7.0-gamma3/leptos/reactive/signal/struct.ReadSignal.html#impl-Get-for-T) clones the current value of the signal and tracks further changes to the value. -```rust -let (count, set_count) = create_signal(0); -set_count(1); -logging::log!(count()); -``` +`.get()` is the most common method of accessing a signal. `.read()` is useful for methods that take an immutable reference, without cloning the value (`my_vec_signal.read().len()`). `.with()` is useful if you need to do more with that reference, but want to make sure you don’t hold onto the lock longer than you need. -is the same as +### Setting -```rust -let (count, set_count) = create_signal(0); -set_count.set(1); -logging::log!(count.get()); +1. [`.write()`](https://docs.rs/leptos/0.7.0-gamma3/leptos/reactive/signal/struct.WriteSignal.html#impl-Write-for-WriteSignal%3CT,+S%3E) returns a write guard which is a mutable references to the value of the signal, and notifies any subscribers that they need to update. Note that you cannot read from the value of the signal until this guard is dropped, or it will cause a runtime error. +1. [`.update()`](https://docs.rs/leptos/0.7.0-gamma3/leptos/reactive/signal/struct.WriteSignal.html#impl-Update-for-T) takes a function, which receives a mutable reference to the current value of the signal (`&mut T`), and notifies subscribers. (`.update()` doesn’t return the value returned by the closure, but you can use [`.try_update()`](https://docs.rs/leptos/latest/leptos/trait.SignalUpdate.html#tymethod.try_update) if you need to; for example, if you’re removing an item from a `Vec<_>` and want the removed item.) +1. [`.set()`](https://docs.rs/leptos/0.7.0-gamma3/leptos/reactive/signal/struct.WriteSignal.html#impl-Set-for-T) replaces the current value of the signal and notifies subscribers. + +`.set()` is most common for setting a new value; `.write()` is very useful for updating a value in place. Just as is the case with `.read()` and `.with()`, `.update()` can be useful when you want to avoid the possibility of holding on the write lock longer than you intended to. + +```admonish note +These traits are based on trait composition and provided by blanket implementations. For example, `Read` is implemented for any type that implements `Track` and `ReadUntracked`. `With` is implemented for any type that implements `Read`. `Get` is implemented for any type that implements `With` and `Clone`. And so on. + +Similar relationships exist for `Write`, `Update`, and `Set`. + +This is worth noting when reading docs: if you only see `ReadUntracked` and `Track` as implemented traits, you will still be able to use `.with()`, `.get()` (if `T: Clone`), and so on. ``` -You might notice that `.get()` and `.set()` can be implemented in terms of `.with()` and `.update()`. In other words, `count.get()` is identical with `count.with(|n| n.clone())`, and `count.set(1)` is implemented by doing `count.update(|n| *n = 1)`. +## Working with Signals -But of course, `.get()` and `.set()` (or the plain function-call forms!) are much nicer syntax. +You might notice that `.get()` and `.set()` can be implemented in terms of `.read()` and `.write()`, or `.with()` and `.update()`. In other words, `count.get()` is identical with `count.with(|n| n.clone())` or `count.read().clone()`, and `count.set(1)` is implemented by doing `count.update(|n| *n = 1)` or `*count.write() = 1`. -However, there are some very good use cases for `.with()` and `.update()`. +But of course, `.get()` and `.set()` are nicer syntax. + +However, there are some very good use cases for the other methods. For example, consider a signal that holds a `Vec<String>`. ```rust -let (names, set_names) = create_signal(Vec::new()); -if names().is_empty() { +let (names, set_names) = signal(Vec::new()); +if names.get().is_empty() { set_names(vec!["Alice".to_string()]); } ``` -In terms of logic, this is simple enough, but it’s hiding some significant inefficiencies. Remember that `names().is_empty()` is sugar for `names.get().is_empty()`, which clones the value (it’s `names.with(|n| n.clone()).is_empty()`). This means we clone the whole `Vec<String>`, run `is_empty()`, and then immediately throw away the clone. +In terms of logic, this is simple enough, but it’s hiding some significant inefficiencies. Remember that `names.get().is_empty()` clones the value. This means we clone the whole `Vec<String>`, run `is_empty()`, and then immediately throw away the clone. Likewise, `set_names` replaces the value with a whole new `Vec<_>`. This is fine, but we might as well just mutate the original `Vec<_>` in place. ```rust -let (names, set_names) = create_signal(Vec::new()); -if names.with(|names| names.is_empty()) { - set_names.update(|names| names.push("Alice".to_string())); +let (names, set_names) = signal(Vec::new()); +if names.read().is_empty() { + set_names.write().push("Alice".to_string()); } ``` -Now our function simply takes `names` by reference to run `is_empty()`, avoiding that clone. +Now our function simply takes `names` by reference to run `is_empty()`, avoiding that clone, and then mutates the `Vec<_>` in place. -And if you have Clippy on, or if you have sharp eyes, you may notice we can make this even neater: +## Nightly Syntax -```rust -if names.with(Vec::is_empty) { - // ... -} -``` - -After all, `.with()` simply takes a function that takes the value by reference. Since `Vec::is_empty` takes `&self`, we can pass it in directly and avoid the unnecessary closure. - -There are some helper macros to make using `.with()` and `.update()` easier to use, especially when using multiple signals. - -```rust -let (first, _) = create_signal("Bob".to_string()); -let (middle, _) = create_signal("J.".to_string()); -let (last, _) = create_signal("Smith".to_string()); -``` - -If you wanted to concatenate these 3 signals together without unnecessary cloning, you would have to write something like: +When using the `nightly` feature and `nightly` syntax, calling a `ReadSignal` as a function is syntax sugar for `.get()`. Calling a `WriteSignal` as a function is syntax sugar for `.set()`. So ```rust -let name = move || { - first.with(|first| { - middle.with(|middle| last.with(|last| format!("{first} {middle} {last}"))) - }) -}; +let (count, set_count) = signal(0); +set_count(1); +logging::log!(count()); ``` -Which is very long and annoying to write. - -Instead, you can use the `with!` macro to get references to all the signals at the same time. +is the same as ```rust -let name = move || with!(|first, middle, last| format!("{first} {middle} {last}")); +let (count, set_count) = signal(0); +set_count.set(1); +logging::log!(count.get()); ``` -This expands to the same thing as above. Take a look at the [`with!`](https://docs.rs/leptos/latest/leptos/macro.with.html) docs for more info, and the corresponding macros [`update!`](https://docs.rs/leptos/latest/leptos/macro.update.html), [`with_value!`](https://docs.rs/leptos/latest/leptos/macro.with_value.html) and [`update_value!`](https://docs.rs/leptos/latest/leptos/macro.update_value.html). +This is not just syntax sugar, but makes for a more consistent API by making signals semantically the same thing as functions: see the [Interlude](./interlude_functions.md). ## Making signals depend on each other @@ -102,31 +89,39 @@ Often people ask about situations in which some signal needs to change based on **1) B is a function of A.** Create a signal for A and a derived signal or memo for B. ```rust -let (count, set_count) = create_signal(1); // A -let derived_signal_double_count = move || count() * 2; // B is a function of A -let memoized_double_count = create_memo(move |_| count() * 2); // B is a function of A +// A +let (count, set_count) = signal(1); +// B is a function of A +let derived_signal_double_count = move || count.get() * 2; +// B is a function of A +let memoized_double_count = Memo::new(move |_| count.get() * 2); ``` -> For guidance on whether to use a derived signal or a memo, see the docs for [`create_memo`](https://docs.rs/leptos/latest/leptos/fn.create_memo.html) +> For guidance on whether to use a derived signal or a memo, see the docs for [`Memo`](https://docs.rs/leptos/0.7.0-gamma3/leptos/reactive/computed/struct.Memo.html) **2) C is a function of A and some other thing B.** Create signals for A and B and a derived signal or memo for C. ```rust -let (first_name, set_first_name) = create_signal("Bridget".to_string()); // A -let (last_name, set_last_name) = create_signal("Jones".to_string()); // B -let full_name = move || with!(|first_name, last_name| format!("{first_name} {last_name}")); // C is a function of A and B +// A +let (first_name, set_first_name) = signal("Bridget".to_string()); +// B +let (last_name, set_last_name) = signal("Jones".to_string()); +// C is a function of A and B +let full_name = move || format!("{} {}", &*first_name.read(), &*last_name.read())); ``` **3) A and B are independent signals, but sometimes updated at the same time.** When you make the call to update A, make a separate call to update B. ```rust -let (age, set_age) = create_signal(32); // A -let (favorite_number, set_favorite_number) = create_signal(42); // B +// A +let (age, set_age) = signal(32); +// B +let (favorite_number, set_favorite_number) = signal(42); // use this to handle a click on a `Clear` button let clear_handler = move |_| { // update both A and B - set_age(0); - set_favorite_number(0); + set_age.set(0); + set_favorite_number.set(0); }; ``` @@ -138,4 +133,4 @@ b) It increases your chances of accidentally creating things like infinite loops In most situations, it’s best to rewrite things such that there’s a clear, top-down data flow based on derived signals or memos. But this isn’t the end of the world. -> I’m intentionally not providing an example here. Read the [`create_effect`](https://docs.rs/leptos/latest/leptos/fn.create_effect.html) docs to figure out how this would work. +> I’m intentionally not providing an example here. Read the [`Effect`](https://docs.rs/leptos/0.7.0-gamma3/leptos/reactive/effect/struct.Effect.html) docs to figure out how this would work. diff --git a/src/router/16_routes.md b/src/router/16_routes.md index 9c1b5cf..5150b7b 100644 --- a/src/router/16_routes.md +++ b/src/router/16_routes.md @@ -4,83 +4,77 @@ It’s easy to get started with the router. -First things first, make sure you’ve added the `leptos_router` package to your dependencies. Like `leptos`, the router relies on activating a `csr`, `hydrate`, or `ssr` feature. For example, if you’re adding the router to a client-side rendered app, you’ll want to run -```sh -cargo add leptos_router --features=csr -``` +First things first, make sure you’ve added the `leptos_router` package to your dependencies. Unlike `leptos`, this does not have separate `csr` and `hydrate` features; it does have an `ssr` feature, intended for use only on the server side, so activate that for your server-side build. > It’s important that the router is a separate package from `leptos` itself. This means that everything in the router can be defined in user-land code. If you want to create your own router, or use no router, you’re completely free to do that! And import the relevant types from the router, either with something like ```rust -use leptos_router::{Route, RouteProps, Router, RouterProps, Routes, RoutesProps}; -``` - -or simply - -```rust -use leptos_router::*; +use leptos_router::components::{Router, Route, Routes}; ``` ## Providing the `<Router/>` -Routing behavior is provided by the [`<Router/>`](https://docs.rs/leptos_router/latest/leptos_router/fn.Router.html) component. This should usually be somewhere near the root of your application, wrapping the rest of the app. +Routing behavior is provided by the [`<Router/>`](https://docs.rs/leptos_router/0.7.0-gamma3/leptos_router/components/fn.Router.html) component. This should usually be somewhere near the root of your application, wrapping the rest of the app. > You shouldn’t try to use multiple `<Router/>`s in your app. Remember that the router drives global state: if you have multiple routers, which one decides what to do when the URL changes? Let’s start with a simple `<App/>` component using the router: ```rust -use leptos::*; -use leptos_router::*; +use leptos::prelude::*; +use leptos_router::components::Router; #[component] pub fn App() -> impl IntoView { - view! { - <Router> - <nav> - /* ... */ - </nav> - <main> - /* ... */ - </main> - </Router> - } + view! { + <Router> + <nav> + /* ... */ + </nav> + <main> + /* ... */ + </main> + </Router> + } } + ``` ## Defining `<Routes/>` -The [`<Routes/>`](https://docs.rs/leptos_router/latest/leptos_router/fn.Routes.html) component is where you define all the routes to which a user can navigate in your application. Each possible route is defined by a [`<Route/>`](https://docs.rs/leptos_router/latest/leptos_router/fn.Route.html) component. +The [`<Routes/>`](https://docs.rs/leptos_router/0.7.0-gamma3/leptos_router/components/fn.Routes.html) component is where you define all the routes to which a user can navigate in your application. Each possible route is defined by a [`<Route/>`](https://docs.rs/leptos_router/0.7.0-gamma3/leptos_router/components/fn.Route.html) component. You should place the `<Routes/>` component at the location within your app where you want routes to be rendered. Everything outside `<Routes/>` will be present on every page, so you can leave things like a navigation bar or menu outside the `<Routes/>`. ```rust -use leptos::*; -use leptos_router::*; +use leptos::prelude::*; +use leptos_router::components::*; #[component] pub fn App() -> impl IntoView { - view! { - <Router> - <nav> - /* ... */ - </nav> - <main> - // all our routes will appear inside <main> - <Routes> + view! { + <Router> + <nav> /* ... */ - </Routes> - </main> - </Router> - } + </nav> + <main> + // all our routes will appear inside <main> + <Routes fallback=|| "Not found."> + /* ... */ + </Routes> + </main> + </Router> + } } ``` +`<Routes/>` should also have a `fallback`, a function that defines what should be shown if no route is matched. + Individual routes are defined by providing children to `<Routes/>` with the `<Route/>` component. `<Route/>` takes a `path` and a `view`. When the current location matches `path`, the `view` will be created and displayed. -The `path` can include +The `path` is most easily defined using the `path` macro, and can include - a static path (`/users`), - dynamic, named parameters beginning with a colon (`/:id`), @@ -89,11 +83,11 @@ The `path` can include The `view` is a function that returns a view. Any component with no props works here, as does a closure that returns some view. ```rust -<Routes> - <Route path="/" view=Home/> - <Route path="/users" view=Users/> - <Route path="/users/:id" view=UserProfile/> - <Route path="/*any" view=|| view! { <h1>"Not Found"</h1> }/> +<Routes fallback=|| "Not found."> + <Route path=path!("/") view=Home/> + <Route path=path!("/users") view=Users/> + <Route path=path!("/users/:id") view=UserProfile/> + <Route path=path!("/*any") view=|| view! { <h1>"Not Found"</h1> }/> </Routes> ``` @@ -101,47 +95,4 @@ The `view` is a function that returns a view. Any component with no props works Now if you navigate to `/` or to `/users` you’ll get the home page or the `<Users/>`. If you go to `/users/3` or `/blahblah` you’ll get a user profile or your 404 page (`<NotFound/>`). On every navigation, the router determines which `<Route/>` should be matched, and therefore what content should be displayed where the `<Routes/>` component is defined. -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/src/router/17_nested_routing.md b/src/router/17_nested_routing.md index 7610b37..90d73a8 100644 --- a/src/router/17_nested_routing.md +++ b/src/router/17_nested_routing.md @@ -3,11 +3,11 @@ We just defined the following set of routes: ```rust -<Routes> - <Route path="/" view=Home/> - <Route path="/users" view=Users/> - <Route path="/users/:id" view=UserProfile/> - <Route path="/*any" view=NotFound/> +<Routes fallback=|| "Not found."> + <Route path=path!("/") view=Home/> + <Route path=path!("/users") view=Users/> + <Route path=path!("/users/:id") view=UserProfile/> + <Route path=path!("/*any") view=|| view! { <h1>"Not Found"</h1> }/> </Routes> ``` @@ -16,15 +16,17 @@ There’s a certain amount of duplication here: `/users` and `/users/:id`. This Well... you can! ```rust -<Routes> - <Route path="/" view=Home/> - <Route path="/users" view=Users> - <Route path=":id" view=UserProfile/> - </Route> - <Route path="/*any" view=NotFound/> +<Routes fallback=|| "Not found."> + <Route path=path!("/") view=Home/> + <ParentRoute path=path!("/users") view=Users> + <Route path=path!(":id") view=UserProfile/> + </ParentRoute> + <Route path=path!("/*any") view=|| view! { <h1>"Not Found"</h1> }/> </Routes> ``` +You can nest a `<Route/>` inside a `<ParentRoute/>`. Seems straightforward. + But wait. We’ve just subtly changed what our application does. The next section is one of the most important in this entire routing section of the guide. Read it carefully, and feel free to ask questions if there’s anything you don’t understand. @@ -38,9 +40,9 @@ Let me put that another way: The goal of defining nested routes is not primarily Let’s look back at our practical example. ```rust -<Routes> - <Route path="/users" view=Users/> - <Route path="/users/:id" view=UserProfile/> +<Routes fallback=|| "Not found."> + <Route path=path!("/users") view=Users/> + <Route path=path!("/users/:id") view=UserProfile/> </Routes> ``` @@ -52,10 +54,10 @@ This means: Let’s say I use nested routes instead: ```rust -<Routes> - <Route path="/users" view=Users> - <Route path=":id" view=UserProfile/> - </Route> +<Routes fallback=|| "Not found."> + <ParentRoute path=path!("/users") view=Users> + <Route path=path!(":id") view=UserProfile/> + </ParentRoute> </Routes> ``` @@ -93,30 +95,30 @@ Most web applications contain levels of navigation that correspond to different You can easily define this with nested routes ```rust -<Routes> - <Route path="/contacts" view=ContactList> - <Route path=":id" view=ContactInfo/> - <Route path="" view=|| view! { +<Routes fallback=|| "Not found."> + <ParentRoute path=path!("/contacts") view=ContactList> + <Route path=path!(":id") view=ContactInfo/> + <Route path=path!("") view=|| view! { <p>"Select a contact to view more info."</p> }/> - </Route> + </ParentRoute> </Routes> ``` You can go even deeper. Say you want to have tabs for each contact’s address, email/phone, and your conversations with them. You can add _another_ set of nested routes inside `:id`: ```rust -<Routes> - <Route path="/contacts" view=ContactList> - <Route path=":id" view=ContactInfo> - <Route path="" view=EmailAndPhone/> - <Route path="address" view=Address/> - <Route path="messages" view=Messages/> - </Route> - <Route path="" view=|| view! { +<Routes fallback=|| "Not found."> + <ParentRoute path=path!("/contacts") view=ContactList> + <ParentRoute path=path!(":id") view=ContactInfo> + <Route path=path!("") view=EmailAndPhone/> + <Route path=path!("address") view=Address/> + <Route path=path!("messages") view=Messages/> + </ParentRoute> + <Route path=path!("") view=|| view! { <p>"Select a contact to view more info."</p> }/> - </Route> + </ParentRoute> </Routes> ``` @@ -161,34 +163,35 @@ 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> - } +pub fn App() -> impl IntoView { + view! { + <Router> + <Routes fallback=|| "Not found."> + <Route path=path!("/contacts") view=ContactList> + <ContactInfoRoutes/> + <Route path=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> - } +fn ContactInfoRoutes() -> impl MatchNestedRoutes + Clone { + view! { + <ParentRoute path=path!(":id") view=ContactInfo> + <Route path=path!("") view=EmailAndPhone/> + <Route path=path!("address") view=Address/> + <Route path=path!("messages") view=Messages/> + </ParentRoute> + } + .into_inner() } ``` -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. +This second component is a `#[component(transparent)]`, meaning it just returns its data, not a view; likewise, it uses `.into_inner()` to remove some debug info added by the `view` macro and just return the route definitions created by `<ParentRoute/>`. ## Nested Routing and Performance @@ -206,14 +209,14 @@ In fact, in this case, we don’t even need to rerender the `<Contact/>` compone ```admonish sandbox title="Live example" collapsible=true -[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/16-router-0-5-4xp4zz?file=%2Fsrc%2Fmain.rs%3A102%2C2) +[Click to open CodeSandbox.](https://codesandbox.io/p/devbox/16-router-0-7-csm8t5?file=%2Fsrc%2Fmain.rs) <noscript> Please enable JavaScript to view examples. </noscript> <template> - <iframe src="https://codesandbox.io/p/sandbox/16-router-0-5-4xp4zz?file=%2Fsrc%2Fmain.rs%3A102%2C2" width="100%" height="1000px" style="max-height: 100vh"></iframe> + <iframe src="https://codesandbox.io/p/devbox/16-router-0-7-csm8t5?file=%2Fsrc%2Fmain.rs" width="100%" height="1000px" style="max-height: 100vh"></iframe> </template> ``` @@ -222,11 +225,13 @@ In fact, in this case, we don’t even need to rerender the `<Contact/>` compone <summary>CodeSandbox Source</summary> ```rust -use leptos::*; -use leptos_router::*; +use leptos::prelude::*; +use leptos_router::components::{Outlet, ParentRoute, Route, Router, Routes, A}; +use leptos_router::hooks::use_params_map; +use leptos_router::path; #[component] -fn App() -> impl IntoView { +pub fn App() -> impl IntoView { view! { <Router> <h1>"Contact App"</h1> @@ -235,41 +240,40 @@ fn App() -> impl IntoView { // note: we can just use normal <a> tags // and the router will use client-side navigation <nav> - <h2>"Navigation"</h2> <a href="/">"Home"</a> <a href="/contacts">"Contacts"</a> </nav> <main> - <Routes> + <Routes fallback=|| "Not found."> // / just has an un-nested "Home" - <Route path="/" view=|| view! { + <Route path=path!("/") view=|| view! { <h3>"Home"</h3> }/> // /contacts has nested routes - <Route - path="/contacts" + <ParentRoute + path=path!("/contacts") view=ContactList > // if no id specified, fall back - <Route path=":id" view=ContactInfo> - <Route path="" view=|| view! { + <ParentRoute path=path!(":id") view=ContactInfo> + <Route path=path!("") view=|| view! { <div class="tab"> "(Contact Info)" </div> }/> - <Route path="conversations" view=|| view! { + <Route path=path!("conversations") view=|| view! { <div class="tab"> "(Conversations)" </div> }/> - </Route> + </ParentRoute> // if no id specified, fall back - <Route path="" view=|| view! { + <Route path=path!("") view=|| view! { <div class="select-user"> "Select a user to view contact info." </div> }/> - </Route> + </ParentRoute> </Routes> </main> </Router> @@ -281,8 +285,8 @@ fn ContactList() -> impl IntoView { view! { <div class="contact-list"> // here's our contact list component itself + <h3>"Contacts"</h3> <div class="contact-list-contacts"> - <h3>"Contacts"</h3> <A href="alice">"Alice"</A> <A href="bob">"Bob"</A> <A href="steve">"Steve"</A> @@ -300,7 +304,7 @@ fn ContactList() -> impl IntoView { fn ContactInfo() -> impl IntoView { // we can access the :id param reactively with `use_params_map` let params = use_params_map(); - let id = move || params.with(|params| params.get("id").cloned().unwrap_or_default()); + let id = move || params.read().get("id").unwrap_or_default(); // imagine we're loading data from an API here let name = move || match id().as_str() { @@ -311,8 +315,8 @@ fn ContactInfo() -> impl IntoView { }; view! { + <h4>{name}</h4> <div class="contact-info"> - <h4>{name}</h4> <div class="tabs"> <A href="" exact=true>"Contact Info"</A> <A href="conversations">"Conversations"</A> @@ -326,7 +330,7 @@ fn ContactInfo() -> impl IntoView { } fn main() { - leptos::mount_to_body(App) + leptos::mount::mount_to_body(App) } ``` diff --git a/src/router/18_params_and_queries.md b/src/router/18_params_and_queries.md index acf2195..6fdfc44 100644 --- a/src/router/18_params_and_queries.md +++ b/src/router/18_params_and_queries.md @@ -11,12 +11,12 @@ Because of the way URLs are built, you can access the query from _any_ `<Route/> Accessing params and queries is pretty simple with a couple of hooks: -- [`use_query`](https://docs.rs/leptos_router/latest/leptos_router/fn.use_query.html) or [`use_query_map`](https://docs.rs/leptos_router/latest/leptos_router/fn.use_query_map.html) -- [`use_params`](https://docs.rs/leptos_router/latest/leptos_router/fn.use_params.html) or [`use_params_map`](https://docs.rs/leptos_router/latest/leptos_router/fn.use_params_map.html) +- [`use_query`](https://docs.rs/leptos_router/0.7.0-gamma3/leptos_router/hooks/fn.use_query.html) or [`use_query_map`](https://docs.rs/leptos_router/0.7.0-gamma3/leptos_router/hooks/fn.use_query_map.html) +- [`use_params`](https://docs.rs/leptos_router/0.7.0-gamma3/leptos_router/hooks/fn.use_params.html) or [`use_params_map`](https://docs.rs/leptos_router/0.7.0-gamma3/leptos_router/hooks/fn.use_params_map.html) Each of these comes with a typed option (`use_query` and `use_params`) and an untyped option (`use_query_map` and `use_params_map`). -The untyped versions hold a simple key-value map. To use the typed versions, derive the [`Params`](https://docs.rs/leptos_router/0.2.3/leptos_router/trait.Params.html) trait on a struct. +The untyped versions hold a simple key-value map. To use the typed versions, derive the [`Params`](https://docs.rs/leptos_router/0.7.0-gamma3/leptos_router/params/trait.Params.html) trait on a struct. > `Params` is a very lightweight trait to convert a flat key-value map of strings into a struct by applying `FromStr` to each field. Because of the flat structure of route params and URL queries, it’s significantly less flexible than something like `serde`; it also adds much less weight to your binary. @@ -26,24 +26,19 @@ use leptos_router::*; #[derive(Params, PartialEq)] struct ContactParams { - id: usize + id: Option<usize> } #[derive(Params, PartialEq)] struct ContactSearch { - q: String + q: Option<String> } ``` -> Note: The `Params` derive macro is located at `leptos::Params`, and the `Params` trait is at `leptos_router::Params`. If you avoid using glob imports like `use leptos::*;`, make sure you’re importing the right one for the derive macro. +> Note: The `Params` derive macro is located at `leptos_router::params::Params`. > -> If you are not using the `nightly` feature, you will get the error -> -> ``` -> no function or associated item named `into_param` found for struct `std::string::String` in the current scope -> ``` -> -> At the moment, supporting both `T: FromStr` and `Option<T>` for typed params requires a nightly feature. You can fix this by simply changing the struct to use `q: Option<String>` instead of `q: String`. +> Using stable, you can only use `Option<T>` in params. If you are using the `nightly` feature, +> you can use either `T` or `Option<T>`. Now we can use them in a component. Imagine a URL that has both params and a query, like `/contacts/:id?q=Search`. @@ -55,24 +50,23 @@ let query = use_query::<ContactSearch>(); // id: || -> usize let id = move || { - params.with(|params| { - params.as_ref() - .map(|params| params.id) - .unwrap_or_default() - }) + params + .read() + .as_ref() + .ok() + .and_then(|params| params.id) + .unwrap_or_default() }; ``` -The untyped versions return `Memo<ParamsMap>`. Again, it’s memo to react to changes in the URL. [`ParamsMap`](https://docs.rs/leptos_router/0.2.3/leptos_router/struct.ParamsMap.html) behaves a lot like any other map type, with a `.get()` method that returns `Option<&String>`. +The untyped versions return `Memo<ParamsMap>`. Again, it’s memo to react to changes in the URL. [`ParamsMap`](https://docs.rs/leptos_router/0.7.0-gamma3/leptos_router/params/struct.ParamsMap.html) behaves a lot like any other map type, with a `.get()` method that returns `Option<String>`. ```rust let params = use_params_map(); let query = use_query_map(); // id: || -> Option<String> -let id = move || { - params.with(|params| params.get("id").cloned()) -}; +let id = move || params.read().get("id"); ``` This can get a little messy: deriving a signal that wraps an `Option<_>` or `Result<_>` can involve a couple steps. But it’s worth doing this for two reasons: diff --git a/src/router/19_a.md b/src/router/19_a.md index ecbe834..525f1a8 100644 --- a/src/router/19_a.md +++ b/src/router/19_a.md @@ -13,7 +13,7 @@ In other words, the router will only try to do a client-side navigation when it > This also means that if you need to opt out of client-side routing, you can do so easily. For example, if you have a link to another page on the same domain, but which isn’t part of your Leptos app, you can just use `<a rel="external">` to tell the router it isn’t something it can handle. -The router also provides an [`<A>`](https://docs.rs/leptos_router/latest/leptos_router/fn.A.html) component, which does two additional things: +The router also provides an [`<A>`](https://docs.rs/leptos_router/0.7.0-gamma3/leptos_router/components/fn.A.html) component, which does two additional things: 1. Correctly resolves relative nested routes. Relative routing with ordinary `<a>` tags can be tricky. For example, if you have a route like `/post/:id`, `<A href="1">` will generate the correct relative route, but `<a href="1">` likely will not (depending on where it appears in your view.) `<A/>` resolves routes relative to the path of the nested route within which it appears. 2. Sets the `aria-current` attribute to `page` if this link is the active link (i.e., it’s a link to the page you’re on). This is helpful for accessibility and for styling. For example, if you want to set the link a different color if it’s a link to the page you’re currently on, you can match this attribute with a CSS selector. @@ -22,16 +22,16 @@ The router also provides an [`<A>`](https://docs.rs/leptos_router/latest/leptos_ Your most-used methods of navigating between pages should be with `<a>` and `<form>` elements or with the enhanced `<A/>` and `<Form/>` components. Using links and forms to navigate is the best solution for accessibility and graceful degradation. -On occasion, though, you’ll want to navigate programmatically, i.e., call a function that can navigate to a new page. In that case, you should use the [`use_navigate`](https://docs.rs/leptos_router/latest/leptos_router/fn.use_navigate.html) function. +On occasion, though, you’ll want to navigate programmatically, i.e., call a function that can navigate to a new page. In that case, you should use the [`use_navigate`](https://docs.rs/leptos_router/0.7.0-gamma3/leptos_router/hooks/fn.use_navigate.html) function. ```rust -let navigate = leptos_router::use_navigate(); +let navigate = leptos_router::hooks::use_navigate(); navigate("/somewhere", Default::default()); ``` > You should almost never do something like `<button on:click=move |_| navigate(/* ... */)>`. Any `on:click` that navigates should be an `<a>`, for reasons of accessibility. -The second argument here is a set of [`NavigateOptions`](https://docs.rs/leptos_router/latest/leptos_router/struct.NavigateOptions.html), which includes options to resolve the navigation relative to the current route as the `<A/>` component does, replace it in the navigation stack, include some navigation state, and maintain the current scroll state on navigation. +The second argument here is a set of [`NavigateOptions`](https://docs.rs/leptos_router/0.7.0-gamma3/leptos_router/struct.NavigateOptions.html), which includes options to resolve the navigation relative to the current route as the `<A/>` component does, replace it in the navigation stack, include some navigation state, and maintain the current scroll state on navigation. > Once again, this is the same example. Check out the relative `<A/>` components, and take a look at the CSS in `index.html` to see the ARIA-based styling. diff --git a/src/router/20_form.md b/src/router/20_form.md index dfaeb82..375df5b 100644 --- a/src/router/20_form.md +++ b/src/router/20_form.md @@ -10,7 +10,7 @@ In plain HTML, there are three ways to navigate to another page: Since we have a client-side router, we can do client-side link navigations without reloading the page, i.e., without a full round-trip to the server and back. It makes sense that we can do client-side form navigations in the same way. -The router provides a [`<Form>`](https://docs.rs/leptos_router/latest/leptos_router/fn.Form.html) component, which works like the HTML `<form>` element, but uses client-side navigations instead of full page reloads. `<Form/>` works with both `GET` and `POST` requests. With `method="GET"`, it will navigate to the URL encoded in the form data. With `method="POST"` it will make a `POST` request and handle the server’s response. +The router provides a [`<Form>`](https://docs.rs/leptos_router/0.7.0-gamma3/leptos_router/components/fn.Form.html) component, which works like the HTML `<form>` element, but uses client-side navigations instead of full page reloads. `<Form/>` works with both `GET` and `POST` requests. With `method="GET"`, it will navigate to the URL encoded in the form data. With `method="POST"` it will make a `POST` request and handle the server’s response. `<Form/>` provides the basis for some components like `<ActionForm/>` and `<MultiActionForm/>` that we’ll see in later chapters. But it also enables some powerful patterns of its own. @@ -20,27 +20,28 @@ It turns out that the patterns we’ve learned so far make this easy to implemen ```rust async fn fetch_results() { - // some async function to fetch our search results + // some async function to fetch our search results } #[component] pub fn FormExample() -> impl IntoView { // reactive access to URL query strings let query = use_query_map(); - // search stored as ?q= - let search = move || query().get("q").cloned().unwrap_or_default(); - // a resource driven by the search string - let search_results = create_resource(search, fetch_results); - - view! { - <Form method="GET" action=""> - <input type="search" name="q" value=search/> - <input type="submit"/> - </Form> - <Transition fallback=move || ()> - /* render search results */ - </Transition> - } + // search stored as ?q= + let search = move || query.read().get("q").unwrap_or_default(); + // a resource driven by the search string + let search_results = Resource::new(search, |_| fetch_results()); + + view! { + <Form method="GET" action=""> + <input type="search" name="q" value=search/> + <input type="submit"/> + </Form> + <Transition fallback=move || ()> + /* render search results */ + {todo!()} + </Transition> + } } ``` @@ -64,14 +65,14 @@ You’ll notice that this version drops the `Submit` button. Instead, we add an ```admonish sandbox title="Live example" collapsible=true -[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/20-form-0-5-9g7v9p?file=%2Fsrc%2Fmain.rs%3A1%2C1) +[Click to open CodeSandbox.](https://codesandbox.io/p/devbox/20-form-0-7-m73jsz) <noscript> Please enable JavaScript to view examples. </noscript> <template> - <iframe src="https://codesandbox.io/p/sandbox/20-form-0-5-9g7v9p?file=%2Fsrc%2Fmain.rs%3A1%2C1" width="100%" height="1000px" style="max-height: 100vh"></iframe> + <iframe src="https://codesandbox.io/p/devbox/20-form-0-7-m73jsz" width="100%" height="1000px" style="max-height: 100vh"></iframe> </template> ``` @@ -80,17 +81,19 @@ You’ll notice that this version drops the `Submit` button. Instead, we add an <summary>CodeSandbox Source</summary> ```rust -use leptos::*; -use leptos_router::*; +use leptos::prelude::*; +use leptos_router::components::{Form, Route, Router, Routes}; +use leptos_router::hooks::use_query_map; +use leptos_router::path; #[component] -fn App() -> impl IntoView { +pub fn App() -> impl IntoView { view! { <Router> <h1><code>"<Form/>"</code></h1> <main> - <Routes> - <Route path="" view=FormExample/> + <Routes fallback=|| "Not found."> + <Route path=path!("") view=FormExample/> </Routes> </main> </Router> @@ -101,9 +104,9 @@ fn App() -> impl IntoView { pub fn FormExample() -> impl IntoView { // reactive access to URL query let query = use_query_map(); - let name = move || query().get("name").cloned().unwrap_or_default(); - let number = move || query().get("number").cloned().unwrap_or_default(); - let select = move || query().get("select").cloned().unwrap_or_default(); + let name = move || query.read().get("name").unwrap_or_default(); + let number = move || query.read().get("number").unwrap_or_default(); + let select = move || query.read().get("select").unwrap_or_default(); view! { // read out the URL query strings @@ -182,7 +185,7 @@ pub fn FormExample() -> impl IntoView { } fn main() { - leptos::mount_to_body(App) + leptos::mount::mount_to_body(App) } ``` diff --git a/src/server/25_server_functions.md b/src/server/25_server_functions.md index 03c0960..71c5050 100644 --- a/src/server/25_server_functions.md +++ b/src/server/25_server_functions.md @@ -16,7 +16,7 @@ Actually, I kind of like that example. What would it look like? It’s pretty si ```rust // todo.rs -#[server(AddTodo, "/api")] +#[server] pub async fn add_todo(title: String) -> Result<(), ServerFnError> { let mut conn = db().await?; @@ -59,87 +59,34 @@ move |_| { } ``` -- Server functions are top-level functions defined with `fn`. Unlike event listeners, derived signals, and most everything else in Leptos, they are not closures! As `fn` calls, they have no access to the reactive state of your app or anything else that is not passed in as an argument. And again, this makes perfect sense: When you make a request to the server, the server doesn’t have access to client state unless you send it explicitly. (Otherwise we’d have to serialize the whole reactive system and send it across the wire with every request, which—while it served classic ASP for a while—is a really bad idea.) -- Server function arguments and return values both need to be serializable with `serde`. Again, hopefully this makes sense: while function arguments in general don’t need to be serialized, calling a server function from the browser means serializing the arguments and sending them over HTTP. +- Server functions are top-level functions defined with `fn`. Unlike event listeners, derived signals, and most everything else in Leptos, they are not closures! As `fn` calls, they have no access to the reactive state of your app or anything else that is not passed in as an argument. And again, this makes perfect sense: When you make a request to the server, the server doesn’t have access to client state unless you send it explicitly. (Otherwise we’d have to serialize the whole reactive system and send it across the wire with every request. This would not be a great idea.) +- Server function arguments and return values both need to be serializable. Again, hopefully this makes sense: while function arguments in general don’t need to be serialized, calling a server function from the browser means serializing the arguments and sending them over HTTP. There are a few things to note about the way you define a server function, too. -- Server functions are created by using the [`#[server]` macro](https://docs.rs/leptos_server/latest/leptos_server/index.html#server) to annotate a top-level function, which can be defined anywhere. -- We provide the macro a type name. The type name is used internally as a container to hold, serialize, and deserialize the arguments. -- We provide the macro a path. This is a prefix for the path at which we’ll mount a server function handler on our server. -- You’ll need to have `serde` as a dependency with the `derive` featured enabled for the macro to work properly. You can easily add it to `Cargo.toml` with `cargo add serde --features=derive`. +- Server functions are created by using the [`#[server]` macro](https://docs.rs/leptos/0.7.0-gamma3/leptos/attr.server.html) to annotate a top-level function, which can be defined anywhere. -## Server Function URL Prefixes +Server functions work by using conditional compilation. On the server, the server function creates an HTTP endpoint that receives its arguments as an HTTP request, and returns its result as an HTTP response. For the client-side/browser build, the body of the server function is stubbed out with an HTTP request. -You can optionally define a specific URL prefix to be used in the definition of the server function. -This is done by providing an optional 2nd argument to the `#[server]` macro. -By default the URL prefix will be `/api`, if not specified. -Here are some examples: +```admonish warning +### An Important Note about Security -```rust -#[server(AddTodo)] // will use the default URL prefix of `/api` -#[server(AddTodo, "/foo")] // will use the URL prefix of `/foo` -``` - -## Server Function Encodings - -By default, the server function call is a `POST` request that serializes the arguments as URL-encoded form data in the body of the request. (This means that server functions can be called from HTML forms, which we’ll see in a future chapter.) But there are a few other methods supported. Optionally, we can provide another argument to the `#[server]` macro to specify an alternate encoding: - -```rust -#[server(AddTodo, "/api", "Url")] -#[server(AddTodo, "/api", "GetJson")] -#[server(AddTodo, "/api", "Cbor")] -#[server(AddTodo, "/api", "GetCbor")] +Server functions are a cool technology, but it’s very important to remember. **Server functions are not magic; they’re syntax sugar for defining a public API.** The _body_ of a server function is never made public; it’s just part of your server binary. But the server function is a publicly accessible API endpoint, and its return value is just a JSON or similar blob. Do not return information from a server function unless it is public, or you've implemented proper security procedures. These procedures might include authenticating incoming requests, ensuring proper encryption, rate limiting access, and more. ``` -The four options use different combinations of HTTP verbs and encoding methods: - -| Name | Method | Request | Response | -| ----------------- | ------ | ----------- | -------- | -| **Url** (default) | POST | URL encoded | JSON | -| **GetJson** | GET | URL encoded | JSON | -| **Cbor** | POST | CBOR | CBOR | -| **GetCbor** | GET | URL encoded | CBOR | - -In other words, you have two choices: - -- `GET` or `POST`? This has implications for things like browser or CDN caching; while `POST` requests should not be cached, `GET` requests can be. -- Plain text (arguments sent with URL/form encoding, results sent as JSON) or a binary format (CBOR, encoded as a base64 string)? +## Customizing Server Functions -**But remember**: Leptos will handle all the details of this encoding and decoding for you. When you use a server function, it looks just like calling any other asynchronous function! +By default, server functions encode their arguments as an HTTP POST request (using `serde_qs`) and their return values as JSON (using `serde_json`). This default is intended to promote compatibility with the `<form>` element, which has native support for making POST requests, even when WASM is disabled, unsupported, or has not yet loaded. They mount their endpoints at a hashed URL intended to prevent name collisions. -> **Why not `PUT` or `DELETE`? Why URL/form encoding, and not JSON?** -> -> These are reasonable questions. Much of the web is built on REST API patterns that encourage the use of semantic HTTP methods like `DELETE` to delete an item from a database, and many devs are accustomed to sending data to APIs in the JSON format. -> -> The reason we use `POST` or `GET` with URL-encoded data by default is the `<form>` support. For better or for worse, HTML forms don’t support `PUT` or `DELETE`, and they don’t support sending JSON. This means that if you use anything but a `GET` or `POST` request with URL-encoded data, it can only work once WASM has loaded. As we’ll see [in a later chapter](../progressive_enhancement), this isn’t always a great idea. -> -> The CBOR encoding is supported for historical reasons; an earlier version of server functions used a URL encoding that didn’t support nested objects like structs or vectors as server function arguments, which CBOR did. But note that the CBOR forms encounter the same issue as `PUT`, `DELETE`, or JSON: they do not degrade gracefully if the WASM version of your app is not available. +However, there are many ways to customize server functions, with a variety of supported input and output encodings, the ability to set specific endpoints, and so on. - -## Server Functions Endpoint Paths - -By default, a unique path will be generated. You can optionally define a specific endpoint path to be used in the URL. This is done by providing an optional 4th argument to the `#[server]` macro. Leptos will generate the complete path by concatenating the URL prefix (2nd argument) and the endpoint path (4th argument). -For example, - -```rust -#[server(MyServerFnType, "/api", "Url", "hello")] -``` -will generate a server function endpoint at `/api/hello` that accepts a POST request. - -> **Can I use the same server function endpoint path with multiple encodings?** -> -> No. Different server functions must have unique paths. The `#[server]` macro automatically generates unique paths, but you need to be careful if you choose to specify the complete path manually, as the server looks up server functions by their path. - -## An Important Note on Security - -Server functions are a cool technology, but it’s very important to remember. **Server functions are not magic; they’re syntax sugar for defining a public API.** The _body_ of a server function is never made public; it’s just part of your server binary. But the server function is a publicly accessible API endpoint, and its return value is just a JSON or similar blob. Do not return information from a server function unless it is public, or you've implemented proper security procedures. These procedures might include authenticating incoming requests, ensuring proper encryption, rate limiting access, and more. +Take a look at the docs for the [`#[server]` macro](https://docs.rs/leptos/0.7.0-gamma3/leptos/attr.server.html) and [`server_fn` crate](https://docs.rs/server_fn/0.7.0-gamma3/server_fn/), and the extensive [`server_fns_axum` example](https://github.com/leptos-rs/leptos/blob/main/examples/server_fns_axum/src/app.rs) in the repo for more information and examples. ## Integrating Server Functions with Leptos So far, everything I’ve said is actually framework agnostic. (And in fact, the Leptos server function crate has been integrated into Dioxus as well!) Server functions are simply a way of defining a function-like RPC call that leans on Web standards like HTTP requests and URL encoding. -But in a way, they also provide the last missing primitive in our story so far. Because a server function is just a plain Rust async function, it integrates perfectly with the async Leptos primitives we discussed [earlier](https://leptos-rs.github.io/leptos/async/index.html). So you can easily integrate your server functions with the rest of your applications: +But in a way, they also provide the last missing primitive in our story so far. Because a server function is just a plain Rust async function, it integrates perfectly with the async Leptos primitives we discussed [earlier](../async/index.html). So you can easily integrate your server functions with the rest of your applications: - Create **resources** that call the server function to load data from the server - Read these resources under `<Suspense/>` or `<Transition/>` to enable streaming SSR and fallback states while data loads. diff --git a/src/server/26_extractors.md b/src/server/26_extractors.md index e886996..5c66fff 100644 --- a/src/server/26_extractors.md +++ b/src/server/26_extractors.md @@ -6,7 +6,7 @@ The server functions we looked at in the last chapter showed how to run code on We call Leptos a “full-stack” framework, but “full-stack” is always a misnomer (after all, it never means everything from the browser to your power company.) For us, “full stack” means that your Leptos app can run in the browser, and can run on the server, and can integrate the two, drawing together the unique features available in each; as we’ve seen in the book so far, a button click on the browser can drive a database read on the server, both written in the same Rust module. But Leptos itself doesn’t provide the server (or the database, or the operating system, or the firmware, or the electrical cables...) -Instead, Leptos provides integrations for the two most popular Rust web server frameworks, Actix Web ([`leptos_actix`](https://docs.rs/leptos_actix/latest/leptos_actix/)) and Axum ([`leptos_axum`](https://docs.rs/leptos_axum/latest/leptos_axum/)). We’ve built integrations with each server’s router so that you can simply plug your Leptos app into an existing server with `.leptos_routes()`, and easily handle server function calls. +Instead, Leptos provides integrations for the two most popular Rust web server frameworks, Actix Web ([`leptos_actix`](https://docs.rs/leptos_actix/0.7.0-gamma3/leptos_actix/)) and Axum ([`leptos_axum`](https://docs.rs/leptos_axum/0.7.0-gamma3/leptos_axum/)). We’ve built integrations with each server’s router so that you can simply plug your Leptos app into an existing server with `.leptos_routes()`, and easily handle server function calls. > If you haven’t seen our [Actix](https://github.com/leptos-rs/start) and [Axum](https://github.com/leptos-rs/start-axum) templates, now’s a good time to check them out. @@ -18,7 +18,7 @@ Leptos provides `extract` helper functions to let you use these extractors direc ### Actix Extractors -The [`extract` function in `leptos_actix`](https://docs.rs/leptos_actix/latest/leptos_actix/fn.extract.html) takes a handler function as its argument. The handler follows similar rules to an Actix handler: it is an async function that receives arguments that will be extracted from the request and returns some value. The handler function receives that extracted data as its arguments, and can do further `async` work on them inside the body of the `async move` block. It returns whatever value you return back out into the server function. +The [`extract` function in `leptos_actix`](https://docs.rs/leptos_actix/0.7.0-gamma3/leptos_actix/fn.extract.html) takes a handler function as its argument. The handler follows similar rules to an Actix handler: it is an async function that receives arguments that will be extracted from the request and returns some value. The handler function receives that extracted data as its arguments, and can do further `async` work on them inside the body of the `async move` block. It returns whatever value you return back out into the server function. ```rust use serde::Deserialize; @@ -31,7 +31,7 @@ struct MyQuery { #[server] pub async fn actix_extract() -> Result<String, ServerFnError> { use actix_web::dev::ConnectionInfo; - use actix_web::web::{Data, Query}; + use actix_web::web::Query; use leptos_actix::extract; let (Query(search), connection): (Query<MyQuery>, ConnectionInfo) = extract().await?; @@ -41,7 +41,7 @@ pub async fn actix_extract() -> Result<String, ServerFnError> { ### Axum Extractors -The syntax for the [`leptos_axum::extract`](https://docs.rs/leptos_axum/latest/leptos_axum/fn.extract.html) function is very similar. +The syntax for the [`leptos_axum::extract`](https://docs.rs/leptos_axum/0.7.0-gamma3/leptos_axum/fn.extract.html) function is very similar. ```rust use serde::Deserialize; @@ -64,7 +64,7 @@ pub async fn axum_extract() -> Result<String, ServerFnError> { These are relatively simple examples accessing basic data from the server. But you can use extractors to access things like headers, cookies, database connection pools, and more, using the exact same `extract()` pattern. -The Axum `extract` function only supports extractors for which the state is `()`. If you need an extractor that uses `State`, you should use [`extract_with_state`](https://docs.rs/leptos_axum/latest/leptos_axum/fn.extract_with_state.html). This requires you to provide the state. You can do this by extending the existing `LeptosOptions` state using the Axum `FromRef` pattern, which providing the state as context during render and server functions with custom handlers. +The Axum `extract` function only supports extractors for which the state is `()`. If you need an extractor that uses `State`, you should use [`extract_with_state`](https://docs.rs/leptos_axum/0.7.0-gamma3/leptos_axum/fn.extract_with_state.html). This requires you to provide the state. You can do this by extending the existing `LeptosOptions` state using the Axum `FromRef` pattern, which providing the state as context during render and server functions with custom handlers. ```rust use axum::extract::FromRef; @@ -99,7 +99,7 @@ let app = Router::new() This context can then be accessed with a simple `use_context::<T>()` inside your server functions. -If you *need* to use `State` in a server function—for example, if you have an existing Axum extractor that requires `State`—that is also possible using Axum's [`FromRef`](https://docs.rs/axum/latest/axum/extract/derive.FromRef.html) pattern and [`extract_with_state`](https://docs.rs/leptos_axum/latest/leptos_axum/fn.extract_with_state.html). Essentially you'll need to provide the state both via context and via Axum router state: +If you _need_ to use `State` in a server function—for example, if you have an existing Axum extractor that requires `State`—that is also possible using Axum's [`FromRef`](https://docs.rs/axum/latest/axum/extract/derive.FromRef.html) pattern and [`extract_with_state`](https://docs.rs/leptos_axum/0.7.0-gamma3/leptos_axum/fn.extract_with_state.html). Essentially you'll need to provide the state both via context and via Axum router state: ```rust #[derive(FromRef, Debug, Clone)] @@ -127,8 +127,8 @@ let app = Router::new() .fallback(file_and_error_handler) .with_state(app_state); -// ... -#[server] +// ... +#[server] pub async fn uses_state() -> Result<(), ServerFnError> { let state = expect_context::<AppState>(); let SomeStateExtractor(data) = extract_with_state(&state).await?; diff --git a/src/server/27_response.md b/src/server/27_response.md index d701c46..78e6fde 100644 --- a/src/server/27_response.md +++ b/src/server/27_response.md @@ -1,68 +1,71 @@ # Responses and Redirects -Extractors provide an easy way to access request data inside server functions. Leptos also provides a way to modify the HTTP response, using the `ResponseOptions` type (see docs for [Actix](https://docs.rs/leptos_actix/latest/leptos_actix/struct.ResponseOptions.html) or [Axum](https://docs.rs/leptos_axum/latest/leptos_axum/struct.ResponseOptions.html)) types and the `redirect` helper function (see docs for [Actix](https://docs.rs/leptos_actix/latest/leptos_actix/fn.redirect.html) or [Axum](https://docs.rs/leptos_axum/latest/leptos_axum/fn.redirect.html)). +Extractors provide an easy way to access request data inside server functions. Leptos also provides a way to modify the HTTP response, using the `ResponseOptions` type (see docs for [Actix](https://docs.rs/leptos_actix/0.7.0-gamma3/leptos_actix/struct.ResponseOptions.html) or [Axum](https://docs.rs/leptos_axum/0.7.0-gamma3/leptos_axum/struct.ResponseOptions.html)) types and the `redirect` helper function (see docs for [Actix](https://docs.rs/leptos_actix/0.7.0-gamma3/leptos_actix/fn.redirect.html) or [Axum](https://docs.rs/leptos_axum/0.7.0-gamma3/leptos_axum/fn.redirect.html)). ## `ResponseOptions` `ResponseOptions` is provided via context during the initial server rendering response and during any subsequent server function call. It allows you to easily set the status code for the HTTP response, or to add headers to the HTTP response, e.g., to set cookies. ```rust -#[server(TeaAndCookies)] +#[server] pub async fn tea_and_cookies() -> Result<(), ServerFnError> { - use actix_web::{cookie::Cookie, http::header, http::header::HeaderValue}; - use leptos_actix::ResponseOptions; + use actix_web::{ + cookie::Cookie, + http::header::HeaderValue, + http::{header, StatusCode}, + }; + use leptos_actix::ResponseOptions; - // pull ResponseOptions from context - let response = expect_context::<ResponseOptions>(); + // pull ResponseOptions from context + let response = expect_context::<ResponseOptions>(); - // set the HTTP status code - response.set_status(StatusCode::IM_A_TEAPOT); + // set the HTTP status code + response.set_status(StatusCode::IM_A_TEAPOT); - // set a cookie in the HTTP response - let mut cookie = Cookie::build("biscuits", "yes").finish(); - if let Ok(cookie) = HeaderValue::from_str(&cookie.to_string()) { - response.insert_header(header::SET_COOKIE, cookie); - } + // set a cookie in the HTTP response + let cookie = Cookie::build("biscuits", "yes").finish(); + if let Ok(cookie) = HeaderValue::from_str(&cookie.to_string()) { + response.insert_header(header::SET_COOKIE, cookie); + } + Ok(()) } ``` ## `redirect` -One common modification to an HTTP response is to redirect to another page. The Actix and Axum integrations provide a `redirect` function to make this easy to do. `redirect` simply sets an HTTP status code of `302 Found` and sets the `Location` header. - -Here’s a simplified example from our [`session_auth_axum` example](https://github.com/leptos-rs/leptos/blob/a5f73b441c079f9138102b3a7d8d4828f045448c/examples/session_auth_axum/src/auth.rs#L154-L181). +One common modification to an HTTP response is to redirect to another page. The Actix and Axum integrations provide a `redirect` function to make this easy to do. ```rust -#[server(Login, "/api")] +#[server] pub async fn login( username: String, password: String, remember: Option<String>, ) -> Result<(), ServerFnError> { - // pull the DB pool and auth provider from context + // pull the DB pool and auth provider from context let pool = pool()?; let auth = auth()?; - // check whether the user exists + // check whether the user exists let user: User = User::get_from_username(username, &pool) .await .ok_or_else(|| { ServerFnError::ServerError("User does not exist.".into()) })?; - // check whether the user has provided the correct password + // check whether the user has provided the correct password match verify(password, &user.password)? { - // if the password is correct... + // if the password is correct... true => { - // log the user in + // log the user in auth.login_user(user.id); auth.remember_user(remember.is_some()); - // and redirect to the home page + // and redirect to the home page leptos_axum::redirect("/"); Ok(()) } - // if not, return an error + // if not, return an error false => Err(ServerFnError::ServerError( "Password does not match.".to_string(), )), diff --git a/src/ssr/21_cargo_leptos.md b/src/ssr/21_cargo_leptos.md index 4da6038..cc7592f 100644 --- a/src/ssr/21_cargo_leptos.md +++ b/src/ssr/21_cargo_leptos.md @@ -35,9 +35,6 @@ Now `cd` into the directory you’ve created and run cargo leptos watch ``` -> **Note**: Remember that Leptos has a `nightly` feature, which each of these starters use. If you're using the stable Rust compiler, -> that’s fine; just remove the `nightly` feature from each of the Leptos dependencies in your new `Cargo.toml` and you should be all set. - Once your app has compiled you can open up your browser to [`http://localhost:3000`](http://localhost:3000) to see it. `cargo-leptos` has lots of additional features and built in tools. You can learn more [in its `README`](https://github.com/leptos-rs/cargo-leptos/blob/main/README.md). diff --git a/src/ssr/22_life_cycle.md b/src/ssr/22_life_cycle.md index 0d20d9b..9ce16cb 100644 --- a/src/ssr/22_life_cycle.md +++ b/src/ssr/22_life_cycle.md @@ -14,7 +14,7 @@ The [`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos) build tool exist ## On the Server - Your browser makes a `GET` request for that URL to your server. At this point, the browser knows almost nothing about the page that’s going to be rendered. (The question “How does the browser know where to ask for the page?” is an interesting one, but out of the scope of this tutorial!) -- The server receives that request, and checks whether it has a way to handle a `GET` request at that path. This is what the `.leptos_routes()` methods in [`leptos_axum`](https://docs.rs/leptos_axum/0.2.5/leptos_axum/trait.LeptosRoutes.html) and [`leptos_actix`](https://docs.rs/leptos_actix/0.2.5/leptos_actix/trait.LeptosRoutes.html) are for. When the server starts up, these methods walk over the routing structure you provide in `<Routes/>`, generating a list of all possible routes your app can handle and telling the server’s router “for each of these routes, if you get a request... hand it off to Leptos.” +- The server receives that request, and checks whether it has a way to handle a `GET` request at that path. This is what the `.leptos_routes()` methods in [`leptos_axum`](https://docs.rs/leptos_axum/0.7.0-gamma3/leptos_axum/trait.LeptosRoutes.html) and [`leptos_actix`](https://docs.rs/leptos_actix/0.7.0-gamma3/leptos_actix/trait.LeptosRoutes.html) are for. When the server starts up, these methods walk over the routing structure you provide in `<Routes/>`, generating a list of all possible routes your app can handle and telling the server’s router “for each of these routes, if you get a request... hand it off to Leptos.” - The server sees that this route can be handled by Leptos. So it renders your root component (often called something like `<App/>`), providing it with the URL that’s being requested and some other data like the HTTP headers and request metadata. - Your application runs once on the server, building up an HTML version of the component tree that will be rendered at that route. (There’s more to be said here about resources and `<Suspense/>` in the next chapter.) - The server returns this HTML page, also injecting information on how to load the version of your app that has been compiled to WASM so that it can run in the browser. diff --git a/src/ssr/23_ssr_modes.md b/src/ssr/23_ssr_modes.md index f8013cb..a8d44b9 100644 --- a/src/ssr/23_ssr_modes.md +++ b/src/ssr/23_ssr_modes.md @@ -26,7 +26,7 @@ Leptos supports all the major ways of rendering HTML that includes asynchronous If you’re using server-side rendering, the synchronous mode is almost never what you actually want, from a performance perspective. This is because it misses out on an important optimization. If you’re loading async resources during server rendering, you can actually begin loading the data on the server. Rather than waiting for the client to receive the HTML response, then loading its JS + WASM, _then_ realize it needs the resources and begin loading them, server rendering can actually begin loading the resources when the client first makes the response. In this sense, during server rendering an async resource is like a `Future` that begins loading on the server and resolves on the client. As long as the resources are actually serializable, this will always lead to a faster total load time. -> This is why [`create_resource`](https://docs.rs/leptos/latest/leptos/fn.create_resource.html) requires resources data to be serializable by default, and why you need to explicitly use [`create_local_resource`](https://docs.rs/leptos/latest/leptos/fn.create_local_resource.html) for any async data that is not serializable and should therefore only be loaded in the browser itself. Creating a local resource when you could create a serializable resource is always a deoptimization. +> This is why a `Resource` needs its data to be serializable, and why you should use `LocalResource` for any async data that is not serializable and should therefore only be loaded in the browser itself. Creating a local resource when you could create a serializable resource is always a deoptimization. ## Async Rendering @@ -82,14 +82,14 @@ This is useful when you have multiple `<Suspense/>` on the page, and one is more Because it offers the best blend of performance characteristics, Leptos defaults to out-of-order streaming. But it’s really simple to opt into these different modes. You do it by adding an `ssr` property onto one or more of your `<Route/>` components, like in the [`ssr_modes` example](https://github.com/leptos-rs/leptos/blob/main/examples/ssr_modes/src/app.rs). ```rust -<Routes> +<Routes fallback=|| "Not found."> // We’ll load the home page with out-of-order streaming and <Suspense/> - <Route path="" view=HomePage/> + <Route path=path!("") view=HomePage/> // We'll load the posts with async rendering, so they can set // the title and metadata *after* loading the data <Route - path="/post/:id" + path=path!("/post/:id") view=Post ssr=SsrMode::Async /> @@ -100,9 +100,9 @@ For a path that includes multiple nested routes, the most restrictive mode will ## Blocking Resources -Any Leptos versions later than `0.2.5` (i.e., git main and `0.3.x` or later) introduce a new resource primitive with `create_blocking_resource`. A blocking resource still loads asynchronously like any other `async`/`.await` in Rust; it doesn’t block a server thread or anything. Instead, reading from a blocking resource under a `<Suspense/>` blocks the HTML _stream_ from returning anything, including its initial synchronous shell, until that `<Suspense/>` has resolved. +Blocking resources can be created with `Resource::new_blocking`. A blocking resource still loads asynchronously like any other `async`/`.await` in Rust. It doesn’t block a server thread, or anything liek that. Instead, reading from a blocking resource under a `<Suspense/>` blocks the HTML _stream_ from returning anything, including its initial synchronous shell, until that `<Suspense/>` has resolved. -Now from a performance perspective, this is not ideal. None of the synchronous shell for your page will load until that resource is ready. However, rendering nothing means that you can do things like set the `<title>` or `<meta>` tags in your `<head>` in actual HTML. This sounds a lot like `async` rendering, but there’s one big difference: if you have multiple `<Suspense/>` sections, you can block on _one_ of them but still render a placeholder and then stream in the other. +From a performance perspective, this is not ideal. None of the synchronous shell for your page will load until that resource is ready. However, rendering nothing means that you can do things like set the `<title>` or `<meta>` tags in your `<head>` in actual HTML. This sounds a lot like `async` rendering, but there’s one big difference: if you have multiple `<Suspense/>` sections, you can block on _one_ of them but still render a placeholder and then stream in the other. For example, think about a blog post. For SEO and for social sharing, I definitely want my blog post’s title and metadata in the initial HTML `<head>`. But I really don’t care whether comments have loaded yet or not; I’d like to load those as lazily as possible. @@ -111,26 +111,28 @@ With blocking resources, I can do something like this: ```rust #[component] pub fn BlogPost() -> impl IntoView { - let post_data = create_blocking_resource(/* load blog post */); - let comments_data = create_resource(/* load blog comments */); - view! { - <Suspense fallback=|| ()> - {move || { - post_data.with(|data| { - view! { - <Title text=data.title/> - <Meta name="description" content=data.excerpt/> - <article> - /* render the post content */ - </article> - } - }) - }} - </Suspense> - <Suspense fallback=|| "Loading comments..."> - /* render comments data here */ - </Suspense> - } + let post_data = Resource::new_blocking(/* load blog post */); + let comments_data = Resource::new(/* load blog comments */); + view! { + <Suspense fallback=|| ()> + {move || Suspend::new(async move { + let data = post_data.await; + view! { + <Title text=data.title/> + <Meta name="description" content=data.excerpt/> + <article> + /* render the post content */ + </article> + } + })} + </Suspense> + <Suspense fallback=|| "Loading comments..."> + {move || Suspend::new(async move { + let comments = comments_data.await; + todo!() + })} + </Suspense> + } } ``` @@ -139,14 +141,14 @@ The first `<Suspense/>`, with the body of the blog post, will block my HTML stre Combined with the following route definition, which uses `SsrMode::PartiallyBlocked`, the blocking resource will be fully rendered on the server side, making it accessible to users who disable WebAssembly or JavaScript. ```rust -<Routes> +<Routes fallback=|| "Not found."> // We’ll load the home page with out-of-order streaming and <Suspense/> - <Route path="" view=HomePage/> + <Route path=path!("") view=HomePage/> // We'll load the posts with async rendering, so they can set // the title and metadata *after* loading the data <Route - path="/post/:id" + path=path!("/post/:id") view=Post ssr=SsrMode::PartiallyBlocked /> diff --git a/src/ssr/24_hydration_bugs.md b/src/ssr/24_hydration_bugs.md index ce10ce9..60d121e 100644 --- a/src/ssr/24_hydration_bugs.md +++ b/src/ssr/24_hydration_bugs.md @@ -71,17 +71,14 @@ pub fn App() -> impl IntoView { In other words, if this is being compiled to WASM, it has three items; otherwise it’s empty. -When I load the page in the browser, I see nothing. If I open the console I see a bunch of warnings: +When I load the page in the browser, I see nothing. If I open the console I see a panic: ``` -element with id 0-3 not found, ignoring it for hydration -element with id 0-4 not found, ignoring it for hydration -element with id 0-5 not found, ignoring it for hydration -component with id _0-6c not found, ignoring it for hydration -component with id _0-6o not found, ignoring it for hydration +ssr_modes.js:423 panicked at /.../tachys/src/html/element/mod.rs:352:14: +called `Option::unwrap()` on a `None` value ``` -The WASM version of your app, running in the browser, expects to find three items; but the HTML has none. +The WASM version of your app, running in the browser, is expecting to find an element (in fact, it’s expecting three elements!) But the HTML sent from the server has none. #### Solution @@ -107,7 +104,7 @@ There are a few ways to avoid this: 1. Only use libraries that can run on both the server and the client. [`reqwest`](https://docs.rs/reqwest/latest/reqwest/), for example, works for making HTTP requests in both settings. 2. Use different libraries on the server and the client, and gate them using the `#[cfg]` macro. ([Click here for an example](https://github.com/leptos-rs/leptos/blob/main/examples/hackernews/src/api.rs).) -3. Wrap client-only code in `create_effect`. Because `create_effect` only runs on the client, this can be an effective way to access browser APIs that are not needed for initial rendering. +3. Wrap client-only code in `Effect::new`. Because effects only run on the client, this can be an effective way to access browser APIs that are not needed for initial rendering. For example, say that I want to store something in the browser’s `localStorage` whenever a signal changes. @@ -128,9 +125,9 @@ But if I wrap it in an effect... #[component] pub fn App() -> impl IntoView { use gloo_storage::Storage; - create_effect(move |_| { + Effect::new(move |_| { let storage = gloo_storage::LocalStorage::raw(); - logging::log!("{storage:?}"); + log!("{storage:?}"); }); } ``` @@ -145,4 +142,4 @@ In particular, you’ll sometimes see errors about the crate `mio` or missing th You can use `create_effect` to specify that something should only run on the client, and not in the server. Is there a way to specify that something should run only on the server, and not the client? -In fact, there is. The next chapter will cover the topic of server functions in some detail. (In the meantime, you can check out their docs [here](https://docs.rs/leptos_server/latest/leptos_server/index.html).) +In fact, there is. The next chapter will cover the topic of server functions in some detail. (In the meantime, you can check out their docs [here](https://docs.rs/leptos/0.7.0-gamma3/leptos/attr.server.html).) diff --git a/src/ssr/README.md b/src/ssr/README.md index 6adb1c7..2d47034 100644 --- a/src/ssr/README.md +++ b/src/ssr/README.md @@ -1,16 +1,13 @@ # Part 2: Server Side Rendering -The second part of the book is all about how to turn your beautiful UIs into full-stack Rust + Leptos powered websites and applications. - -As you read in the last chapter, there are some limitations to using client-side rendered Leptos apps - over the next few chapters, you'll see how we can overcome those limitations -and get the best performance and SEO out of your Leptos apps. +As you read in the last chapter, there are some limitations to using client-side rendered web applications. This second part of the book will discuss how to use server-side rendering to overcome these limitations and get the best performance and SEO out of your Leptos apps. ```admonish info -When working with Leptos on the server side, you're free to choose either an officially supported Actix-web or Axum integrations, or one of our community supported choices. The full feature set of Leptos is available with the official choices, the community ones may support less. Check their documentation for details. +When working with Leptos on the server side, you’re free to choose either an officially supported Actix or Axum integrations, or one of our community supported choices. The full feature set of Leptos is available with the official choices, the community ones may support less. Check their documentation for details. -We have a variety of community supported choices, including WinterCG-compatible runtimes like Deno or Cloudflare. For Webassembly serverless runtimes we have Spin. There's also Viz and Pavex for more traditional server choices. This list is most likely incomplete, due to the nature of such lists. Writing an integration yourself isn't recommended as a beginner, but medium/advanced Rust users may wish to. Feel free to reach out if you have questions about that on our Discord or Github. +We have a variety of community supported choices, including WinterCG-compatible runtimes like Deno or Cloudflareand server-side WASM runtimes like Spin. Community-supported integrations for Viz and Pavex offer more traditional server choices. Writing an integration yourself isn't recommended as a beginner, but medium/advanced Rust users may wish to. Feel free to reach out if you have questions about that on our Discord or Github. -I'd recommend either Actix or Axum as a beginner, both are fully functional and choosing between them is a matter of personal preference. Axum is a bit more modular, and integrates well with the Tower ecosystem. Actix has been around longer and has a few more first party addon crates. There is no wrong choice there. +I'd recommend either Axum or Actix for beginners. Both are fully functional and choosing between them is a matter of personal preference. There is no wrong choice there, but if you’re looking for a recommendation, the Leptos team currently defaults to Axum for new projects. ``` diff --git a/src/testing.md b/src/testing.md index b9c638c..6fc449e 100644 --- a/src/testing.md +++ b/src/testing.md @@ -15,11 +15,9 @@ For example, instead of embedding logic in a component directly like this: ```rust #[component] pub fn TodoApp() -> impl IntoView { - let (todos, set_todos) = create_signal(vec![Todo { /* ... */ }]); + let (todos, set_todos) = signal(vec![Todo { /* ... */ }]); // ⚠️ this is hard to test because it's embedded in the component - let num_remaining = move || todos.with(|todos| { - todos.iter().filter(|todo| !todo.completed).sum() - }); + let num_remaining = move || todos.read().iter().filter(|todo| !todo.completed).sum(); } ``` @@ -44,9 +42,9 @@ mod tests { #[component] pub fn TodoApp() -> impl IntoView { - let (todos, set_todos) = create_signal(Todos(vec![Todo { /* ... */ }])); + let (todos, set_todos) = signal(Todos(vec![Todo { /* ... */ }])); // ✅ this has a test associated with it - let num_remaining = move || todos.with(Todos::num_remaining); + let num_remaining = move || todos.read().num_remaining(); } ``` @@ -55,7 +53,7 @@ more idiomatic your code will feel and the easier it will be to test. ## 2. Test components with end-to-end (`e2e`) testing -Our [`examples`](https://github.com/leptos-rs/leptos/tree/leptos_0.6/examples) directory has several examples with extensive end-to-end testing, using different testing tools. +Our [`examples`](https://github.com/leptos-rs/leptos/tree/main/examples) directory has several examples with extensive end-to-end testing, using different testing tools. The easiest way to see how to use these is to take a look at the test examples themselves: @@ -67,16 +65,20 @@ This is a fairly simple manual testing setup that uses the [`wasm-pack test`](ht ```rust #[wasm_bindgen_test] -fn clear() { - let document = leptos::document(); +async fn clear() { + let document = document(); let test_wrapper = document.create_element("section").unwrap(); let _ = document.body().unwrap().append_child(&test_wrapper); - mount_to( + // start by rendering our counter and mounting it to the DOM + // note that we start at the initial value of 10 + let _dispose = mount_to( test_wrapper.clone().unchecked_into(), || view! { <SimpleCounter initial_value=10 step=1/> }, ); + // now we extract the buttons by iterating over the DOM + // this would be easier if they had IDs let div = test_wrapper.query_selector("div").unwrap().unwrap(); let clear = test_wrapper .query_selector("button") @@ -84,14 +86,20 @@ fn clear() { .unwrap() .unchecked_into::<web_sys::HtmlElement>(); + // now let's click the `clear` button clear.click(); -assert_eq!( - div.outer_html(), - // here we spawn a mini reactive system to render the test case - run_scope(create_runtime(), || { + // the reactive system is built on top of the async system, so changes are not reflected + // synchronously in the DOM + // in order to detect the changes here, we'll just yield for a brief time after each change, + // allowing the effects that update the view to run + tick().await; + + // now let's test the <div> against the expected value + // we can do this by testing its `outerHTML` + assert_eq!(div.outer_html(), { // it's as if we're creating it with a value of 0, right? - let (value, set_value) = create_signal(0); + let (value, _set_value) = signal(0); // we can remove the event listeners because they're not rendered to HTML view! { @@ -102,38 +110,28 @@ assert_eq!( <button>"+1"</button> </div> } - // the view returned an HtmlElement<Div>, which is a smart pointer for - // a DOM element. So we can still just call .outer_html() + // Leptos supports multiple backend renderers for HTML elements + // .into_view() here is just a convenient way of specifying "use the regular DOM renderer" + .into_view() + // views are lazy -- they describe a DOM tree but don't create it yet + // calling .build() will actually build the DOM elements + .build() + // .build() returned an ElementState, which is a smart pointer for + // a DOM element. So we can still just call .outer_html(), which access the outerHTML on + // the actual DOM element .outer_html() - }) -); -} -``` - -### [`wasm-bindgen-test` with `counters`](https://github.com/leptos-rs/leptos/tree/leptos_0.6/examples/counters/tests/web.rs) - -This more developed test suite uses a system of fixtures to refactor the manual DOM manipulation of the `counter` tests and easily test a wide range of cases. - -#### Sample Test - -```rust -use super::*; -use crate::counters_page as ui; -use pretty_assertions::assert_eq; + }); -#[wasm_bindgen_test] -fn should_increase_the_total_count() { - // Given - ui::view_counters(); - ui::add_counter(); - - // When - ui::increment_counter(1); - ui::increment_counter(1); - ui::increment_counter(1); - - // Then - assert_eq!(ui::total(), 3); + // There's actually an easier way to do this... + // We can just test against a <SimpleCounter/> with the initial value 0 + assert_eq!(test_wrapper.inner_html(), { + let comparison_wrapper = document.create_element("section").unwrap(); + let _dispose = mount_to( + comparison_wrapper.clone().unchecked_into(), + || view! { <SimpleCounter initial_value=0 step=1/>}, + ); + comparison_wrapper.inner_html() + }); } ``` @@ -144,9 +142,6 @@ These tests use the common JavaScript testing tool Playwright to run end-to-end #### Sample Test ```js -import { test, expect } from "@playwright/test"; -import { CountersPage } from "./fixtures/counters_page"; - test.describe("Increment Count", () => { test("should increase the total count", async ({ page }) => { const ui = new CountersPage(page); @@ -162,7 +157,7 @@ test.describe("Increment Count", () => { }); ``` -### [Gherkin/Cucumber Tests with `todo_app_sqlite`](https://github.com/leptos-rs/leptos/blob/leptos_0.6/examples/todo_app_sqlite/e2e/README.md) +### [Gherkin/Cucumber Tests with `todo_app_sqlite`](https://github.com/leptos-rs/leptos/blob/main/examples/todo_app_sqlite/e2e/README.md) You can integrate any testing tool you’d like into this flow. This example uses Cucumber, a testing framework based on natural language. diff --git a/src/view/01_basic_component.md b/src/view/01_basic_component.md index be210c7..c3bddc1 100644 --- a/src/view/01_basic_component.md +++ b/src/view/01_basic_component.md @@ -12,41 +12,55 @@ DOM, with self-contained, defined behavior. Unlike HTML elements, they are in `<App/>` component. ```rust +use leptos::mount::mount_to_body; + fn main() { - leptos::mount_to_body(|| view! { <App/> }) + mount_to_body(App); } ``` -Now let’s define our `<App/>` component itself. Because it’s relatively simple, +Now let’s define our `App` component itself. Because it’s relatively simple, I’ll give you the whole thing up front, then walk through it line by line. ```rust +use leptos::prelude::*; + #[component] fn App() -> impl IntoView { - let (count, set_count) = create_signal(0); + let (count, set_count) = signal(0); view! { <button - on:click=move |_| { - // on stable, this is set_count.set(3); - set_count(3); - } + on:click=move |_| set_count.set(3) > "Click me: " - // on stable, this is move || count.get(); - {move || count()} + {count} </button> + <p> + "Double count: " + {move || count.get() * 2} + </p> } } ``` +## Importing the Prelude + +```rust +use leptos::prelude::*; +``` + +Leptos provides a prelude which includes commonly-used traits and functions. +If you'd prefer to use individual imports, feel free to do that; the compiler +will provide helpful recommendations for each import. + ## The Component Signature ```rust #[component] ``` -Like all component definitions, this begins with the [`#[component]`](https://docs.rs/leptos/latest/leptos/attr.component.html) macro. `#[component]` annotates a function so it can be +Like all component definitions, this begins with the [`#[component]`](https://docs.rs/leptos/0.7.0-gamma3/leptos/attr.component.html) macro. `#[component]` annotates a function so it can be used as a component in your Leptos application. We’ll see some of the other features of this macro in a couple chapters. @@ -60,7 +74,8 @@ Every component is a function with the following characteristics 2. It returns `impl IntoView`, which is an opaque type that includes anything you could return from a Leptos `view`. -> Component function arguments are gathered together into a single props struct which is built by the `view` macro as needed. +> Component function arguments are gathered together into a single props struct +> which is built by the `view` macro as needed. ## The Component Body @@ -70,87 +85,87 @@ few reactive variables, define any side effects that run in response to those va changing, and describe the user interface. ```rust -let (count, set_count) = create_signal(0); +let (count, set_count) = signal(0); ``` -[`create_signal`](https://docs.rs/leptos/latest/leptos/fn.create_signal.html) +[`signal`](https://docs.rs/leptos/0.7.0-gamma3/leptos/reactive/signal/fn.signal.html) creates a signal, the basic unit of reactive change and state management in Leptos. This returns a `(getter, setter)` tuple. To access the current value, you’ll use `count.get()` (or, on `nightly` Rust, the shorthand `count()`). To set the -current value, you’ll call `set_count.set(...)` (or `set_count(...)`). +current value, you’ll call `set_count.set(...)` (or, on nightly, `set_count(...)`). -> `.get()` clones the value and `.set()` overwrites it. In many cases, it’s more efficient to use `.with()` or `.update()`; check out the docs for [`ReadSignal`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html) and [`WriteSignal`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html) if you’d like to learn more about those trade-offs at this point. +> `.get()` clones the value and `.set()` overwrites it. In many cases, it’s more efficient to use `.with()` or `.update()`; check out the docs for [`ReadSignal`](https://docs.rs/leptos/0.7.0-gamma3/leptos/reactive/signal/struct.ReadSignal.html) and [`WriteSignal`](https://docs.rs/leptos/0.7.0-gamma3/leptos/reactive/signal/struct.WriteSignal.html) if you’d like to learn more about those trade-offs at this point. ## The View -Leptos defines user interfaces using a JSX-like format via the [`view`](https://docs.rs/leptos/latest/leptos/macro.view.html) macro. +Leptos defines user interfaces using a JSX-like format via the [`view`](https://docs.rs/leptos/0.7.0-gamma3/leptos/macro.view.html) macro. ```rust view! { <button // define an event listener with on: - on:click=move |_| { - set_count(3); - } + on:click=move |_| set_count.set(3) > // text nodes are wrapped in quotation marks "Click me: " - // blocks can include Rust code - {move || count()} + + // blocks include Rust code + // in this case, it renders the value of the signal + {count} </button> + <p> + "Double count: " + {move || count.get() * 2} + </p> } ``` This should mostly be easy to understand: it looks like HTML, with a special -`on:click` to define a `click` event listener, a text node that’s formatted like -a Rust string, and then... +`on:click` to define a `click` event listener, a few text nodes that look like +Rust strings, and then two values in braces: one, `{count}`, seems pretty easy +to understand (it's just the value of our signal), and then... ```rust -{move || count()} +{move || count.get() * 2} ``` whatever that is. People sometimes joke that they use more closures in their first Leptos application -than they’ve ever used in their lives. And fair enough. Basically, passing a function -into the view tells the framework: “Hey, this is something that might change.” +than they’ve ever used in their lives. And fair enough. + +Passing a function into the view tells the framework: “Hey, this is something +that might change.” When we click the button and call `set_count`, the `count` signal is updated. This -`move || count()` closure, whose value depends on the value of `count`, reruns, -and the framework makes a targeted update to that one specific text node, touching +`move || count.get() * 2` closure, whose value depends on the value of `count`, reruns, +and the framework makes a targeted update to that specific text node, touching nothing else in your application. This is what allows for extremely efficient updates to the DOM. -Now, if you have Clippy on—or if you have a particularly sharp eye—you might notice -that this closure is redundant, at least if you’re in `nightly` Rust. If you’re using -Leptos with `nightly` Rust, signals are already functions, so the closure is unnecessary. -As a result, you can write a simpler view: +Remember—and this is _very important_—only signals and functions are treated as reactive +values in the view. -```rust -view! { - <button /* ... */> - "Click me: " - // identical to {move || count()} - {count} - </button> -} -``` +This means that `{count}` and `{count.get()}` do very different things in your view. +`{count}` passes in a signal, telling the framework to update the view every time `count` changes. +`{count.get()}` accesses the value of `count` once, and passes an `i32` into the view, +rendering it once, unreactively. + +In the same way, `{move || count.get() * 2}` and `{count.get() * 2}` behave differently. +The first one is a function, so it's rendered reactively. The second is a value, so it's +just rendered once, and won't update when `count` changes. -Remember—and this is _very important_—only functions are reactive. This means that -`{count}` and `{count()}` do very different things in your view. `{count}` passes -in a function, telling the framework to update the view every time `count` changes. -`{count()}` accesses the value of `count` once, and passes an `i32` into the view, -rendering it once, unreactively. You can see the difference in the CodeSandbox below! +You can see the difference in the CodeSandbox below! -Let’s make one final change. `set_count(3)` is a pretty useless thing for a click handler to do. Let’s replace “set this value to 3” with “increment this value by 1”: +Let’s make one final change. `set_count.set(3)` is a pretty useless thing for a click handler to do. Let’s replace “set this value to 3” with “increment this value by 1”: ```rust move |_| { - set_count.update(|n| *n += 1); + *set_count.write() += 1; } ``` -You can see here that while `set_count` just sets the value, `set_count.update()` gives us a mutable reference and mutates the value in place. Either one will trigger a reactive update in our UI. +You can see here that while `set_count` just sets the value, `set_count.write()` gives us a mutable reference and mutates the value in place. Either one will trigger a reactive update in our UI. > Throughout this tutorial, we’ll use CodeSandbox to show interactive examples. > Hover over any of the variables to show Rust-Analyzer details @@ -158,7 +173,7 @@ You can see here that while `set_count` just sets the value, `set_count.update() ```admonish sandbox title="Live example" collapsible=true -[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/1-basic-component-3d74p3?file=%2Fsrc%2Fmain.rs%3A1%2C1) +[Click to open CodeSandbox.](https://codesandbox.io/p/devbox/1-basic-component-0-7-qvgdxs?file=%2Fsrc%2Fmain.rs%3A1%2C1-59%2C2&workspaceId=478437f3-1f86-4b1e-b665-5c27a31451fb) <noscript> Please enable JavaScript to view examples. @@ -168,7 +183,7 @@ You can see here that while `set_count` just sets the value, `set_count.update() Other Previews > 8080.` <template> - <iframe src="https://codesandbox.io/p/sandbox/1-basic-component-3d74p3?file=%2Fsrc%2Fmain.rs%3A1%2C1" width="100%" height="1000px" style="max-height: 100vh"></iframe> + <iframe src="https://codesandbox.io/p/devbox/1-basic-component-0-7-qvgdxs?file=%2Fsrc%2Fmain.rs%3A1%2C1-59%2C2&workspaceId=478437f3-1f86-4b1e-b665-5c27a31451fb" width="100%" height="1000px" style="max-height: 100vh"></iframe> </template> ``` @@ -177,7 +192,7 @@ Other Previews > 8080.` <summary>CodeSandbox Source</summary> ```rust -use leptos::*; +use leptos::prelude::*; // The #[component] macro marks a function as a reusable component // Components are the building blocks of your user interface @@ -188,7 +203,7 @@ fn App() -> impl IntoView { // and get a (getter, setter) pair // signals are the basic unit of change in the framework // we'll talk more about them later - let (count, set_count) = create_signal(0); + let (count, set_count) = signal(0); // the `view` macro is how we define the user interface // it uses an HTML-like format that can accept certain Rust values @@ -199,31 +214,32 @@ fn App() -> impl IntoView { // we're able to move `set_count` into the closure // because signals are Copy and 'static - on:click=move |_| { - set_count.update(|n| *n += 1); - } + + on:click=move |_| *set_count.write() += 1 > // text nodes in RSX should be wrapped in quotes, // like a normal Rust string - "Click me" + "Click me: " + {count} </button> <p> <strong>"Reactive: "</strong> // you can insert Rust expressions as values in the DOM // by wrapping them in curly braces // if you pass in a function, it will reactively update - {move || count()} + {move || count.get()} </p> <p> <strong>"Reactive shorthand: "</strong> - // signals are functions, so we can remove the wrapping closure + // you can use signals directly in the view, as a shorthand + // for a function that just wraps the getter {count} </p> <p> <strong>"Not reactive: "</strong> - // NOTE: if you write {count()}, this will *not* be reactive + // NOTE: if you just write {count.get()}, this will *not* be reactive // it simply gets the value of count once - {count()} + {count.get()} </p> } } @@ -233,6 +249,6 @@ fn App() -> impl IntoView { // Because we defined it as `fn App`, we can now use it in a // template as <App/> fn main() { - leptos::mount_to_body(|| view! { <App/> }) + leptos::mount::mount_to_body(App) } ``` diff --git a/src/view/02_dynamic_attributes.md b/src/view/02_dynamic_attributes.md index c76c7ef..c5cdc54 100644 --- a/src/view/02_dynamic_attributes.md +++ b/src/view/02_dynamic_attributes.md @@ -13,22 +13,22 @@ increment a counter. ```rust #[component] fn App() -> impl IntoView { - let (count, set_count) = create_signal(0); + let (count, set_count) = signal(0); view! { <button on:click=move |_| { - set_count.update(|n| *n += 1); + *set_count.write() += 1; } > "Click me: " - {move || count()} + {count} </button> } } ``` -So far, this is just the example from the last chapter. +So far, we’ve covered all of this in the previous chapter. ## Dynamic Classes @@ -37,7 +37,7 @@ For example, let’s say I want to add the class `red` when the count is odd. I do this using the `class:` syntax. ```rust -class:red=move || count() % 2 == 1 +class:red=move || count.get() % 2 == 1 ``` `class:` attributes take @@ -55,11 +55,11 @@ the number switches between even and odd. ```rust <button on:click=move |_| { - set_count.update(|n| *n += 1); + *set_count.write() += 1; } // the class: syntax reactively updates a single class // here, we'll set the `red` class when `count` is odd - class:red=move || count() % 2 == 1 + class:red=move || count.get() % 2 == 1 > "Click me" </button> @@ -78,7 +78,7 @@ the number switches between even and odd. Some CSS class names can’t be directly parsed by the `view` macro, especially if they include a mix of dashes and numbers or other characters. In that case, you can use a tuple syntax: `class=("name", value)` still directly updates a single class. ```rust -class=("button-20", move || count() % 2 == 1) +class=("button-20", move || count.get() % 2 == 1) ``` The tuple syntax also allows to specify multiple classes under a single condition using an array as the first tuple element. @@ -92,24 +92,25 @@ class=(["button-20", "rounded"], move || count() % 2 == 1) Individual CSS properties can be directly updated with a similar `style:` syntax. ```rust - let (x, set_x) = create_signal(0); - view! { - <button - on:click={move |_| { - set_x.update(|n| *n += 10); - }} - // set the `style` attribute - style="position: absolute" - // and toggle individual CSS properties with `style:` - style:left=move || format!("{}px", x() + 100) - style:background-color=move || format!("rgb({}, {}, 100)", x(), 100) - style:max-width="400px" - // Set a CSS variable for stylesheet use - style=("--columns", x) - > - "Click to Move" - </button> - } +let (x, set_x) = signal(0); + +view! { + <button + on:click=move |_| { + *set_count.write() += 10; + } + // set the `style` attribute + style="position: absolute" + // and toggle individual CSS properties with `style:` + style:left=move || format!("{}px", x.get() + 100) + style:background-color=move || format!("rgb({}, {}, 100)", x.get(), 100) + style:max-width="400px" + // Set a CSS variable for stylesheet use + style=("--columns", move || x.get().to_string()) + > + "Click to Move" + </button> +} ``` ## Dynamic Attributes @@ -143,7 +144,7 @@ suppose we want it to move twice as fast: ```rust <progress max="50" - value=move || count() * 2 + value=move || count.get() * 2 /> ``` @@ -151,7 +152,7 @@ But imagine we want to reuse that calculation in more than one place. You can do using a **derived signal**: a closure that accesses a signal. ```rust -let double_count = move || count() * 2; +let double_count = move || count.get() * 2; /* insert the rest of the view */ <progress @@ -190,18 +191,18 @@ for expensive calculations. > } > ``` > -> [Click here for the full `view` macros docs](https://docs.rs/leptos/latest/leptos/macro.view.html). +> [Click here for the full `view` macros docs](https://docs.rs/leptos/0.7.0-gamma3/leptos/macro.view.html). ```admonish sandbox title="Live example" collapsible=true -[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/2-dynamic-attributes-0-5-lwdrpm?file=%2Fsrc%2Fmain.rs%3A1%2C1) +[Click to open CodeSandbox.](https://codesandbox.io/p/devbox/2-dynamic-attributes-0-7-wddqfp?file=%2Fsrc%2Fmain.rs%3A1%2C1-58%2C1) <noscript> Please enable JavaScript to view examples. </noscript> <template> - <iframe src="https://codesandbox.io/p/sandbox/2-dynamic-attributes-0-5-lwdrpm?file=%2Fsrc%2Fmain.rs%3A1%2C1" width="100%" height="1000px" style="max-height: 100vh"></iframe> + <iframe src="https://codesandbox.io/p/devbox/2-dynamic-attributes-0-7-wddqfp?file=%2Fsrc%2Fmain.rs%3A1%2C1-58%2C1" width="100%" height="1000px" style="max-height: 100vh"></iframe> </template> ``` @@ -210,26 +211,26 @@ for expensive calculations. <summary>CodeSandbox Source</summary> ```rust -use leptos::*; +use leptos::prelude::*; #[component] fn App() -> impl IntoView { - let (count, set_count) = create_signal(0); + let (count, set_count) = signal(0); // a "derived signal" is a function that accesses other signals // we can use this to create reactive values that depend on the // values of one or more other signals - let double_count = move || count() * 2; + let double_count = move || count.get() * 2; view! { <button on:click=move |_| { - set_count.update(|n| *n += 1); + *set_count.write() += 1; } - // the class: syntax reactively updates a single class // here, we'll set the `red` class when `count` is odd - class:red=move || count() % 2 == 1 + class:red=move || count.get() % 2 == 1 + class=("button-20", move || count.get() % 2 == 1) > "Click me" </button> @@ -246,7 +247,8 @@ fn App() -> impl IntoView { // signals are functions, so `value=count` and `value=move || count.get()` // are interchangeable. value=count - ></progress> + > + </progress> <br/> // This progress bar will use `double_count` @@ -256,14 +258,15 @@ fn App() -> impl IntoView { // derived signals are functions, so they can also // reactively update the DOM value=double_count - ></progress> + > + </progress> <p>"Count: " {count}</p> <p>"Double Count: " {double_count}</p> } } fn main() { - leptos::mount_to_body(App) + leptos::mount::mount_to_body(App) } ``` diff --git a/src/view/03_components.md b/src/view/03_components.md index 88ab181..daa31ff 100644 --- a/src/view/03_components.md +++ b/src/view/03_components.md @@ -12,8 +12,8 @@ per click. You _could_ do this by just creating two `<progress>` elements: ```rust -let (count, set_count) = create_signal(0); -let double_count = move || count() * 2; +let (count, set_count) = signal(0); +let double_count = move || count.get() * 2; view! { <progress @@ -79,9 +79,9 @@ Now we can use our component in the main `<App/>` component’s view. ```rust #[component] fn App() -> impl IntoView { - let (count, set_count) = create_signal(0); + let (count, set_count) = signal(0); view! { - <button on:click=move |_| { set_count.update(|n| *n += 1); }> + <button on:click=move |_| *set_count.write() += 1> "Click me" </button> // now we use our component! @@ -109,8 +109,7 @@ be a signal. ### `optional` Props Right now the `max` setting is hard-coded. Let’s take that as a prop too. But -let’s add a catch: let’s make this prop optional by annotating the particular -argument to the component function with `#[prop(optional)]`. +let’s make this prop optional. We can do that by annotating it with `#[prop(optional)]`. ```rust #[component] @@ -164,11 +163,11 @@ This is great. But we began with two counters, one driven by `count`, and one by the derived signal `double_count`. Let’s recreate that by using `double_count` as the `progress` prop on another `<ProgressBar/>`. -```rust +```rust,compile_fail #[component] fn App() -> impl IntoView { - let (count, set_count) = create_signal(0); - let double_count = move || count() * 2; + let (count, set_count) = signal(0); + let double_count = move || count.get() * 2; view! { <button on:click=move |_| { set_count.update(|n| *n += 1); }> @@ -187,8 +186,9 @@ that the `progress` prop takes `ReadSignal<i32>`, and `double_count` is not it’s a closure that returns an `i32`. There are a couple ways to handle this. One would be to say: “Well, I know that -a `ReadSignal` is a function, and I know that a closure is a function; maybe I -could just take any function?” If you’re savvy, you may know that both these +for the view to be reactive, it needs to take a function or a signal. I can always +turn a signal into a function by wrapping it in a closure... Maybe I could +just take any function?” If you’re savvy, you may know that both these implement the trait `Fn() -> i32`. So you could use a generic component: ```rust @@ -196,7 +196,7 @@ implement the trait `Fn() -> i32`. So you could use a generic component: fn ProgressBar( #[prop(default = 100)] max: u16, - progress: impl Fn() -> i32 + 'static + progress: impl Fn() -> i32 + Send + Sync + 'static ) -> impl IntoView { view! { <progress @@ -212,7 +212,7 @@ fn ProgressBar( This is a perfectly reasonable way to write this component: `progress` now takes any value that implements this `Fn()` trait. -> Generic props can also be specified using a `where` clause, or using inline generics like `ProgressBar<F: Fn() -> i32 + 'static>`. Note that support for `impl Trait` syntax was released in 0.6.12; if you receive an error message you may need to `cargo update` to ensure that you are on the latest version. +> Generic props can also be specified using a `where` clause, or using inline generics like `ProgressBar<F: Fn() -> i32 + 'static>`. Generics need to be used somewhere in the component props. This is because props are built into a struct, so all generic types must be used somewhere in the struct. This is often easily accomplished using an optional `PhantomData` prop. You can then specify a generic in the view using the syntax for expressing types: `<Component<T>/>` (not with the turbofish-style `<Component::<T>/>`). @@ -240,11 +240,10 @@ This attribute automatically calls `.into()` on the values you pass as props, which allows you to easily pass props with different values. In this case, it’s helpful to know about the -[`Signal`](https://docs.rs/leptos/latest/leptos/struct.Signal.html) type. `Signal` -is an enumerated type that represents any kind of readable reactive signal. It can -be useful when defining APIs for components you’ll want to reuse while passing -different sorts of signals. The [`MaybeSignal`](https://docs.rs/leptos/latest/leptos/enum.MaybeSignal.html) type is useful when you want to be able to take either a static or -reactive value. +[`Signal`](https://docs.rs/leptos/0.7.0-gamma3/leptos/reactive/wrappers/read/struct.Signal.html) type. `Signal` +is an enumerated type that represents any kind of readable reactive signal, or a plain value. +It can be useful when defining APIs for components you’ll want to reuse while passing +different sorts of signals. ```rust #[component] @@ -266,11 +265,11 @@ fn ProgressBar( #[component] fn App() -> impl IntoView { - let (count, set_count) = create_signal(0); - let double_count = move || count() * 2; + let (count, set_count) = signal(0); + let double_count = move || count.get() * 2; view! { - <button on:click=move |_| { set_count.update(|n| *n += 1); }> + <button on:click=move |_| *set_count.write() += 1> "Click me" </button> // .into() converts `ReadSignal` to `Signal` @@ -287,7 +286,7 @@ Note that you can’t specify optional generic props for a component. Let’s se ```rust,compile_fail #[component] -fn ProgressBar<F: Fn() -> i32 + 'static>( +fn ProgressBar<F: Fn() -> i32 + Send + Sync + 'static>( #[prop(optional)] progress: Option<F>, ) -> impl IntoView { progress.map(|progress| { @@ -328,7 +327,7 @@ However, you can get around this by providing a concrete type using `Box<dyn _>` ```rust #[component] fn ProgressBar( - #[prop(optional)] progress: Option<Box<dyn Fn() -> i32>>, + #[prop(optional)] progress: Option<Box<dyn Fn() -> i32 + Send + Sync>>, ) -> impl IntoView { progress.map(|progress| { view! { @@ -387,34 +386,59 @@ type, and each of the fields used to add props. It can be a little hard to understand how powerful this is until you hover over the component name or props and see the power of the `#[component]` macro combined with rust-analyzer here. -> #### Advanced Topic: `#[component(transparent)]` -> -> All Leptos components return `-> impl IntoView`. Some, though, need to return -> some data directly without any additional wrapping. These can be marked with -> `#[component(transparent)]`, in which case they return exactly the value they -> return, without the rendering system transforming them in any way. -> -> This is mostly used in two situations: -> -> 1. Creating wrappers around `<Suspense/>` or `<Transition/>`, which return a -> transparent suspense structure to integrate with SSR and hydration properly. -> 2. Refactoring `<Route/>` definitions for `leptos_router` out into separate -> components, because `<Route/>` is a transparent component that returns a -> `RouteDefinition` struct rather than a view. -> -> In general, you should not need to use transparent components unless you are -> creating custom wrapping components that fall into one of these two categories. +## Spreading Attributes onto Components + +Sometimes you want users to be able to add additional attributes to a component. For example, you might want users to be able to add their own `class` or `id` attributes for styling or other purposes. + +You _could_ do this by creating `class` or `id` props that you then apply to the appropriate element. But Leptos also supports “spreading” additional attributes onto components. Attributes added to a component will be applied to all top-level HTML elements that components returns from its view. + +```rust +// you can create attribute lists by using the view macro with a spread {..} as the tag name +let spread_onto_component = view! { + <{..} aria-label="a component with attribute spreading"/> +}; + + +view! { + // attributes that are spread onto a component will be applied to *all* elements returned as part of + // the component's view. to apply attributes to a subset of the component, pass them via a component prop + <ComponentThatTakesSpread + // plain identifiers are for props + some_prop="foo" + another_prop=42 + + // the class:, style:, prop:, on: syntaxes work just as they do on elements + class:foo=true + style:font-weight="bold" + prop:cool=42 + on:click=move |_| alert("clicked ComponentThatTakesSpread") + + // to pass a plain HTML attribute, prefix it with attr: + attr:id="foo" + + // or, if you want to include multiple attributes, rather than prefixing each with + // attr:, you can separate them from component props with the spread {..} + {..} // everything after this is treated as an HTML attribute + title="ooh, a title!" + + // we can add the whole list of attributes defined above + {..spread_onto_component} + /> +} +``` + +See the [`spread` example](https://github.com/leptos-rs/leptos/blob/main/examples/spread/src/lib.rs) for more examples. ```admonish sandbox title="Live example" collapsible=true -[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/3-components-0-5-5vvl69?file=%2Fsrc%2Fmain.rs%3A1%2C1) +[Click to open CodeSandbox.](https://codesandbox.io/p/devbox/3-components-0-7-rkjn3j?file=%2Fsrc%2Fmain.rs%3A39%2C10) <noscript> Please enable JavaScript to view examples. </noscript> <template> - <iframe src="https://codesandbox.io/p/sandbox/3-components-0-5-5vvl69?file=%2Fsrc%2Fmain.rs%3A1%2C1" width="100%" height="1000px" style="max-height: 100vh"></iframe> + <iframe src="https://codesandbox.io/p/devbox/3-components-0-7-rkjn3j?file=%2Fsrc%2Fmain.rs%3A39%2C10" width="100%" height="1000px" style="max-height: 100vh"></iframe> </template> ``` @@ -423,7 +447,7 @@ and see the power of the `#[component]` macro combined with rust-analyzer here. <summary>CodeSandbox Source</summary> ```rust -use leptos::*; +use leptos::prelude::*; // Composing different components together is how we build // user interfaces. Here, we'll define a reusable <ProgressBar/>. @@ -457,14 +481,14 @@ fn ProgressBar( #[component] fn App() -> impl IntoView { - let (count, set_count) = create_signal(0); + let (count, set_count) = signal(0); - let double_count = move || count() * 2; + let double_count = move || count.get() * 2; view! { <button on:click=move |_| { - set_count.update(|n| *n += 1); + *set_count.write() += 1; } > "Click me" @@ -484,7 +508,7 @@ fn App() -> impl IntoView { } fn main() { - leptos::mount_to_body(App) + leptos::mount::mount_to_body(App) } ``` diff --git a/src/view/04_iteration.md b/src/view/04_iteration.md index f08d07e..d012560 100644 --- a/src/view/04_iteration.md +++ b/src/view/04_iteration.md @@ -53,8 +53,14 @@ You can render dynamic items as part of a static list. ```rust // create a list of 5 signals let length = 5; -let counters = (1..=length).map(|idx| create_signal(idx)); +let counters = (1..=length).map(|idx| RwSignal::new(idx)); +``` + +Note here that instead of calling `signal()` to get a tuple with a reader and a writer, +here we use `RwSignal::new()` to get a single, read-write signal. This is just more convenient +for a situation where we’d otherwise be passing the tuples around. +``` // each item manages a reactive view // but the list itself will never change let counter_buttons = counters @@ -62,7 +68,7 @@ let counter_buttons = counters view! { <li> <button - on:click=move |_| set_count.update(|n| *n += 1) + on:click=move |_| *set_count.write() += 1 > {count} </button> @@ -82,10 +88,10 @@ Fortunately, there’s a better way. ## Dynamic Rendering with the `<For/>` Component -The [`<For/>`](https://docs.rs/leptos/latest/leptos/fn.For.html) component is a +The [`<For/>`](https://docs.rs/leptos/0.7.0-gamma3/leptos/control_flow/fn.For.html) component is a keyed dynamic list. It takes three props: -- `each`: a function (such as a signal) that returns the items `T` to be iterated over +- `each`: a reactive function that returns the items `T` to be iterated over - `key`: a key function that takes `&T` and returns a stable, unique key or ID - `children`: renders each `T` into a view @@ -106,14 +112,14 @@ Check out the `<DynamicList/>` component below for an example. ```admonish sandbox title="Live example" collapsible=true -[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/4-iteration-0-5-pwdn2y?file=%2Fsrc%2Fmain.rs%3A1%2C1) +[Click to open CodeSandbox.](https://codesandbox.io/p/devbox/4-iteration-0-7-dw4dfl?file=%2Fsrc%2Fmain.rs%3A1%2C1-159%2C1&workspaceId=478437f3-1f86-4b1e-b665-5c27a31451fb) <noscript> Please enable JavaScript to view examples. </noscript> <template> - <iframe src="https://codesandbox.io/p/sandbox/4-iteration-0-5-pwdn2y?file=%2Fsrc%2Fmain.rs%3A1%2C1" width="100%" height="1000px" style="max-height: 100vh"></iframe> + <iframe src="https://codesandbox.io/p/devbox/4-iteration-0-7-dw4dfl?file=%2Fsrc%2Fmain.rs%3A1%2C1-159%2C1&workspaceId=478437f3-1f86-4b1e-b665-5c27a31451fb" width="100%" height="1000px" style="max-height: 100vh"></iframe> </template> ``` @@ -122,7 +128,7 @@ Check out the `<DynamicList/>` component below for an example. <summary>CodeSandbox Source</summary> ```rust -use leptos::*; +use leptos::prelude::*; // Iteration is a very common task in most applications. // So how do you take a list of data and render it in the DOM? @@ -151,17 +157,17 @@ fn StaticList( length: usize, ) -> impl IntoView { // create counter signals that start at incrementing numbers - let counters = (1..=length).map(|idx| create_signal(idx)); + let counters = (1..=length).map(|idx| RwSignal::new(idx)); // when you have a list that doesn't change, you can // manipulate it using ordinary Rust iterators // and collect it into a Vec<_> to insert it into the DOM let counter_buttons = counters - .map(|(count, set_count)| { + .map(|count| { view! { <li> <button - on:click=move |_| set_count.update(|n| *n += 1) + on:click=move |_| *count.write() += 1 > {count} </button> @@ -198,18 +204,24 @@ fn DynamicList( // we generate an initial list as in <StaticList/> // but this time we include the ID along with the signal + // see NOTE in add_counter below re: ArcRwSignal let initial_counters = (0..initial_length) - .map(|id| (id, create_signal(id + 1))) + .map(|id| (id, ArcRwSignal::new(id + 1))) .collect::<Vec<_>>(); // now we store that initial list in a signal // this way, we'll be able to modify the list over time, // adding and removing counters, and it will change reactively - let (counters, set_counters) = create_signal(initial_counters); + let (counters, set_counters) = signal(initial_counters); let add_counter = move |_| { // create a signal for the new counter - let sig = create_signal(next_counter_id + 1); + // we use ArcRwSignal here, instead of RwSignal + // ArcRwSignal is a reference-counted type, rather than the arena-allocated + // signal types we've been using so far. + // When we're creating a collection of signals like this, using ArcRwSignal + // allows each signal to be deallocated when its row is removed. + let sig = ArcRwSignal::new(next_counter_id + 1); // add this counter to the list of counters set_counters.update(move |counters| { // since `.update()` gives us `&mut T` @@ -232,7 +244,7 @@ fn DynamicList( // `each` takes any function that returns an iterator // this should usually be a signal or derived signal // if it's not reactive, just render a Vec<_> instead of <For/> - each=counters + each=move || counters.get() // the key should be unique and stable for each row // using an index is usually a bad idea, unless your list // can only grow, because moving items around inside the list @@ -240,19 +252,24 @@ fn DynamicList( key=|counter| counter.0 // `children` receives each item from your `each` iterator // and returns a view - children=move |(id, (count, set_count))| { + children=move |(id, count)| { + // we can convert our ArcRwSignal to a Copy-able RwSignal + // for nicer DX when moving it into the view + let count = RwSignal::from(count); view! { <li> <button - on:click=move |_| set_count.update(|n| *n += 1) + on:click=move |_| *count.write() += 1 > {count} </button> <button on:click=move |_| { - set_counters.update(|counters| { - counters.retain(|(counter_id, _)| counter_id != &id) - }); + set_counters + .write() + .retain(|(counter_id, _)| { + counter_id != &id + }); } > "Remove" @@ -267,7 +284,7 @@ fn DynamicList( } fn main() { - leptos::mount_to_body(App) + leptos::mount::mount_to_body(App) } ``` diff --git a/src/view/04b_iteration.md b/src/view/04b_iteration.md index f65d87b..bb8b805 100644 --- a/src/view/04b_iteration.md +++ b/src/view/04b_iteration.md @@ -27,8 +27,8 @@ Let’s define a simple component that will iterate over the rows and display ea ```rust #[component] pub fn App() -> impl IntoView { - // start with a set of three rows - let (data, set_data) = create_signal(vec![ + // start with a set of three rows + let (data, set_data) = signal(vec![ DatabaseEntry { key: "foo".to_string(), value: 10, @@ -43,22 +43,22 @@ pub fn App() -> impl IntoView { }, ]); view! { - // when we click, update each row, - // doubling its value + // when we click, update each row, + // doubling its value <button on:click=move |_| { set_data.update(|data| { for row in data { row.value *= 2; } }); - // log the new value of the signal - logging::log!("{:?}", data.get()); + // log the new value of the signal + leptos::logging::log!("{:?}", data.get()); }> "Update Values" </button> - // iterate over the rows and display each value + // iterate over the rows and display each value <For - each=data + each=move || data.get() key=|state| state.key.clone() let:child > @@ -105,7 +105,7 @@ because the key didn’t change. So: why not just force the key to change? ```rust <For - each=data + each=move || data.get() key=|state| (state.key.clone(), state.value) let:child > @@ -153,38 +153,36 @@ and setters. ```rust #[component] pub fn App() -> impl IntoView { - // start with a set of three rows - let (data, set_data) = create_signal(vec![ + // start with a set of three rows + let (data, set_data) = signal(vec![ DatabaseEntry { key: "foo".to_string(), - value: create_rw_signal(10), + value: RwSignal::new(10), }, DatabaseEntry { key: "bar".to_string(), - value: create_rw_signal(20), + value: RwSignal::new(20), }, DatabaseEntry { key: "baz".to_string(), - value: create_rw_signal(15), + value: RwSignal::new(15), }, ]); view! { - // when we click, update each row, - // doubling its value + // when we click, update each row, + // doubling its value <button on:click=move |_| { - data.with(|data| { - for row in data { - row.value.update(|value| *value *= 2); - } - }); - // log the new value of the signal - logging::log!("{:?}", data.get()); + for row in &*data.read() { + row.value.update(|value| *value *= 2); + } + // log the new value of the signal + leptos::logging::log!("{:?}", data.get()); }> "Update Values" </button> - // iterate over the rows and display each value + // iterate over the rows and display each value <For - each=data + each=move || data.get() key=|state| state.key.clone() let:child > @@ -199,11 +197,11 @@ see that unlike in the previous version, in this version only the individual tex nodes are updated. Passing the signal directly into `{child.value}` works, as signals do keep their reactivity if you pass them into the view. -Note that I changed the `set_data.update()` to a `data.with()`. `.with()` is the +Note that I changed the `set_data.update()` to a `data.read()`. `.read()` is a non-cloning way of accessing a signal’s value. In this case, we are only updating -the internal values, not updating the list of values: because signals maintain their +the inner values, not updating the list of values: because signals maintain their own state, we don’t actually need to update the `data` signal at all, so the immutable -`.with()` is fine here. +`.read()` is fine here. > In fact, this version doesn’t update `data`, so the `<For/>` is essentially a static > list as in the last chapter, and this could just be a plain iterator. But the `<For/>` @@ -223,7 +221,7 @@ each field in a signal. ## Option 3: Memoized Slices -Leptos provides a primitive called [`create_memo`](https://docs.rs/leptos/latest/leptos/fn.create_memo.html), +Leptos provides a primitive called a [`Memo`](https://docs.rs/leptos/0.7.0-gamma3/leptos/reactive/computed/struct.Memo.html), which creates a derived computation that only triggers a reactive update when its value has changed. @@ -235,10 +233,10 @@ will be updated to this: ```rust <For - each=move || data().into_iter().enumerate() + each=move || data.get().into_iter().enumerate() key=|(_, state)| state.key.clone() children=move |(index, _)| { - let value = create_memo(move |_| { + let value = Memo::new(move |_| { data.with(|data| data.get(index).map(|d| d.value).unwrap_or(0)) }); view! { @@ -268,7 +266,7 @@ wrap the data in signals. It’s a bit more complex to set up this memo-per-row inside the `<For/>` loop rather than using nested signals. For example, you’ll notice that we have to guard against the possibility -that the `data[index]` would panic by using `data.get(index)`, because this memo may be +that the `data[index]` would panic by using `data.get(index)`, because this memo may be triggered to re-run once just after the row is removed. (This is because the memo for each row and the whole `<For/>` both depend on the same `data` signal, and the order of execution for multiple reactive values that depend on the same signal isn’t guaranteed.) @@ -276,3 +274,10 @@ multiple reactive values that depend on the same signal isn’t guaranteed.) Note also that while memos memoize their reactive changes, the same calculation does need to re-run to check the value every time, so nested reactive signals will still be more efficient for pinpoint updates here. + +## Option 4: Stores + +Leptos 0.7 introduces a new reactive primitive called “stores.” Stores are designed to address +the issues described in this chapter so far. They’re a bit experimental, so they require an additional dependency called `reactive_stores` in your `Cargo.toml`. + +**TODO finish stores example for this chapter.** diff --git a/src/view/05_forms.md b/src/view/05_forms.md index 087a006..94d4618 100644 --- a/src/view/05_forms.md +++ b/src/view/05_forms.md @@ -23,16 +23,15 @@ There are two important things to remember: on an `<input type="checkbox">`.) ```rust -let (name, set_name) = create_signal("Controlled".to_string()); +let (name, set_name) = signal("Controlled".to_string()); view! { <input type="text" - on:input=move |ev| { - // event_target_value is a Leptos helper function - // it functions the same way as event.target.value - // in JavaScript, but smooths out some of the typecasting - // necessary to make this work in Rust - set_name(event_target_value(&ev)); + // adding :target gives us typed access to the element + // that is the target of the event that fires + on:input:target=move |ev| { + // .value() returns the current value of an HTML input element + set_name.set(ev.target().value()); } // the `prop:` syntax lets you update a DOM property, @@ -74,16 +73,16 @@ view! { In an "uncontrolled input," the browser controls the state of the input element. Rather than continuously updating a signal to hold its value, we use a -[`NodeRef`](https://docs.rs/leptos/latest/leptos/struct.NodeRef.html) to access +[`NodeRef`](https://docs.rs/leptos/0.7.0-gamma3/leptos/tachys/reactive_graph/node_ref/struct.NodeRef.html) to access the input when we want to get its value. In this example, we only notify the framework when the `<form>` fires a `submit` event. -Note the use of the [`leptos::html`](https://docs.rs/leptos/latest/leptos/html/index.html#) module, which provides a bunch of types for every HTML element. +Note the use of the [`leptos::html`](https://docs.rs/leptos/0.7.0-gamma3/leptos/html/index.html) module, which provides a bunch of types for every HTML element. ```rust -let (name, set_name) = create_signal("Uncontrolled".to_string()); +let (name, set_name) = signal("Uncontrolled".to_string()); -let input_element: NodeRef<html::Input> = create_node_ref(); +let input_element: NodeRef<html::Input> = NodeRef::new(); view! { <form on:submit=on_submit> // on_submit defined below @@ -109,12 +108,13 @@ The view should be pretty self-explanatory by now. Note two things: underlying DOM node. Its value will be set when the element is rendered. ```rust -let on_submit = move |ev: leptos::ev::SubmitEvent| { +let on_submit = move |ev: SubmitEvent| { // stop the page from reloading! ev.prevent_default(); // here, we'll extract the value from the input - let value = input_element() + let value = input_element + .get() // event handlers can only fire after the view // is mounted to the DOM, so the `NodeRef` will be `Some` .expect("<input> should be mounted") @@ -160,7 +160,7 @@ support the `value` **property**...) view! { <textarea prop:value=move || some_value.get() - on:input=/* etc */ + on:input:target=move |ev| some_value.set(ev.target().value()) > /* plain-text initial value, does not change if the signal changes */ {some_value.get_untracked()} @@ -174,12 +174,11 @@ The `<select>` element can likewise be controlled via a `value` property on the which will select whichever `<option>` has that value. ```rust -let (value, set_value) = create_signal(0i32); +let (value, set_value) = signal(0i32); view! { <select - on:change=move |ev| { - let new_value = event_target_value(&ev); - set_value(new_value.parse().unwrap()); + on:change:target=move |ev| { + set_value(ev.target().value().parse().unwrap()); } prop:value=move || value.get().to_string() > @@ -202,14 +201,14 @@ view! { ```admonish sandbox title="Controlled vs uncontrolled forms CodeSandbox" collapsible=true -[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/5-forms-0-5-rf2t7c?file=%2Fsrc%2Fmain.rs%3A1%2C1) +[Click to open CodeSandbox.](https://codesandbox.io/p/devbox/5-forms-0-7-l5hktg?file=%2Fsrc%2Fmain.rs&workspaceId=478437f3-1f86-4b1e-b665-5c27a31451fb) <noscript> Please enable JavaScript to view examples. </noscript> <template> - <iframe src="https://codesandbox.io/p/sandbox/5-forms-0-5-rf2t7c?file=%2Fsrc%2Fmain.rs%3A1%2C1" width="100%" height="1000px" style="max-height: 100vh"></iframe> + <iframe src="https://codesandbox.io/p/devbox/5-forms-0-7-l5hktg?file=%2Fsrc%2Fmain.rs&workspaceId=478437f3-1f86-4b1e-b665-5c27a31451fb" width="100%" height="1000px" style="max-height: 100vh"></iframe> </template> ``` @@ -218,7 +217,8 @@ view! { <summary>CodeSandbox Source</summary> ```rust -use leptos::{ev::SubmitEvent, *}; +use leptos::{ev::SubmitEvent}; +use leptos::prelude::*; #[component] fn App() -> impl IntoView { @@ -233,17 +233,15 @@ fn App() -> impl IntoView { #[component] fn ControlledComponent() -> impl IntoView { // create a signal to hold the value - let (name, set_name) = create_signal("Controlled".to_string()); + let (name, set_name) = signal("Controlled".to_string()); view! { <input type="text" // fire an event whenever the input changes - on:input=move |ev| { - // event_target_value is a Leptos helper function - // it functions the same way as event.target.value - // in JavaScript, but smooths out some of the typecasting - // necessary to make this work in Rust - set_name(event_target_value(&ev)); + // adding :target after the event gives us access to + // a correctly-typed element at ev.target() + on:input:target=move |ev| { + set_name.set(ev.target().value()); } // the `prop:` syntax lets you update a DOM property, @@ -269,11 +267,11 @@ fn UncontrolledComponent() -> impl IntoView { // import the type for <input> use leptos::html::Input; - let (name, set_name) = create_signal("Uncontrolled".to_string()); + let (name, set_name) = signal("Uncontrolled".to_string()); // we'll use a NodeRef to store a reference to the input element // this will be filled when the element is created - let input_element: NodeRef<Input> = create_node_ref(); + let input_element: NodeRef<Input> = NodeRef::new(); // fires when the form `submit` event happens // this will store the value of the <input> in our signal @@ -282,7 +280,7 @@ fn UncontrolledComponent() -> impl IntoView { ev.prevent_default(); // here, we'll extract the value from the input - let value = input_element() + let value = input_element.get() // event handlers can only fire after the view // is mounted to the DOM, so the `NodeRef` will be `Some` .expect("<input> to exist") @@ -290,7 +288,7 @@ fn UncontrolledComponent() -> impl IntoView { // this means we can call`HtmlInputElement::value()` // to get the current value of the input .value(); - set_name(value); + set_name.set(value); }; view! { @@ -315,7 +313,7 @@ fn UncontrolledComponent() -> impl IntoView { // Because we defined it as `fn App`, we can now use it in a // template as <App/> fn main() { - leptos::mount_to_body(App) + leptos::mount::mount_to_body(App) } ``` diff --git a/src/view/06_control_flow.md b/src/view/06_control_flow.md index 45ab480..eb7139d 100644 --- a/src/view/06_control_flow.md +++ b/src/view/06_control_flow.md @@ -38,14 +38,10 @@ special knowledge. For example, let’s start with a simple signal and derived signal: ```rust -let (value, set_value) = create_signal(0); -let is_odd = move || value() & 1 == 1; +let (value, set_value) = signal(0); +let is_odd = move || value.get() % 2 == 0; ``` -> If you don’t recognize what’s going on with `is_odd`, don’t worry about it -> too much. It’s just a simple way to test whether an integer is odd by doing a -> bitwise `AND` with `1`. - We can use these signals and ordinary Rust to build most control flow. ### `if` statements @@ -56,11 +52,11 @@ if it’s even. Well, how about this? ```rust view! { <p> - {move || if is_odd() { - "Odd" - } else { - "Even" - }} + {move || if is_odd() { + "Odd" + } else { + "Even" + }} </p> } ``` @@ -105,7 +101,7 @@ pattern matching at your disposal. ```rust let message = move || { - match value() { + match value.get() { 0 => "Zero", 1 => "One", n if is_odd() => "Odd", @@ -131,9 +127,9 @@ above, where the value switches from even to odd on every change, this is fine. But consider the following example: ```rust -let (value, set_value) = create_signal(0); +let (value, set_value) = signal(0); -let message = move || if value() > 5 { +let message = move || if value.get() > 5 { "Big" } else { "Small" @@ -147,7 +143,7 @@ view! { This _works_, for sure. But if you added a log, you might be surprised ```rust -let message = move || if value() > 5 { +let message = move || if value.get() > 5 { logging::log!("{}: rendering Big", value()); "Big" } else { @@ -176,7 +172,7 @@ the `if` statement and rerendering isn’t a big deal. But imagine it were like this: ```rust -let message = move || if value() > 5 { +let message = move || if value.get() > 5 { <Big/> } else { <Small/> @@ -189,16 +185,16 @@ unnecessary work. ### `<Show/>` -The [`<Show/>`](https://docs.rs/leptos/latest/leptos/fn.Show.html) component is +The [`<Show/>`](https://docs.rs/leptos/0.7.0-gamma3/leptos/control_flow/fn.Show.html) component is the answer. You pass it a `when` condition function, a `fallback` to be shown if the `when` function returns `false`, and children to be rendered if `when` is `true`. ```rust -let (value, set_value) = create_signal(0); +let (value, set_value) = signal(0); view! { <Show - when=move || { value() > 5 } + when=move || { value.get() > 5 } fallback=|| view! { <Small/> } > <Big/> @@ -221,46 +217,35 @@ efficient. But if it’s at all expensive to render either branch, reach for There‘s one final thing it’s important to say in this section. -The `view` macro doesn’t return the most-generic wrapping type -[`View`](https://docs.rs/leptos/latest/leptos/enum.View.html). -Instead, it returns things with types like `Fragment` or `HtmlElement<Input>`. This -can be a little annoying if you’re returning different HTML elements from -different branches of a conditional: +Leptos uses a statically-typed view tree. The `view` macro returns different types +for different kinds of view. + +This won’t compile, because the different HTML elements are different types. ```rust,compile_error view! { <main> {move || match is_odd() { - true if value() == 1 => { - // returns HtmlElement<Pre> + true if value.get() == 1 => { view! { <pre>"One"</pre> } }, - false if value() == 2 => { - // returns HtmlElement<P> + false if value.get() == 2 => { view! { <p>"Two"</p> } } // returns HtmlElement<Textarea> - _ => view! { <textarea>{value()}</textarea> } + _ => view! { <textarea>{value.get()}</textarea> } }} </main> } ``` -This strong typing is actually very powerful, because -[`HtmlElement`](https://docs.rs/leptos/0.1.3/leptos/struct.HtmlElement.html) is, -among other things, a smart pointer: each `HtmlElement<T>` type implements -`Deref` for the appropriate underlying `web_sys` type. In other words, in the browser -your `view` returns real DOM elements, and you can access native DOM methods on -them. - +This strong typing is very powerful, because it enables all sorts of compile-time optimizations. But it can be a little annoying in conditional logic like this, because you can’t return different types from different branches of a condition in Rust. There are two ways to get yourself out of this situation: -1. If you have multiple `HtmlElement` types, convert them to `HtmlElement<AnyElement>` - with [`.into_any()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.into_any) -2. If you have a variety of view types that are not all `HtmlElement`, convert them to - `View`s with [`.into_view()`](https://docs.rs/leptos/latest/leptos/trait.IntoView.html#tymethod.into_view). +1. Use the enum `Either` (and `EitherOf3`, `EitherOf4`, etc.) to convert the different types to the same type. +2. Use `.into_any()` to convert multiple types into one typed-erased `AnyView`. Here’s the same example, with the conversion added: @@ -285,14 +270,14 @@ view! { ```admonish sandbox title="Live example" collapsible=true -[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/6-control-flow-0-5-4yn7qz?file=%2Fsrc%2Fmain.rs%3A1%2C1) +[Click to open CodeSandbox.](https://codesandbox.io/p/devbox/6-control-flow-0-7-3m4c9j?file=%2Fsrc%2Fmain.rs%3A1%2C1-91%2C2&workspaceId=478437f3-1f86-4b1e-b665-5c27a31451fb) <noscript> Please enable JavaScript to view examples. </noscript> <template> - <iframe src="https://codesandbox.io/p/sandbox/6-control-flow-0-5-4yn7qz?file=%2Fsrc%2Fmain.rs%3A1%2C1" width="100%" height="1000px" style="max-height: 100vh"></iframe> + <iframe src="https://codesandbox.io/p/devbox/6-control-flow-0-7-3m4c9j?file=%2Fsrc%2Fmain.rs%3A1%2C1-91%2C2&workspaceId=478437f3-1f86-4b1e-b665-5c27a31451fb" width="100%" height="1000px" style="max-height: 100vh"></iframe> </template> ``` @@ -301,19 +286,23 @@ view! { <summary>CodeSandbox Source</summary> ```rust -use leptos::*; +use leptos::prelude::*; #[component] fn App() -> impl IntoView { - let (value, set_value) = create_signal(0); - let is_odd = move || value() & 1 == 1; - let odd_text = move || if is_odd() { Some("How odd!") } else { None }; + let (value, set_value) = signal(0); + let is_odd = move || value.get() & 1 == 1; + let odd_text = move || if is_odd() { + Some("How odd!") + } else { + None + }; view! { <h1>"Control Flow"</h1> // Simple UI to update and show a value - <button on:click=move |_| set_value.update(|n| *n += 1)> + <button on:click=move |_| *set_value.write() += 1> "+1" </button> <p>"Value is: " {value}</p> @@ -371,22 +360,22 @@ fn App() -> impl IntoView { // `.into_any()` (for different HTML element types) // or `.into_view()` (for all view types) {move || match is_odd() { - true if value() == 1 => { + true if value.get() == 1 => { // <pre> returns HtmlElement<Pre> view! { <pre>"One"</pre> }.into_any() }, - false if value() == 2 => { + false if value.get() == 2 => { // <p> returns HtmlElement<P> // so we convert into a more generic type view! { <p>"Two"</p> }.into_any() } - _ => view! { <textarea>{value()}</textarea> }.into_any() + _ => view! { <textarea>{value.get()}</textarea> }.into_any() }} } } fn main() { - leptos::mount_to_body(App) + leptos::mount::mount_to_body(App) } ``` diff --git a/src/view/07_errors.md b/src/view/07_errors.md index 0b3d277..5cc7a46 100644 --- a/src/view/07_errors.md +++ b/src/view/07_errors.md @@ -11,15 +11,15 @@ Let’s start with a simple component to capture a number input. ```rust #[component] fn NumericInput() -> impl IntoView { - let (value, set_value) = create_signal(Ok(0)); - - // when input changes, try to parse a number from the input - let on_input = move |ev| set_value(event_target_value(&ev).parse::<i32>()); + let (value, set_value) = signal(Ok(0)); view! { <label> "Type an integer (or not!)" - <input type="number" on:input=on_input/> + <input type="number" on:input:target=move |ev| { + // when input changes, try to parse a number from the input + set_value.set(ev.target().value().parse::<i32>()) + }/> <p> "You entered " <strong>{value}</strong> @@ -46,11 +46,11 @@ You entered This is not great. It saves us using `.unwrap_or_default()` or something, but it would be much nicer if we could catch the error and do something with it. -You can do that, with the [`<ErrorBoundary/>`](https://docs.rs/leptos/latest/leptos/fn.ErrorBoundary.html) +You can do that, with the [`<ErrorBoundary/>`](https://docs.rs/leptos/0.7.0-gamma3/leptos/error/fn.ErrorBoundary.html) component. ```admonish note -People often try to point out that `<input type="number">` prevents you from typing a string +People often try to point out that `<input type="number">` prevents you from typing a string like `foo`, or anything else that's not a number. This is true in some browsers, but not in all! Moreover, there are a variety of things that can be typed into a plain number input that are not an `i32`: a floating-point number, a larger-than-32-bit number, the letter `e`, and so on. The browser @@ -70,32 +70,45 @@ Let’s add an `<ErrorBoundary/>` to this example. ```rust #[component] fn NumericInput() -> impl IntoView { - let (value, set_value) = create_signal(Ok(0)); - - let on_input = move |ev| set_value(event_target_value(&ev).parse::<i32>()); + let (value, set_value) = signal(Ok(0)); view! { <h1>"Error Handling"</h1> <label> "Type a number (or something that's not a number!)" - <input type="number" on:input=on_input/> + <input type="number" on:input:target=move |ev| { + // when input changes, try to parse a number from the input + set_value.set(ev.target().value().parse::<i32>()) + }/> + // If an `Err(_) had been rendered inside the <ErrorBoundary/>, + // the fallback will be displayed. Otherwise, the children of the + // <ErrorBoundary/> will be displayed. <ErrorBoundary // the fallback receives a signal containing current errors fallback=|errors| view! { <div class="error"> <p>"Not a number! Errors: "</p> - // we can render a list of errors as strings, if we'd like + // we can render a list of errors + // as strings, if we'd like <ul> {move || errors.get() .into_iter() .map(|(_, e)| view! { <li>{e.to_string()}</li>}) - .collect_view() + .collect::<Vec<_>>() } </ul> </div> } > - <p>"You entered " <strong>{value}</strong></p> + <p> + "You entered " + // because `value` is `Result<i32, _>`, + // it will render the `i32` if it is `Ok`, + // and render nothing and trigger the error boundary + // if it is `Err`. It's a signal, so this will dynamically + // update when `value` changes + <strong>{value}</strong> + </p> </ErrorBoundary> </label> } @@ -121,14 +134,14 @@ an `<ErrorBoundary/>` will appear again. ```admonish sandbox title="Live example" collapsible=true -[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/7-errors-0-5-5mptv9?file=%2Fsrc%2Fmain.rs%3A1%2C1) +[Click to open CodeSandbox.](https://codesandbox.io/p/devbox/7-errors-0-7-qqywqz?file=%2Fsrc%2Fmain.rs%3A5%2C1-46%2C6&workspaceId=478437f3-1f86-4b1e-b665-5c27a31451fb) <noscript> Please enable JavaScript to view examples. </noscript> <template> - <iframe src="https://codesandbox.io/p/sandbox/7-errors-0-5-5mptv9?file=%2Fsrc%2Fmain.rs%3A1%2C1" width="100%" height="1000px" style="max-height: 100vh"></iframe> + <iframe src="https://codesandbox.io/p/devbox/7-errors-0-7-qqywqz?file=%2Fsrc%2Fmain.rs%3A5%2C1-46%2C6&workspaceId=478437f3-1f86-4b1e-b665-5c27a31451fb" width="100%" height="1000px" style="max-height: 100vh"></iframe> </template> ``` @@ -136,20 +149,20 @@ an `<ErrorBoundary/>` will appear again. <summary>CodeSandbox Source</summary> ```rust -use leptos::*; +use leptos::prelude::*; #[component] fn App() -> impl IntoView { - let (value, set_value) = create_signal(Ok(0)); - - // when input changes, try to parse a number from the input - let on_input = move |ev| set_value(event_target_value(&ev).parse::<i32>()); + let (value, set_value) = signal(Ok(0)); view! { <h1>"Error Handling"</h1> <label> "Type a number (or something that's not a number!)" - <input type="number" on:input=on_input/> + <input type="number" on:input:target=move |ev| { + // when input changes, try to parse a number from the input + set_value.set(ev.target().value().parse::<i32>()) + }/> // If an `Err(_) had been rendered inside the <ErrorBoundary/>, // the fallback will be displayed. Otherwise, the children of the // <ErrorBoundary/> will be displayed. @@ -185,7 +198,7 @@ fn App() -> impl IntoView { } fn main() { - leptos::mount_to_body(App) + leptos::mount::mount_to_body(App) } ``` diff --git a/src/view/08_parent_child.md b/src/view/08_parent_child.md index f0a4fae..4654e94 100644 --- a/src/view/08_parent_child.md +++ b/src/view/08_parent_child.md @@ -13,16 +13,16 @@ the two? It’s easy to communicate state from a parent component to a child component. We covered some of this in the material on [components and props](./03_components.md). Basically if you want the parent to communicate to the child, you can pass a -[`ReadSignal`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html), a -[`Signal`](https://docs.rs/leptos/latest/leptos/struct.Signal.html), or even a -[`MaybeSignal`](https://docs.rs/leptos/latest/leptos/enum.MaybeSignal.html) as a prop. +[`ReadSignal`](https://docs.rs/leptos/0.7.0-gamma3/leptos/reactive/signal/struct.ReadSignal.html), a +[`Signal`](https://docs.rs/leptos/0.7.0-gamma3/leptos/reactive/wrappers/read/struct.Signal.html), or even a +[`MaybeSignal`](https://docs.rs/leptos/0.7.0-gamma3/leptos/reactive/wrappers/read/enum.MaybeSignal.html) as a prop. But what about the other direction? How can a child send notifications about events or state changes back up to the parent? There are four basic patterns of parent-child communication in Leptos. -## 1. Pass a [`WriteSignal`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html) +## 1. Pass a [`WriteSignal`](https://docs.rs/leptos/0.7.0-gamma3/leptos/reactive/signal/struct.WriteSignal.html) One approach is simply to pass a `WriteSignal` from the parent down to the child, and update it in the child. This lets you manipulate the state of the parent from the child. @@ -30,7 +30,7 @@ it in the child. This lets you manipulate the state of the parent from the child ```rust #[component] pub fn App() -> impl IntoView { - let (toggled, set_toggled) = create_signal(false); + let (toggled, set_toggled) = signal(false); view! { <p>"Toggled? " {toggled}</p> <ButtonA setter=set_toggled/> @@ -63,17 +63,15 @@ Another approach would be to pass a callback to the child: say, `on_click`. ```rust #[component] pub fn App() -> impl IntoView { - let (toggled, set_toggled) = create_signal(false); + let (toggled, set_toggled) = signal(false); view! { <p>"Toggled? " {toggled}</p> <ButtonB on_click=move |_| set_toggled.update(|value| *value = !*value)/> } } - #[component] -pub fn ButtonB(#[prop(into)] on_click: Callback<MouseEvent>) -> impl IntoView -{ +pub fn ButtonB(on_click: impl FnMut(MouseEvent) + 'static) -> impl IntoView { view! { <button on:click=on_click> "Toggle" @@ -88,48 +86,6 @@ of keeping local state local, preventing the problem of spaghetti mutation. But the logic to mutate that signal needs to exist up in `<App/>`, not down in `<ButtonB/>`. These are real trade-offs, not a simple right-or-wrong choice. -> Note the way we use the `Callback<In, Out>` type. This is basically a -> wrapper around a closure `Fn(In) -> Out` that is also `Copy` and makes it -> easy to pass around. -> -> We also used the `#[prop(into)]` attribute so we can pass a normal closure into -> `on_click`. Please see the [chapter "`into` Props"](./03_components.md#into-props) for more details. - -### 2.1 Use Closure instead of `Callback` - -You can use a Rust closure `Fn(MouseEvent)` directly instead of `Callback`: - -```rust -#[component] -pub fn App() -> impl IntoView { - let (toggled, set_toggled) = create_signal(false); - view! { - <p>"Toggled? " {toggled}</p> - <ButtonB on_click=move |_| set_toggled.update(|value| *value = !*value)/> - } -} - - -#[component] -pub fn ButtonB<F>(on_click: F) -> impl IntoView -where - F: Fn(MouseEvent) + 'static -{ - view! { - <button on:click=on_click> - "Toggle" - </button> - } -} -``` - -The code is very similar in this case. On more advanced use-cases using a -closure might require some cloning compared to using a `Callback`. - -> Note the way we declare the generic type `F` here for the callback. If you’re -> confused, look back at the [generic props](./03_components.html#generic-props) section -> of the chapter on components. - ## 3. Use an Event Listener You can actually write Option 2 in a slightly different way. If the callback maps directly onto @@ -139,7 +95,7 @@ in your `view` macro in `<App/>`. ```rust #[component] pub fn App() -> impl IntoView { - let (toggled, set_toggled) = create_signal(false); + let (toggled, set_toggled) = signal(false); view! { <p>"Toggled? " {toggled}</p> // note the on:click instead of on_click @@ -148,7 +104,6 @@ pub fn App() -> impl IntoView { } } - #[component] pub fn ButtonC() -> impl IntoView { view! { @@ -175,7 +130,7 @@ tree: ```rust #[component] pub fn App() -> impl IntoView { - let (toggled, set_toggled) = create_signal(false); + let (toggled, set_toggled) = signal(false); view! { <p>"Toggled? " {toggled}</p> <Layout/> @@ -204,9 +159,10 @@ pub fn Content() -> impl IntoView { } #[component] -pub fn ButtonD<F>() -> impl IntoView { +pub fn ButtonD() -> impl IntoView { todo!() } + ``` Now `<ButtonD/>` is no longer a direct child of `<App/>`, so you can’t simply @@ -216,7 +172,7 @@ pass your `WriteSignal` to its props. You could do what’s sometimes called ```rust #[component] pub fn App() -> impl IntoView { - let (toggled, set_toggled) = create_signal(false); + let (toggled, set_toggled) = signal(false); view! { <p>"Toggled? " {toggled}</p> <Layout set_toggled/> @@ -245,7 +201,7 @@ pub fn Content(set_toggled: WriteSignal<bool>) -> impl IntoView { } #[component] -pub fn ButtonD<F>(set_toggled: WriteSignal<bool>) -> impl IntoView { +pub fn ButtonD(set_toggled: WriteSignal<bool>) -> impl IntoView { todo!() } ``` @@ -262,8 +218,8 @@ There is! ### 4.1 The Context API -You can provide data that skips levels by using [`provide_context`](https://docs.rs/leptos/latest/leptos/fn.provide_context.html) -and [`use_context`](https://docs.rs/leptos/latest/leptos/fn.use_context.html). Contexts are identified +You can provide data that skips levels by using [`provide_context`](https://docs.rs/leptos/0.7.0-gamma3/leptos/context/fn.provide_context.html) +and [`use_context`](https://docs.rs/leptos/0.7.0-gamma3/leptos/context/fn.use_context.html). Contexts are identified by the type of the data you provide (in this example, `WriteSignal<bool>`), and they exist in a top-down tree that follows the contours of your UI tree. In this example, we can use context to skip the unnecessary prop drilling. @@ -271,7 +227,7 @@ unnecessary prop drilling. ```rust #[component] pub fn App() -> impl IntoView { - let (toggled, set_toggled) = create_signal(false); + let (toggled, set_toggled) = signal(false); // share `set_toggled` with all children of this component provide_context(set_toggled); @@ -283,15 +239,14 @@ pub fn App() -> impl IntoView { } // <Layout/> and <Content/> omitted -// To work in this version, drop their references to set_toggled +// To work in this version, drop the `set_toggled` parameter on each #[component] pub fn ButtonD() -> impl IntoView { // use_context searches up the context tree, hoping to // find a `WriteSignal<bool>` // in this case, I .expect() because I know I provided it - let setter = use_context::<WriteSignal<bool>>() - .expect("to have found the setter provided"); + let setter = use_context::<WriteSignal<bool>>().expect("to have found the setter provided"); view! { <button @@ -301,6 +256,7 @@ pub fn ButtonD() -> impl IntoView { </button> } } + ``` The same caveats apply to this as to `<ButtonA/>`: passing a `WriteSignal` @@ -321,14 +277,14 @@ signals and effects, all the way down. ```admonish sandbox title="Live example" collapsible=true -[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/8-parent-child-0-5-7rz7qd?file=%2Fsrc%2Fmain.rs%3A1%2C2) +[Click to open CodeSandbox.](https://codesandbox.io/p/devbox/8-parent-child-0-7-cgcgk9?file=%2Fsrc%2Fmain.rs%3A1%2C1-116%2C2&workspaceId=478437f3-1f86-4b1e-b665-5c27a31451fb) <noscript> Please enable JavaScript to view examples. </noscript> <template> - <iframe src="https://codesandbox.io/p/sandbox/8-parent-child-0-5-7rz7qd?file=%2Fsrc%2Fmain.rs%3A1%2C2" width="100%" height="1000px" style="max-height: 100vh"></iframe> + <iframe src="https://codesandbox.io/p/devbox/8-parent-child-0-7-cgcgk9?file=%2Fsrc%2Fmain.rs%3A1%2C1-116%2C2&workspaceId=478437f3-1f86-4b1e-b665-5c27a31451fb" width="100%" height="1000px" style="max-height: 100vh"></iframe> </template> ``` @@ -337,7 +293,7 @@ signals and effects, all the way down. <summary>CodeSandbox Source</summary> ```rust -use leptos::{ev::MouseEvent, *}; +use leptos::{ev::MouseEvent, prelude::*}; // This highlights four different ways that child components can communicate // with their parent: @@ -354,10 +310,10 @@ struct SmallcapsContext(WriteSignal<bool>); #[component] pub fn App() -> impl IntoView { // just some signals to toggle three classes on our <p> - let (red, set_red) = create_signal(false); - let (right, set_right) = create_signal(false); - let (italics, set_italics) = create_signal(false); - let (smallcaps, set_smallcaps) = create_signal(false); + let (red, set_red) = signal(false); + let (right, set_right) = signal(false); + let (italics, set_italics) = signal(false); + let (smallcaps, set_smallcaps) = signal(false); // the newtype pattern isn't *necessary* here but is a good practice // it avoids confusion with other possible future `WriteSignal<bool>` contexts @@ -410,12 +366,10 @@ pub fn ButtonA( /// Button B receives a closure #[component] -pub fn ButtonB<F>( +pub fn ButtonB( /// Callback that will be invoked when the button is clicked. - on_click: F, + on_click: impl FnMut(MouseEvent) + 'static, ) -> impl IntoView -where - F: Fn(MouseEvent) + 'static, { view! { <button @@ -424,18 +378,6 @@ where "Toggle Right" </button> } - - // just a note: in an ordinary function ButtonB could take on_click: impl Fn(MouseEvent) + 'static - // and save you from typing out the generic - // the component macro actually expands to define a - // - // struct ButtonBProps<F> where F: Fn(MouseEvent) + 'static { - // on_click: F - // } - // - // this is what allows us to have named props in our component invocation, - // instead of an ordered list of function arguments - // if Rust ever had named function arguments we could drop this requirement } /// Button C is a dummy: it renders a button but doesn't handle @@ -465,7 +407,7 @@ pub fn ButtonD() -> impl IntoView { } fn main() { - leptos::mount_to_body(App) + leptos::mount::mount_to_body(App) } ``` diff --git a/src/view/09_component_children.md b/src/view/09_component_children.md index 5770207..5ac1662 100644 --- a/src/view/09_component_children.md +++ b/src/view/09_component_children.md @@ -31,7 +31,7 @@ In fact, you’ve already seen these both in action in the [`<Show/>`](/view/06_ view! { <Show // `when` is a normal prop - when=move || value() > 5 + when=move || value.get() > 5 // `fallback` is a "render prop": a function that returns a view fallback=|| view! { <Small/> } > @@ -45,12 +45,14 @@ view! { Let’s define a component that takes some children and a render prop. ```rust +/// Displays a `render_prop` and some children within markup. #[component] pub fn TakesChildren<F, IV>( /// Takes a function (type F) that returns anything that can be /// converted into a View (type IV) render_prop: F, - /// `children` takes the `Children` type + /// `children` can take one of several different types, each of which + /// is a function that returns some view type children: Children, ) -> impl IntoView where @@ -58,9 +60,10 @@ where IV: IntoView, { view! { + <h1><code>"<TakesChildren/>"</code></h1> <h2>"Render Prop"</h2> {render_prop()} - + <hr/> <h2>"Children"</h2> {children()} } @@ -69,7 +72,10 @@ where `render_prop` and `children` are both functions, so we can call them to generate the appropriate views. `children`, in particular, is an alias for -`Box<dyn FnOnce() -> Fragment>`. (Aren't you glad we named it `Children` instead?) +`Box<dyn FnOnce() -> AnyView>`. (Aren't you glad we named it `Children` instead?) +The `AnyView` returned here is an opaque, type-erased view: you can’t do anything to +inspect it. There are a variety of other child types: for example, `ChildrenFragment` +will return a `Fragment`, which is a collection whose children can be iterated over. > If you need a `Fn` or `FnMut` here because you need to call `children` more than once, > we also provide `ChildrenFn` and `ChildrenMut` aliases. @@ -88,23 +94,29 @@ view! { ## Manipulating Children -The [`Fragment`](https://docs.rs/leptos/latest/leptos/struct.Fragment.html) type is -basically a way of wrapping a `Vec<View>`. You can insert it anywhere into your view. +The [`Fragment`](https://docs.rs/leptos/0.7.0-gamma3/leptos/tachys/view/fragment/struct.Fragment.html) type is +basically a way of wrapping a `Vec<AnyView>`. You can insert it anywhere into your view. But you can also access those inner views directly to manipulate them. For example, here’s a component that takes its children and turns them into an unordered list. ```rust +/// Wraps each child in an `<li>` and embeds them in a `<ul>`. #[component] -pub fn WrapsChildren(children: Children) -> impl IntoView { - // Fragment has `nodes` field that contains a Vec<View> +pub fn WrapsChildren(children: ChildrenFragment) -> impl IntoView { + // children() returns a `Fragment`, which has a + // `nodes` field that contains a Vec<View> + // this means we can iterate over the children + // to create something new! let children = children() .nodes .into_iter() .map(|child| view! { <li>{child}</li> }) - .collect_view(); + .collect::<Vec<_>>(); view! { + <h1><code>"<WrapsChildren/>"</code></h1> + // wrap our wrapped children in a UL <ul>{children}</ul> } } @@ -124,14 +136,14 @@ view! { ```admonish sandbox title="Live example" collapsible=true -[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/9-component-children-0-5-m4jwhp?file=%2Fsrc%2Fmain.rs%3A1%2C1) +[Click to open CodeSandbox.](https://codesandbox.io/p/devbox/9-component-children-0-7-736s9r?file=%2Fsrc%2Fmain.rs%3A1%2C1-90%2C2&workspaceId=478437f3-1f86-4b1e-b665-5c27a31451fb) <noscript> Please enable JavaScript to view examples. </noscript> <template> - <iframe src="https://codesandbox.io/p/sandbox/9-component-children-0-5-m4jwhp?file=%2Fsrc%2Fmain.rs%3A1%2C1" width="100%" height="1000px" style="max-height: 100vh"></iframe> + <iframe src="https://codesandbox.io/p/devbox/9-component-children-0-7-736s9r?file=%2Fsrc%2Fmain.rs%3A1%2C1-90%2C2&workspaceId=478437f3-1f86-4b1e-b665-5c27a31451fb" width="100%" height="1000px" style="max-height: 100vh"></iframe> </template> ``` @@ -140,7 +152,7 @@ view! { <summary>CodeSandbox Source</summary> ```rust -use leptos::*; +use leptos::prelude::*; // Often, you want to pass some kind of child view to another // component. There are two basic patterns for doing this: @@ -152,12 +164,9 @@ use leptos::*; #[component] pub fn App() -> impl IntoView { - let (items, set_items) = create_signal(vec![0, 1, 2]); + let (items, set_items) = signal(vec![0, 1, 2]); let render_prop = move || { - // items.with(...) reacts to the value without cloning - // by applying a function. Here, we pass the `len` method - // on a `Vec<_>` directly - let len = move || items.with(Vec::len); + let len = move || items.read().len(); view! { <p>"Length: " {len}</p> } @@ -212,7 +221,7 @@ where /// Wraps each child in an `<li>` and embeds them in a `<ul>`. #[component] -pub fn WrapsChildren(children: Children) -> impl IntoView { +pub fn WrapsChildren(children: ChildrenFragment) -> impl IntoView { // children() returns a `Fragment`, which has a // `nodes` field that contains a Vec<View> // this means we can iterate over the children @@ -231,7 +240,7 @@ pub fn WrapsChildren(children: Children) -> impl IntoView { } fn main() { - leptos::mount_to_body(App) + leptos::mount::mount_to_body(App) } ``` diff --git a/src/view/builder.md b/src/view/builder.md index 36b375f..19e5c3e 100644 --- a/src/view/builder.md +++ b/src/view/builder.md @@ -20,73 +20,57 @@ Elements are created by calling a function with the same name as the HTML elemen p() ``` -You can add children to the element with [`.child()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.child), which takes a single child or a tuple or array of types that implement [`IntoView`](https://docs.rs/leptos/latest/leptos/trait.IntoView.html). +You can add children to the element with [`.child()`](https://docs.rs/leptos/0.7.0-gamma3/leptos/html/trait.ElementChild.html#tymethod.child), which takes a single child or a tuple or array of types that implement [`IntoView`](https://docs.rs/leptos/0.7.0-gamma3/leptos/trait.IntoView.html). ```rust p().child((em().child("Big, "), strong().child("bold "), "text")) ``` -Attributes are added with [`.attr()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.attr). This can take any of the same types that you could pass as an attribute into the view macro (types that implement [`IntoAttribute`](https://docs.rs/leptos/latest/leptos/trait.IntoAttribute.html)). +Attributes are added with [`.attr()`](https://docs.rs/leptos/0.7.0-gamma3/leptos/attr/custom/trait.CustomAttribute.html#method.attr). This can take any of the same types that you could pass as an attribute into the view macro (types that implement [`Attribute`](https://docs.rs/leptos/0.7.0-gamma3/leptos/attr/trait.Attribute.html)). ```rust -p().attr("id", "foo").attr("data-count", move || count().to_string()) +p().attr("id", "foo") + .attr("data-count", move || count.get().to_string()) ``` -Similarly, the `class:`, `prop:`, and `style:` syntaxes map directly onto [`.class()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.class), [`.prop()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.prop), and [`.style()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.style) methods. +They can also be added with attribute methods, which are available for any built-in HTML attribute name: -Event listeners can be added with [`.on()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.on). Typed events found in [`leptos::ev`](https://docs.rs/leptos/latest/leptos/ev/index.html) prevent typos in event names and allow for correct type inference in the callback function. +```rust +p().id("foo") + .attr("data-count", move || count.get().to_string()) +``` + +Similarly, the `class:`, `prop:`, and `style:` syntaxes map directly onto [`.class()`](https://docs.rs/leptos/0.7.0-gamma3/leptos/attr/global/trait.ClassAttribute.html#tymethod.class), [`.prop()`](https://docs.rs/leptos/0.7.0-gamma3/leptos/attr/global/trait.PropAttribute.html#tymethod.prop), and [`.style()`](https://docs.rs/leptos/0.7.0-gamma3/leptos/attr/global/trait.StyleAttribute.html#tymethod.style) methods. + +Event listeners can be added with [`.on()`](https://docs.rs/leptos/0.7.0-gamma3/leptos/attr/global/trait.OnAttribute.html#tymethod.on). Typed events found in [`leptos::ev`](https://docs.rs/leptos/0.7.0-gamma3/leptos/tachys/html/event/index.html) prevent typos in event names and allow for correct type inference in the callback function. ```rust button() - .on(ev::click, move |_| set_count.update(|count| *count = 0)) + .on(ev::click, move |_| set_count.set(0)) .child("Clear") ``` -> Many additional methods can be found in the [`HtmlElement`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.child) docs, including some methods that are not directly available in the `view` macro. - All of this adds up to a very Rusty syntax to build full-featured views, if you prefer this style. ```rust /// 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(0); +pub fn counter(initial_value: i32, step: i32) -> impl IntoView { + let (count, set_count) = signal(initial_value); div().child(( button() // 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 = 0)) + .on(ev::click, move |_| set_count.set(0)) .child("Clear"), button() - .on(ev::click, move |_| set_count.update(|count| *count -= 1)) + .on(ev::click, move |_| *set_count.write() -= step) .child("-1"), span().child(("Value: ", move || count.get(), "!")), button() - .on(ev::click, move |_| set_count.update(|count| *count += 1)) + .on(ev::click, move |_| *set_count.write() += step) .child("+1"), )) } ``` - -This also has the benefit of being more flexible: because these are all plain Rust functions and methods, it’s easier to use them in things like iterator adapters without any additional “magic”: - -```rust -// take some set of attribute names and values -let attrs: Vec<(&str, AttributeValue)> = todo!(); -// you can use the builder syntax to “spread” these onto the -// element in a way that’s not possible with the view macro -let p = attrs - .into_iter() - .fold(p(), |el, (name, value)| el.attr(name, value)); - -``` - -> ## Performance Note -> -> One caveat: the `view` macro applies significant optimizations in server-side-rendering (SSR) mode to improve HTML rendering performance significantly (think 2-4x faster, depending on the characteristics of any given app). It does this by analyzing your `view` at compile time and converting the static parts into simple HTML strings, rather than expanding them into the builder syntax. -> -> This means two things: -> -> 1. The builder syntax and `view` macro should not be mixed, or should only be mixed very carefully: at least in SSR mode, the output of the `view` should be treated as a “black box” that can’t have additional builder methods applied to it without causing inconsistencies. -> 2. Using the builder syntax will result in less-than-optimal SSR performance. It won’t be slow, by any means (and it’s worth running your own benchmarks in any case), just slower than the `view`-optimized version. diff --git a/src/web_sys.md b/src/web_sys.md index fd9e8dd..c611b56 100644 --- a/src/web_sys.md +++ b/src/web_sys.md @@ -1,54 +1,53 @@ # Integrating with JavaScript: `wasm-bindgen`, `web_sys` and `HtmlElement` -Leptos provides a variety of tools to allow you to build declarative web applications without leaving the world -of the framework. Things like the reactive system, `component` and `view` macros, and router allow you to build -user interfaces without directly interacting with the Web APIs provided by the browser. And they let you do it -all directly in Rust, which is great—assuming you like Rust. (And if you’ve gotten this far in the book, we assume +Leptos provides a variety of tools to allow you to build declarative web applications without leaving the world +of the framework. Things like the reactive system, `component` and `view` macros, and router allow you to build +user interfaces without directly interacting with the Web APIs provided by the browser. And they let you do it +all directly in Rust, which is great—assuming you like Rust. (And if you’ve gotten this far in the book, we assume you like Rust.) -Ecosystem crates like the fantastic set of utilities provided by [`leptos-use`](https://leptos-use.rs/) can take you +Ecosystem crates like the fantastic set of utilities provided by [`leptos-use`](https://leptos-use.rs/) can take you even further, by providing Leptos-specific reactive wrappers around many Web APIs. Nevertheless, in many cases you will need to access JavaScript libraries or Web APIs directly. This chapter can help. -## Using JS Libraries with `wasm-bindgen` +## Using JS Libraries with `wasm-bindgen` -Your Rust code can be compiled to a WebAssembly (WASM) module and loaded to run in the browser. However, WASM does not -have direct access to browser APIs. Instead, the Rust/WASM ecosystem depends on generating bindings from your Rust code -to the JavaScript browser environment that hosts it. +Your Rust code can be compiled to a WebAssembly (WASM) module and loaded to run in the browser. However, WASM does not +have direct access to browser APIs. Instead, the Rust/WASM ecosystem depends on generating bindings from your Rust code +to the JavaScript browser environment that hosts it. -The [`wasm-bindgen`](https://rustwasm.github.io/docs/wasm-bindgen/) crate is at the center of that ecosystem. It provides -both an interface for marking parts of Rust code with annotations telling it how to call JS, and a CLI tool for generating -the necessary JS glue code. You’ve been using this without knowing it all along: both `trunk` and `cargo-leptos` rely on +The [`wasm-bindgen`](https://rustwasm.github.io/docs/wasm-bindgen/) crate is at the center of that ecosystem. It provides +both an interface for marking parts of Rust code with annotations telling it how to call JS, and a CLI tool for generating +the necessary JS glue code. You’ve been using this without knowing it all along: both `trunk` and `cargo-leptos` rely on `wasm-bindgen` under the hood. -If there is a JavaScript library that you want to call from Rust, you should refer to the `wasm-bindgen` docs on -[importing functions from JS](https://rustwasm.github.io/docs/wasm-bindgen/examples/import-js.html). It is relatively +If there is a JavaScript library that you want to call from Rust, you should refer to the `wasm-bindgen` docs on +[importing functions from JS](https://rustwasm.github.io/docs/wasm-bindgen/examples/import-js.html). It is relatively easy to import individual functions, classes, or values from JavaScript to use in your Rust app. -It is not always easy to integrate JS libraries into your app directly. In particular, any library that depends on a -particular JS framework like React may be hard to integrate. Libraries that manipulate DOM state in some way (for example, -rich text editors) should also be used with care: both Leptos and the JS library will probably assume that they are +It is not always easy to integrate JS libraries into your app directly. In particular, any library that depends on a +particular JS framework like React may be hard to integrate. Libraries that manipulate DOM state in some way (for example, +rich text editors) should also be used with care: both Leptos and the JS library will probably assume that they are the ultimate source of truth for the app’s state, so you should be careful to separate their responsibilities. ## Accessing Web APIs with `web-sys` -If you just need to access some browser APIs without pulling in a separate JS library, you can do so using the -[`web_sys`](https://docs.rs/web-sys/latest/web_sys/) crate. This provides bindings for all of the Web APIs provided by +If you just need to access some browser APIs without pulling in a separate JS library, you can do so using the +[`web_sys`](https://docs.rs/web-sys/latest/web_sys/) crate. This provides bindings for all of the Web APIs provided by the browser, with 1:1 mappings from browser types and functions to Rust structs and methods. -In general, if you’re asking “how do I *do X* with Leptos?” where *do X* is accessing some Web API, looking up a vanilla -JavaScript solution and translating it to Rust using the [`web-sys` docs](https://docs.rs/web-sys/latest/web_sys/) is a +In general, if you’re asking “how do I _do X_ with Leptos?” where _do X_ is accessing some Web API, looking up a vanilla +JavaScript solution and translating it to Rust using the [`web-sys` docs](https://docs.rs/web-sys/latest/web_sys/) is a good approach. - -> After this section, you might find -> [the `wasm-bindgen` guide chapter on `web-sys`](https://rustwasm.github.io/docs/wasm-bindgen/web-sys/index.html) +> After this section, you might find +> [the `wasm-bindgen` guide chapter on `web-sys`](https://rustwasm.github.io/docs/wasm-bindgen/web-sys/index.html) > useful for additional reading. ### Enabling features -`web_sys` is heavily feature-gated to keep compile times low. If you would like to use one of its many APIs, you may +`web_sys` is heavily feature-gated to keep compile times low. If you would like to use one of its many APIs, you may need to enable a feature to use it. The features required to use an item are always listed in its documentation. @@ -70,11 +69,13 @@ In order to use this, you need to add `RUSTFLAGS=--cfg=web_sys_unstable_apis` as This can either be done by adding it to every command, or add it to `.cargo/config.toml` in your repository. As part of a command: + ```sh RUSTFLAGS=--cfg=web_sys_unstable_apis cargo # ... ``` In `.cargo/config.toml`: + ```toml [env] RUSTFLAGS = "--cfg=web_sys_unstable_apis" @@ -82,126 +83,33 @@ RUSTFLAGS = "--cfg=web_sys_unstable_apis" ## Accessing raw `HtmlElement`s from your `view` -The declarative style of the framework means that you don’t need to directly manipulate DOM nodes to build up your user interface. -However, in some cases you want direct access to the underlying DOM element that represents part of your view. The section of the book -on [“uncontrolled inputs”](/view/05_forms.html?highlight=NodeRef#uncontrolled-inputs) showed how to do this using the -[`NodeRef`](https://docs.rs/leptos/latest/leptos/struct.NodeRef.html) type. - -You may notice that [`NodeRef::get`](https://docs.rs/leptos/latest/leptos/struct.NodeRef.html#method.get) returns an `Option<leptos::HtmlElement<T>>`. This is *not* the same type as a [`web_sys::HtmlElement`](https://docs.rs/web-sys/latest/web_sys/struct.HtmlElement.html), although they -are related. So what is this [`HtmlElement<T>`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html) type, and how do you use it? - -### Overview +The declarative style of the framework means that you don’t need to directly manipulate DOM nodes to build up your user interface. +However, in some cases you want direct access to the underlying DOM element that represents part of your view. The section of the book +on [“uncontrolled inputs”](/view/05_forms.html?highlight=NodeRef#uncontrolled-inputs) showed how to do this using the +[`NodeRef`](https://docs.rs/leptos/0.7.0-gamma3/leptos/tachys/reactive_graph/node_ref/struct.NodeRef.html) type. -`web_sys::HtmlElement` is the Rust equivalent of the browser’s [`HTMLElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement) -interface, which is implemented for all HTML elements. It provides access to a minimal set of functions and APIs that are guaranteed to be -available for any HTML element. Each particular HTML element then has its own element class, which implements additional functionality. -The goal of `leptos::HtmlElement<T>` is to bridge the gap between elements in your view and these more specific JavaScript types, so that you -can access the particular functionality of those elements. +`NodeRef::get` returns a correctly-typed +`web-sys` element that can be directly manipulated. -This is implemented by using the Rust `Deref` trait to allow you to dereference a `leptos::HtmlElement<T>` to the appropriately-typed JS object -for that particular element type `T`. - -### Definition - -Understanding this relationship involves understanding some related traits. - -The following simply defines what types are allowed inside the `T` of `leptos::HtmlElement<T>` and how it links to `web_sys`. +For example, consider the following: ```rust -pub struct HtmlElement<El> where El: ElementDescriptor { /* ... */ } - -pub trait ElementDescriptor: ElementDescriptorBounds { /* ... */ } - -pub trait ElementDescriptorBounds: Debug {} -impl<El> ElementDescriptorBounds for El where El: Debug {} - -// this is implemented for every single element in `leptos::{html, svg, math}::*` -impl ElementDescriptor for leptos::html::Div { /* ... */ } - -// same with this, derefs to the corresponding `web_sys::Html*Element` -impl Deref for leptos::html::Div { - type Target = web_sys::HtmlDivElement; - // ... +#[component] +pub fn App() -> impl IntoView { + let node_ref = NodeRef::<Input>::new(); + + Effect::new(move |_| { + if let Some(node) = node_ref.get() { + leptos::logging::log!("value = {}", node.value()); + } + }); + + view! { + <input node_ref=node_ref/> + } } ``` -The following is from `web_sys`: -```rust -impl Deref for web_sys::HtmlDivElement { - type Target = web_sys::HtmlElement; - // ... -} - -impl Deref for web_sys::HtmlElement { - type Target = web_sys::Element; - // ... -} - -impl Deref for web_sys::Element { - type Target = web_sys::Node; - // ... -} - -impl Deref for web_sys::Node { - type Target = web_sys::EventTarget; - // ... -} -``` - -`web_sys` uses long deref chains to emulate the inheritance used in JavaScript. -If you can't find the method you're looking for on one type, take a look further down the deref chain. -The `leptos::html::*` types all deref into `web_sys::Html*Element` or `web_sys::HtmlElement`. -By calling `element.method()`, Rust will automatically add more derefs as needed to call the correct method! - -However, some methods have the same name, such as [`leptos::HtmlElement::style`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.style) and [`web_sys::HtmlElement::style`](https://docs.rs/web-sys/latest/web_sys/struct.HtmlElement.html#method.style). -In these cases, Rust will pick the one that requires the least amount of derefs, which is `leptos::HtmlElement::style` if you're getting an element straight from a `NodeRef`. -If you wish to use the `web_sys` method instead, you can manually deref with `(*element).style()`. - -If you want to have even more control over which type you are calling a method from, `AsRef<T>` is implemented for all types that are part of the deref chain, so you can explicitly state which type you want. - -> See also: [The `wasm-bindgen` Guide: Inheritance in `web-sys`](https://rustwasm.github.io/wasm-bindgen/web-sys/inheritance.html). - -### Clones - -The `web_sys::HtmlElement` (and by extension the `leptos::HtmlElement` too) actually only store references to the HTML element it affects. -Therefore, calling `.clone()` doesn't actually make a new HTML element, it simply gets another reference to the same one. -Calling methods that change the element from any of its clones will affect the original element. - -Unfortunately, `web_sys::HtmlElement` does not implement `Copy`, so you may need to add a bunch of clones especially when using it in closures. -Don't worry though, these clones are cheap! - -### Casting - -You can get less specific types through `Deref` or `AsRef`, so use those when possible. -However, if you need to cast to a more specific type (e.g. from an `EventTarget` to a `HtmlInputElement`), you will need to use the methods provided by `wasm_bindgen::JsCast` (re-exported through `web_sys::wasm_bindgen::JsCast`). -You'll probably only need the [`dyn_ref`](https://docs.rs/wasm-bindgen/0.2.90/wasm_bindgen/trait.JsCast.html#method.dyn_ref) method. - -```rust -use web_sys::wasm_bindgen::JsCast; - -let on_click = |ev: MouseEvent| { - let target: HtmlInputElement = ev.current_target().unwrap().dyn_ref().unwrap(); - // or, just use the existing `leptos::event_target_*` functions -} -``` - -> See the [`event_target_*` functions here](https://docs.rs/leptos/latest/leptos/fn.event_target.html?search=event_target), if you're curious. - -### `leptos::HtmlElement` - -The [`leptos::HtmlElement`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html) adds some extra convenience methods to make it easier to manipulate common attributes. -These methods were built for the [builder syntax](./view/builder.md), so it takes and returns `self`. -You can just do `_ = element.clone().<method>()` to ignore the element it returns - it'll still affect the original element, even though it doesn't look like it (see previous section on [Clones](#clones))! - -Here are some of the common methods you may want to use, for example in event listeners or `use:` directives. -- [`id`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.id): *overwrites* the id on the element. -- [`classes`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.classes): *adds* the classes to the element. - You can specify multiple classes with a space-separated string. - You can also use [`class`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.class) to conditionally add a *single* class: do not add multiple with this method. -- [`attr`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.attr): sets a `key=value` attribute to the element. -- [`prop`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.prop): sets a *property* on the element: see the distinction between [properties and attributes here](./view/05_forms.md#why-do-you-need-propvalue). -- [`on`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.on): adds an event listener to the element. - Specify the event type through one of [`leptos::ev::*`](https://docs.rs/leptos/latest/leptos/ev/index.html) (it's the ones in all lowercase). -- [`child`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.child): adds an element as the last child of the element. +Inside the effect here, `node` is simply a `web_sys::HtmlInputElement`. This allows us to call any appropriate methods. -Take a look at the rest of the [`leptos::HtmlElement`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html) methods too. If none of them fit your requirements, also take a look at [`leptos-use`](https://leptos-use.rs/). Otherwise, you’ll have to use the `web_sys` APIs. +(Note that `.get()` returns an `Option` here, because the `NodeRef` is empty until it is filled when the DOM elements are actually created. Effects run a tick after the component runs, so in most cases the `<input>` will already have been created by the time the effect runs.)