diff --git a/docs/book/README.md b/docs/book/README.md index 5ce9a90f1f..ced5b4931f 100644 --- a/docs/book/README.md +++ b/docs/book/README.md @@ -1,15 +1,3 @@ -This project contains the core of a new introductory guide to Leptos. - -It is built using `mdbook`. You can view a local copy by installing `mdbook` - -```bash -cargo install mdbook -``` - -and run the book with -``` -mdbook serve -``` - -It should be available at `http://localhost:3000`. +The Leptos book is now available at [https://book.leptos.dev](https://book.leptos.dev). +The source code for the book has moved to [https://github.com/leptos-rs/book](https://github.com/leptos-rs/book). Please open issues or make PRs in that repository. diff --git a/docs/book/src/01_introduction.md b/docs/book/src/01_introduction.md index a69dcad788..a7b691c8dd 100644 --- a/docs/book/src/01_introduction.md +++ b/docs/book/src/01_introduction.md @@ -1,22 +1,2 @@ -# Introduction - -This book is intended as an introduction to the [Leptos](https://github.com/leptos-rs/leptos) Web framework. -It will walk through the fundamental concepts you need to build applications, -beginning with a simple application rendered in the browser, and building toward a -full-stack application with server-side rendering and hydration. - -The guide doesn’t assume you know anything about fine-grained reactivity or the -details of modern Web frameworks. It does assume you are familiar with the Rust -programming language, HTML, CSS, and the DOM and basic Web APIs. - -Leptos is most similar to frameworks like [Solid](https://www.solidjs.com) (JavaScript) -and [Sycamore](https://sycamore-rs.netlify.app/) (Rust). There are some similarities -to other frameworks like React (JavaScript), Svelte (JavaScript), Yew (Rust), and -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/). - -**Important Note**: This current version of the book reflects the `0.5.1` release. The CodeSandbox versions of the examples still reflect `0.4` and earlier APIs and are in the process of being updated. - -> The source code for the book is available [here](https://github.com/leptos-rs/leptos/tree/main/docs/book). PRs for typos or clarification are always welcome. + + diff --git a/docs/book/src/15_global_state.md b/docs/book/src/15_global_state.md index 57873be079..7dd4311717 100644 --- a/docs/book/src/15_global_state.md +++ b/docs/book/src/15_global_state.md @@ -1,402 +1,2 @@ -# Global State Management - -So far, we've only been working with local state in components, and we’ve seen how to coordinate state between parent and child components. On occasion, there are times where people look for a more general solution for global state management that can work throughout an application. - -In general, **you do not need this chapter.** The typical pattern is to compose your application out of components, each of which manages its own local state, not to store all state in a global structure. However, there are some cases (like theming, saving user settings, or sharing data between components in different parts of your UI) in which you may want to use some kind of global state management. - -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 - -In many ways, the URL is actually the best way to store global state. It can be accessed from any component, anywhere in your tree. There are native HTML elements like `
` and `` that exist solely to update the URL. And it persists across page reloads and between devices; you can share a URL with a friend or send it from your phone to your laptop and any state stored in it will be replicated. - -The next few sections of the tutorial will be about the router, and we’ll get much more into these topics. - -But for now, we'll just look at options #2 and #3. - -## Option #2: Passing Signals through Context - -In the section on [parent-child communication](view/08_parent_child.md), we saw that you can use `provide_context` to pass signal from a parent component to a child, and `use_context` to read it in the child. But `provide_context` works across any distance. If you want to create a global signal that holds some piece of state, you can provide it and access it via context anywhere in the descendants of the component where you provide it. - -A signal provided via context only causes reactive updates where it is read, not in any of the components in between, so it maintains the power of fine-grained reactive updates, even at a distance. - -We start by creating a signal in the root of the app and providing it to -all its children and descendants using `provide_context`. - -```rust -#[component] -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); - // we'll pass the setter to specific components, - // but provide the count itself to the whole app via context - provide_context(count); - - view! { - // 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 - - - } -} -``` - -`` is the kind of counter we’ve written several times now. -(See the sandbox below if you don’t understand what I mean.) - -`` and `` both consume the signal we’re providing via -`use_context` and do something with it. - -```rust -/// 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." -
- } -} -``` - -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: - -```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()); - - // etc. -} -``` - -## 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. - -```rust -#[derive(Copy, Clone, Debug, Default)] -struct GlobalState { - count: i32, - name: String -} - -#[component] -fn App() -> impl IntoView { - provide_context(create_rw_signal(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::>(); - - // `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} -
- } -} -``` - -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 -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. - -[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! { }) -} -``` - -
    - + + diff --git a/docs/book/src/appendix_reactive_graph.md b/docs/book/src/appendix_reactive_graph.md index b2c82770ed..c796cea688 100644 --- a/docs/book/src/appendix_reactive_graph.md +++ b/docs/book/src/appendix_reactive_graph.md @@ -1,243 +1,2 @@ -# Appendix: How does the Reactive System Work? - -You don’t need to know very much about how the reactive system actually works in order to use the library successfully. But it’s always useful to understand what’s going on behind the scenes once you start working with the framework at an advanced level. - -The reactive primitives you use are divided into three sets: - -- **Signals** (`ReadSignal`/`WriteSignal`, `RwSignal`, `Resource`, `Trigger`) Values you can actively change to trigger reactive updates. -- **Computations** (`Memo`s) Values that depend on signals (or other computations) and derive a new reactive value through some pure computation. -- **Effects** Observers that listen to changes in some signals or computations and run a function, causing some side effect. - -Derived signals are a kind of non-primitve computation: as plain closures, they simply allow you to refactor some repeated signal-based computation into a reusable function that can be called in multiple places, but they are not represented in the reactive system itself. - -All the other primitives actually exist in the reactive system as nodes in a reactive graph. - -Most of the work of the reactive system consists of propagating changes from signals to effects, possibly through some intervening memos. - -The assumption of the reactive system is that effects (like rendering to the DOM or making a network request) are orders of magnitude more expensive than things like updating a Rust data structure inside your app. - -So the **primary goal** of the reactive system is to **run effects as infrequently as possible**. - -Leptos does this through the construction of a reactive graph. - -> Leptos’s current reactive system is based heavily on the [Reactively](https://github.com/modderme123/reactively) library for JavaScript. You can read Milo’s article “[Super-Charging Fine-Grained Reactivity](https://dev.to/modderme123/super-charging-fine-grained-reactive-performance-47ph)” for an excellent account of its algorithm, as well as fine-grained reactivity in general—including some beautiful diagrams! - -## The Reactive Graph - -Signals, memos, and effects all share three characteristics: - -- **Value** They have a current value: either the signal’s value, or (for memos and effects) the value returned by the previous run, if any. -- **Sources** Any other reactive primitives they depend on. (For signals, this is an empty set.) -- **Subscribers** Any other reactive primitives that depend on them. (For effects, this is an empty set.) - -In reality then, signals, memos, and effects are just conventional names for one generic concept of a “node” in a reactive graph. Signals are always “root nodes,” with no sources/parents. Effects are always “leaf nodes,” with no subscribers. Memos typically have both sources and subscribers. - -### Simple Dependencies - -So imagine the following code: - -```rust -// A -let (name, set_name) = create_signal("Alice"); - -// B -let name_upper = create_memo(move |_| name.with(|n| n.to_uppercase())); - -// C -create_effect(move |_| { - log!("{}", name_upper()); -}); - -set_name("Bob"); -``` - -You can easily imagine the reactive graph here: `name` is the only signal/origin node, the `create_effect` is the only effect/terminal node, and there’s one intervening memo. - -``` -A (name) -| -B (name_upper) -| -C (the effect) -``` - -### Splitting Branches - -Let’s make it a little more complex. - -```rust -// A -let (name, set_name) = create_signal("Alice"); - -// B -let name_upper = create_memo(move |_| name.with(|n| n.to_uppercase())); - -// C -let name_len = create_memo(move |_| name.len()); - -// D -create_effect(move |_| { - log!("len = {}", name_len()); -}); - -// E -create_effect(move |_| { - log!("name = {}", name_upper()); -}); -``` - -This is also pretty straightforward: a signal source signal (`name`/`A`) divides into two parallel tracks: `name_upper`/`B` and `name_len`/`C`, each of which has an effect that depends on it. - -``` - __A__ -| | -B C -| | -D E -``` - -Now let’s update the signal. - -```rust -set_name("Bob"); -``` - -We immediately log - -``` -len = 3 -name = BOB -``` - -Let’s do it again. - -```rust -set_name("Tim"); -``` - -The log should shows - -``` -name = TIM -``` - -`len = 3` does not log again. - -Remember: the goal of the reactive system is to run effects as infrequently as possible. Changing `name` from `"Bob"` to `"Tim"` will cause each of the memos to re-run. But they will only notify their subscribers if their value has actually changed. `"BOB"` and `"TIM"` are different, so that effect runs again. But both names have the length `3`, so they do not run again. - -### Reuniting Branches - -One more example, of what’s sometimes called **the diamond problem**. - -```rust -// A -let (name, set_name) = create_signal("Alice"); - -// B -let name_upper = create_memo(move |_| name.with(|n| n.to_uppercase())); - -// C -let name_len = create_memo(move |_| name.len()); - -// D -create_effect(move |_| { - log!("{} is {} characters long", name_upper(), name_len()); -}); -``` - -What does the graph look like for this? - -``` - __A__ -| | -B C -| | -|__D__| -``` - -You can see why it's called the “diamond problem.” If I’d connected the nodes with straight lines instead of bad ASCII art, it would form a diamond: two memos, each of which depend on a signal, which feed into the same effect. - -A naive, push-based reactive implementation would cause this effect to run twice, which would be bad. (Remember, our goal is to run effects as infrequently as we can.) For example, you could implement a reactive system such that signals and memos immediately propagate their changes all the way down the graph, through each dependency, essentially traversing the graph depth-first. In other words, updating `A` would notify `B`, which would notify `D`; then `A` would notify `C`, which would notify `D` again. This is both inefficient (`D` runs twice) and glitchy (`D` actually runs with the incorrect value for the second memo during its first run.) - -## Solving the Diamond Problem - -Any reactive implementation worth its salt is dedicated to solving this issue. There are a number of different approaches (again, [see Milo’s article](https://dev.to/modderme123/super-charging-fine-grained-reactive-performance-47ph) for an excellent overview). - -Here’s how ours works, in brief. - -A reactive node is always in one of three states: - -- `Clean`: it is known not to have changed -- `Check`: it is possible it has changed -- `Dirty`: it has definitely changed - -Updating a signal `Dirty` marks that signal `Dirty`, and marks all its descendants `Check`, recursively. Any of its descendants that are effects are added to a queue to be re-run. - -``` - ____A (DIRTY)___ - | | -B (CHECK) C (CHECK) - | | - |____D (CHECK)__| -``` - -Now those effects are run. (All of the effects will be marked `Check` at this point.) Before re-running its computation, the effect checks its parents to see if they are dirty. So - -- So `D` goes to `B` and checks if it is `Dirty`. -- But `B` is also marked `Check`. So `B` does the same thing: - - `B` goes to `A`, and finds that it is `Dirty`. - - This means `B` needs to re-run, because one of its sources has changed. - - `B` re-runs, generating a new value, and marks itself `Clean` - - Because `B` is a memo, it then checks its prior value against the new value. - - If they are the same, `B` returns "no change." Otherwise, it returns "yes, I changed." -- If `B` returned “yes, I changed,” `D` knows that it definitely needs to run and re-runs immediately before checking any other sources. -- If `B` returned “no, I didn’t change,” `D` continues on to check `C` (see process above for `B`.) -- If neither `B` nor `C` has changed, the effect does not need to re-run. -- If either `B` or `C` did change, the effect now re-runs. - -Because the effect is only marked `Check` once and only queued once, it only runs once. - -If the naive version was a “push-based” reactive system, simply pushing reactive changes all the way down the graph and therefore running the effect twice, this version could be called “push-pull.” It pushes the `Check` status all the way down the graph, but then “pulls” its way back up. In fact, for large graphs it may end up bouncing back up and down and left and right on the graph as it tries to determine exactly which nodes need to re-run. - -**Note this important trade-off**: Push-based reactivity propagates signal changes more quickly, at the expense of over-re-running memos and effects. Remember: the reactive system is designed to minimize how often you re-run effects, on the (accurate) assumption that side effects are orders of magnitude more expensive than this kind of cache-friendly graph traversal happening entirely inside the library’s Rust code. The measurement of a good reactive system is not how quickly it propagates changes, but how quickly it propagates changes _without over-notifying_. - -## Memos vs. Signals - -Note that signals always notify their children; i.e., a signal is always marked `Dirty` when it updates, even if its new value is the same as the old value. Otherwise, we’d have to require `PartialEq` on signals, and this is actually quite an expensive check on some types. (For example, add an unnecessary equality check to something like `some_vec_signal.update(|n| n.pop())` when it’s clear that it has in fact changed.) - -Memos, on the other hand, check whether they change before notifying their children. They only run their calculation once, no matter how many times you `.get()` the result, but they run whenever their signal sources change. This means that if the memo’s computation is _very_ expensive, you may actually want to memoize its inputs as well, so that the memo only re-calculates when it is sure its inputs have changed. - -## Memos vs. Derived Signals - -All of this is cool, and memos are pretty great. But most actual applications have reactive graphs that are quite shallow and quite wide: you might have 100 source signals and 500 effects, but no memos or, in rare case, three or four memos between the signal and the effect. Memos are extremely good at what they do: limiting how often they notify their subscribers that they have changed. But as this description of the reactive system should show, they come with overhead in two forms: - -1. A `PartialEq` check, which may or may not be expensive. -2. Added memory cost of storing another node in the reactive system. -3. Added computational cost of reactive graph traversal. - -In cases in which the computation itself is cheaper than this reactive work, you should avoid “over-wrapping” with memos and simply use derived signals. Here’s a great example in which you should never use a memo: - -```rust -let (a, set_a) = create_signal(1); -// none of these make sense as memos -let b = move || a() + 2; -let c = move || b() % 2 == 0; -let d = move || if c() { "even" } else { "odd" }; - -set_a(2); -set_a(3); -set_a(5); -``` - -Even though memoizing would technically save an extra calculation of `d` between setting `a` to `3` and `5`, these calculations are themselves cheaper than the reactive algorithm. - -At the very most, you might consider memoizing the final node before running some expensive side effect: - -```rust -let text = create_memo(move |_| { - d() -}); -create_effect(move |_| { - engrave_text_into_bar_of_gold(&text()); -}); -``` + + diff --git a/docs/book/src/async/10_resources.md b/docs/book/src/async/10_resources.md index 08a6ad392f..8201ce20f7 100644 --- a/docs/book/src/async/10_resources.md +++ b/docs/book/src/async/10_resources.md @@ -1,136 +1,2 @@ -# Loading Data with Resources - -A [Resource](https://docs.rs/leptos/latest/leptos/struct.Resource.html) is a reactive data structure that reflects the current state of an asynchronous task, allowing you to integrate asynchronous `Future`s into the synchronous reactive system. Rather than waiting for its data to load with `.await`, you transform the `Future` into a signal that returns `Some(T)` if it has resolved, and `None` if it’s still pending. - -You do this by using the [`create_resource`](https://docs.rs/leptos/latest/leptos/fn.create_resource.html) function. This takes two arguments: - -1. a source signal, which will generate a new `Future` whenever it changes -2. a fetcher function, which takes the data from that signal and returns a `Future` - -Here’s an example - -```rust -// our source signal: some synchronous, local state -let (count, set_count) = create_signal(0); - -// our resource -let async_data = create_resource( - count, - // every time `count` changes, this will run - |value| async move { - logging::log!("loading data from API"); - load_data(value).await - }, -); -``` - -To create a resource that simply runs once, you can pass a non-reactive, empty source signal: - -```rust -let once = create_resource(|| (), |_| async move { load_data().await }); -``` - -To access the value you can use `.get()` or `.with(|data| /* */)`. These work just like `.get()` and `.with()` on a signal—`get` clones the value and returns it, `with` applies a closure to it—but for any `Resource<_, T>`, they always return `Option`, not `T`: because it’s always possible that your resource is still loading. - -So, you can show the current state of a resource in your view: - -```rust -let once = create_resource(|| (), |_| async move { load_data().await }); -view! { -

    "My Data"

    - {move || match once.get() { - None => view! {

    "Loading..."

    }.into_view(), - Some(data) => view! { }.into_view() - }} -} -``` - -Resources also provide a `refetch()` method that allows you to manually reload the data (for example, in response to a button click) and a `loading()` method that returns a `ReadSignal` indicating whether the resource is currently loading or not. - -[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/10-resources-0-5-x6h5j6?file=%2Fsrc%2Fmain.rs%3A2%2C3) - - - -
    -CodeSandbox Source - -```rust -use gloo_timers::future::TimeoutFuture; -use leptos::*; - -// Here we define an async function -// This could be anything: a network request, database read, etc. -// Here, we just multiply a number by 10 -async fn load_data(value: i32) -> i32 { - // fake a one-second delay - TimeoutFuture::new(1_000).await; - value * 10 -} - -#[component] -fn App() -> impl IntoView { - // this count is our synchronous, local state - let (count, set_count) = create_signal(0); - - // create_resource takes two arguments after its scope - let async_data = create_resource( - // the first is the "source signal" - count, - // the second is the loader - // it takes the source signal's value as its argument - // and does some async work - |value| async move { load_data(value).await }, - ); - // whenever the source signal changes, the loader reloads - - // you can also create resources that only load once - // just return the unit type () from the source signal - // that doesn't depend on anything: we just load it once - let stable = create_resource(|| (), |_| async move { load_data(1).await }); - - // we can access the resource values with .get() - // this will reactively return None before the Future has resolved - // and update to Some(T) when it has resolved - let async_result = move || { - async_data - .get() - .map(|value| format!("Server returned {value:?}")) - // This loading state will only show before the first load - .unwrap_or_else(|| "Loading...".into()) - }; - - // the resource's loading() method gives us a - // signal to indicate whether it's currently loading - let loading = async_data.loading(); - let is_loading = move || if loading() { "Loading..." } else { "Idle." }; - - view! { - -

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

    -

    - "count"": " {count} -

    -

    - "async_value"": " - {async_result} -
    - {is_loading} -

    - } -} - -fn main() { - leptos::mount_to_body(App) -} -``` - -
    - + + diff --git a/docs/book/src/async/11_suspense.md b/docs/book/src/async/11_suspense.md index e23f764ea1..d6e1a2395c 100644 --- a/docs/book/src/async/11_suspense.md +++ b/docs/book/src/async/11_suspense.md @@ -1,156 +1,2 @@ -# `` - -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 }); - -view! { -

    "My Data"

    - {move || match once.get() { - None => view! {

    "Loading..."

    }.into_view(), - Some(data) => view! { }.into_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 }); - -view! { -

    "My Data"

    - {move || match (a.get(), b.get()) { - (Some(a), Some(b)) => view! { - - - }.into_view(), - _ => view! {

    "Loading..."

    }.into_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. - -```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 }); - -view! { -

    "My Data"

    - "Loading..."

    } - > -

    "My Data"

    -

    "A"

    - {move || { - a.get() - .map(|a| view! { }) - }} -

    "B"

    - {move || { - b.get() - .map(|b| view! { }) - }} -
    -} -``` - -Every time one of the resources is reloading, the `"Loading..."` fallback will show again. - -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. - -## `` - -In 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. - -In other words: - -1. It only polls the `Future` once, and does not respond to any reactive changes. -2. It does not render anything until the `Future` resolves. -3. After the `Future` resolves, its binds its data to whatever variable name you choose and then renders its children with that variable in scope. - -```rust -async fn fetch_monkeys(monkey: i32) -> i32 { - // maybe this didn't need to be async - monkey * 2 -} -view! { - - // you receive the data by reference and can use it in your view here -

    {*data} " little monkeys, jumping on the bed."

    -
    -} -``` - -[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/11-suspense-0-5-qzpgqs?file=%2Fsrc%2Fmain.rs%3A1%2C1) - - - -
    -CodeSandbox Source - -```rust -use gloo_timers::future::TimeoutFuture; -use leptos::*; - -async fn important_api_call(name: String) -> String { - TimeoutFuture::new(1_000).await; - name.to_ascii_uppercase() -} - -#[component] -fn App() -> impl IntoView { - let (name, set_name) = create_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 }, - ); - - view! { - -

    "name:" {name}

    - "Loading..."

    } - > - // the children will be rendered once initially, - // and then whenever any resources has been resolved -

    - "Your shouting name is " - {move || async_data.get()} -

    -
    - } -} - -fn main() { - leptos::mount_to_body(App) -} -``` - -
    - + + diff --git a/docs/book/src/async/12_transition.md b/docs/book/src/async/12_transition.md index 85db85b3fc..4773504f57 100644 --- a/docs/book/src/async/12_transition.md +++ b/docs/book/src/async/12_transition.md @@ -1,83 +1,2 @@ -# `` - -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). - -`` 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. - -This example shows how you can create a simple tabbed contact list with ``. When you select a new tab, it continues showing the current contact until the new data loads. This can be a much better user experience than constantly falling back to a loading message. - -[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/12-transition-0-5-2jg5lz?file=%2Fsrc%2Fmain.rs%3A1%2C1) - - - -
    -CodeSandbox Source - -```rust -use gloo_timers::future::TimeoutFuture; -use leptos::*; - -async fn important_api_call(id: usize) -> String { - TimeoutFuture::new(1_000).await; - match id { - 0 => "Alice", - 1 => "Bob", - 2 => "Carol", - _ => "User not found", - } - .to_string() -} - -#[component] -fn App() -> impl IntoView { - let (tab, set_tab) = create_signal(0); - - // this will reload every time `tab` changes - let user_data = create_resource(tab, |tab| async move { important_api_call(tab).await }); - - view! { -
    - - - - {move || if user_data.loading().get() { - "Loading..." - } else { - "" - }} -
    - "Loading..."

    } - > -

    - {move || user_data.read()} -

    -
    - } -} - -fn main() { - leptos::mount_to_body(App) -} -``` - -
    - + + diff --git a/docs/book/src/async/13_actions.md b/docs/book/src/async/13_actions.md index 625dc33baa..cf865fc12a 100644 --- a/docs/book/src/async/13_actions.md +++ b/docs/book/src/async/13_actions.md @@ -1,176 +1,2 @@ -# Mutating Data with Actions - -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... - -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). - -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`. - -Say we have some `async` function we want to run. - -```rust -async fn add_todo_request(new_title: &str) -> Uuid { - /* do some stuff on the server to add a new todo */ -} -``` - -`create_action` 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 input = input.clone(); -> async move { todo!() } -> }); -> -> // if there are no arguments, use the unit type `()` -> let action2 = create_action(|input: &()| async { todo!() }); -> -> // if there are multiple arguments, use a tuple -> let action3 = create_action( -> |input: &(usize, String)| async { todo!() } -> ); -> ``` -> -> Because the action function takes a reference but the `Future` needs to have a `'static` lifetime, you’ll usually need to clone the value to pass it into the `Future`. This is admittedly awkward but it unlocks some powerful features like optimistic UI. We’ll see a little more about that in future chapters. - -So in this case, all we need to do to create an action is - -```rust -let add_todo_action = create_action(|input: &String| { - let input = input.to_owned(); - async move { add_todo_request(&input).await } -}); -``` - -Rather than calling `add_todo_action` directly, we’ll call it with `.dispatch()`, as in - -```rust -add_todo_action.dispatch("Some value".to_string()); -``` - -You can do this from an event listener, a timeout, or anywhere; because `.dispatch()` isn’t an `async` function, it can be called from a synchronous context. - -Actions provide access to a few signals that synchronize between the asynchronous action you’re calling and the synchronous reactive system: - -```rust -let submitted = add_todo_action.input(); // RwSignal> -let pending = add_todo_action.pending(); // ReadSignal -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::(); - -view! { - - - - - // use our loading state -

    {move || pending().then("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.) - -[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/13-actions-0-5-8xk35v?file=%2Fsrc%2Fmain.rs%3A1%2C1) - - - -
    -CodeSandbox Source - -```rust -use gloo_timers::future::TimeoutFuture; -use leptos::{html::Input, *}; -use uuid::Uuid; - -// Here we define an async function -// This could be anything: a network request, database read, etc. -// Think of it as a mutation: some imperative async action you run, -// whereas a resource would be some async data you load -async fn add_todo(text: &str) -> Uuid { - _ = text; - // fake a one-second delay - TimeoutFuture::new(1_000).await; - // pretend this is a post ID or something - Uuid::new_v4() -} - -#[component] -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| { - // 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 - let input = input.to_owned(); - async move { add_todo(&input).await } - }); - - // actions provide a bunch of synchronous, reactive variables - // that tell us different things about the state of the action - let submitted = add_todo.input(); - let pending = add_todo.pending(); - let todo_id = add_todo.value(); - - let input_ref = create_node_ref::(); - - view! { -
    - - -
    -

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

    -

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

    -

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

    -

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

    - } -} - -fn main() { - leptos::mount_to_body(App) -} -``` - -
    - + + diff --git a/docs/book/src/async/README.md b/docs/book/src/async/README.md index 84b20d7d2e..b6758d05af 100644 --- a/docs/book/src/async/README.md +++ b/docs/book/src/async/README.md @@ -1,9 +1,2 @@ -# Working with `async` - -So far we’ve only been working with synchronous users interfaces: You provide some input, -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. - -In this chapter, we’ll see how Leptos helps smooth out that process for you. + + diff --git a/docs/book/src/csr_wrapping_up.md b/docs/book/src/csr_wrapping_up.md index b34c50468c..4c504b0654 100644 --- a/docs/book/src/csr_wrapping_up.md +++ b/docs/book/src/csr_wrapping_up.md @@ -1,26 +1,2 @@ -# Wrapping Up Part 1: Client-Side Rendering - -So far, everything we’ve written has been rendered almost entirely in the browser. When we create an app using Trunk, it’s served using a local development server. If you build it for production and deploy it, it’s served by whatever server or CDN you’re using. In either case, what’s served is an HTML page with - -1. the URL of your Leptos app, which has been compiled to WebAssembly (WASM) -2. the URL of the JavaScript used to initialize this WASM blob -3. an empty `` element - -When the JS and WASM have loaded, Leptos will render your app into the ``. This means that nothing appears on the screen until JS/WASM have loaded and run. This has some drawbacks: - -1. It increases load time, as your user’s screen is blank until additional resources have been downloaded. -2. It’s bad for SEO, as load times are longer and the HTML you serve has no meaningful content. -3. It’s broken for users for whom JS/WASM don’t load for some reason (e.g., they’re on a train and just went into a tunnel before WASM finished loading; they’re using an older device that doesn’t support WASM; they have JavaScript or WASM turned off for some reason; etc.) - -These downsides apply across the web ecosystem, but especially to WASM apps. - -However, depending the on the requirements of your project, you may be fine with these limitations. - -If you just want to deploy your Client-Side Rendered website, skip ahead to the chapter on ["Deployment"](https://leptos-rs.github.io/leptos/deployment/index.html) - there, you'll find directions on how best to deploy your Leptos CSR site. - - -But what do you do if you want to return more than just an empty `` tag in your `index.html` page? Use “Server-Side Rendering”! - -Whole books could be (and probably have been) written about this topic, but at its core, it’s really simple: rather than returning an empty `` tag, with SSR, you'll return an initial HTML page that reflects the actual starting state of your app or site, so that while JS/WASM are loading, and until they load, the user can access the plain HTML version. - -Part 2 of this book, on Leptos SSR, will cover this topic in some detail! + + diff --git a/docs/book/src/deployment/README.md b/docs/book/src/deployment/README.md index fa6c2042af..0a3d6a2b57 100644 --- a/docs/book/src/deployment/README.md +++ b/docs/book/src/deployment/README.md @@ -1,74 +1,2 @@ -# Deployment - -There are as many ways to deploy a web application as there are developers, let alone applications. But there are a couple useful tips to keep in mind when deploying an app. - -## General Advice - -1. Remember: Always deploy Rust apps built in `--release` mode, not debug mode. This has a huge effect on both performance and binary size. -2. Test locally in release mode as well. The framework applies certain optimizations in release mode that it does not apply in debug mode, so it’s possible for bugs to surface at this point. (If your app behaves differently or you do encounter a bug, it’s likely a framework-level bug and you should open a GitHub issue with a reproduction.) -3. See the chapter on "Optimizing WASM Binary Size" for additional tips and tricks to further improve the time-to-interactive metric for your WASM app on first load. - -> We asked users to submit their deployment setups to help with this chapter. I’ll quote from them below, but you can read the full thread [here](https://github.com/leptos-rs/leptos/issues/1152). - -## Deploying a Client-Side-Rendered App - -If you’ve been building an app that only uses client-side rendering, working with Trunk as a dev server and build tool, the process is quite easy. - -```bash -trunk build --release -``` - -`trunk build` will create a number of build artifacts in a `dist/` directory. Publishing `dist` somewhere online should be all you need to deploy your app. This should work very similarly to deploying any JavaScript application. - -> Read more: [Deploying to Vercel with GitHub Actions](https://github.com/leptos-rs/leptos/issues/1152#issuecomment-1577861900). - -## Deploying a Full-Stack App - -The most popular way for people to deploy full-stack apps built with `cargo-leptos` is to use a cloud hosting service that supports deployment via a Docker build. Here’s a sample `Dockerfile`, which is based on the one we use to deploy the Leptos website. - -```dockerfile -# Get started with a build env with Rust nightly -FROM rustlang/rust:nightly-bullseye as builder - -# If you’re using stable, use this instead -# FROM rust:1.70-bullseye as builder - -# Install cargo-binstall, which makes it easier to install other -# cargo extensions like cargo-leptos -RUN wget https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz -RUN tar -xvf cargo-binstall-x86_64-unknown-linux-musl.tgz -RUN cp cargo-binstall /usr/local/cargo/bin - -# Install cargo-leptos -RUN cargo binstall cargo-leptos -y - -# Add the WASM target -RUN rustup target add wasm32-unknown-unknown - -# Make an /app dir, which everything will eventually live in -RUN mkdir -p /app -WORKDIR /app -COPY . . - -# Build the app -RUN cargo leptos build --release -vv - -FROM rustlang/rust:nightly-bullseye as runner -# Copy the server binary to the /app directory -COPY --from=builder /app/target/release/leptos-start /app/ -# /target/site contains our JS/WASM/CSS, etc. -COPY --from=builder /app/target/site /app/site -# Copy Cargo.toml if it’s needed at runtime -COPY --from=builder /app/Cargo.toml /app/ -WORKDIR /app - -# Set any required env variables and -ENV RUST_LOG="info" -ENV LEPTOS_SITE_ADDR="0.0.0.0:8080" -ENV LEPTOS_SITE_ROOT="site" -EXPOSE 8080 -# Run the server -CMD ["/app/leptos_start"] -``` - -> Read more: [`gnu` and `musl` build files for Leptos apps](https://github.com/leptos-rs/leptos/issues/1152#issuecomment-1634916088). + + diff --git a/docs/book/src/deployment/binary_size.md b/docs/book/src/deployment/binary_size.md index 267796fedc..9d3afd9019 100644 --- a/docs/book/src/deployment/binary_size.md +++ b/docs/book/src/deployment/binary_size.md @@ -1,72 +1,2 @@ -# Optimizing WASM Binary Size - -One of the primary downsides of deploying a Rust/WebAssembly frontend app is that splitting a WASM file into smaller chunks to be dynamically loaded is significantly more difficult than splitting a JavaScript bundle. There have been experiments like [`wasm-split`](https://emscripten.org/docs/optimizing/Module-Splitting.html) in the Emscripten ecosystem but at present there’s no way to split and dynamically load a Rust/`wasm-bindgen` binary. This means that the whole WASM binary needs to be loaded before your app becomes interactive. Because the WASM format is designed for streaming compilation, WASM files are much faster to compile per kilobyte than JavaScript files. (For a deeper look, you can [read this great article from the Mozilla team](https://hacks.mozilla.org/2018/01/making-webassembly-even-faster-firefoxs-new-streaming-and-tiering-compiler/) on streaming WASM compilation.) - -Still, it’s important to ship the smallest WASM binary to users that you can, as it will reduce their network usage and make your app interactive as quickly as possible. - -So what are some practical steps? - -## Things to Do - -1. Make sure you’re looking at a release build. (Debug builds are much, much larger.) -2. Add a release profile for WASM that optimizes for size, not speed. - -For a `cargo-leptos` project, for example, you can add this to your `Cargo.toml`: - -```toml -[profile.wasm-release] -inherits = "release" -opt-level = 'z' -lto = true -codegen-units = 1 - -# .... - -[package.metadata.leptos] -# .... -lib-profile-release = "wasm-release" -``` - -This will hyper-optimize the WASM for your release build for size, while keeping your server build optimized for speed. (For a pure client-rendered app without server considerations, just use the `[profile.wasm-release]` block as your `[profile.release]`.) - -3. Always serve compressed WASM in production. WASM tends to compress very well, typically shrinking to less than 50% its uncompressed size, and it’s trivial to enable compression for static files being served from Actix or Axum. - -4. If you’re using nightly Rust, you can rebuild the standard library with this same profile rather than the prebuilt standard library that’s distributed with the `wasm32-unknown-unknown` target. - -To do this, create a file in your project at `.cargo/config.toml` - -```toml -[unstable] -build-std = ["std", "panic_abort", "core", "alloc"] -build-std-features = ["panic_immediate_abort"] -``` - -Note that if you're using this with SSR too, the same Cargo profile will be applied. You'll need to explicitly specify your target: -```toml -[build] -target = "x86_64-unknown-linux-gnu" # or whatever -``` - -Also note that in some cases, the cfg feature `has_std` will not be set, which may cause build errors with some dependencies which check for `has_std`. You may fix any build errors due to this by adding: -```toml -[build] -rustflags = ["--cfg=has_std"] -``` - -And you'll need to add `panic = "abort"` to `[profile.release]` in `Cargo.toml`. Note that this applies the same `build-std` and panic settings to your server binary, which may not be desirable. Some further exploration is probably needed here. - -5. One of the sources of binary size in WASM binaries can be `serde` serialization/deserialization code. Leptos uses `serde` by default to serialize and deserialize resources created with `create_resource`. You might try experimenting with the `miniserde` and `serde-lite` features, which allow you to use those crates for serialization and deserialization instead; each only implements a subset of `serde`’s functionality, but typically optimizes for size over speed. - -## Things to Avoid - -There are certain crates that tend to inflate binary sizes. For example, the `regex` crate with its default features adds about 500kb to a WASM binary (largely because it has to pull in Unicode table data!). In a size-conscious setting, you might consider avoiding regexes in general, or even dropping down and calling browser APIs to use the built-in regex engine instead. (This is what `leptos_router` does on the few occasions it needs a regular expression.) - -In general, Rust’s commitment to runtime performance is sometimes at odds with a commitment to a small binary. For example, Rust monomorphizes generic functions, meaning it creates a distinct copy of the function for each generic type it’s called with. This is significantly faster than dynamic dispatch, but increases binary size. Leptos tries to balance runtime performance with binary size considerations pretty carefully; but you might find that writing code that uses many generics tends to increase binary size. For example, if you have a generic component with a lot of code in its body and call it with four different types, remember that the compiler could include four copies of that same code. Refactoring to use a concrete inner function or helper can often maintain performance and ergonomics while reducing binary size. - -## A Final Thought - -Remember that in a server-rendered app, JS bundle size/WASM binary size affects only _one_ thing: time to interactivity on the first load. This is very important to a good user experience: nobody wants to click a button three times and have it do nothing because the interactive code is still loading — but it's not the only important measure. - -It’s especially worth remembering that streaming in a single WASM binary means all subsequent navigations are nearly instantaneous, depending only on any additional data loading. Precisely because your WASM binary is _not_ bundle split, navigating to a new route does not require loading additional JS/WASM, as it does in nearly every JavaScript framework. Is this copium? Maybe. Or maybe it’s just an honest trade-off between the two approaches! - -Always take the opportunity to optimize the low-hanging fruit in your application. And always test your app under real circumstances with real user network speeds and devices before making any heroic efforts. + + diff --git a/docs/book/src/getting_started/README.md b/docs/book/src/getting_started/README.md index 567a60404c..a95f235d38 100644 --- a/docs/book/src/getting_started/README.md +++ b/docs/book/src/getting_started/README.md @@ -1,125 +1,2 @@ -# Getting Started - -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. - -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. - -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 UI's 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. - -```admonish note -If you're coming from the Javascript world and terms like client-side rendering (CSR) and server-side rendering (SSR) are unfamiliar to you, the easiest way to understand the difference is by analogy: - -Leptos' CSR mode is similar to working with React (or a 'signals'-based framework like SolidJS), and focuses on producing a client-side UI which you can use with any tech stack on the server. - -Using Leptos' SSR mode is similar to working with a full-stack framework like Next.js in the React world (or Solid's "SolidStart" framework) - SSR helps you build sites and apps that are rendered on the server then sent down to the client. SSR can help to improve your site's loading performance and accessibility as well as make it easier for one person to work on *both* client- and server-side without needing to context-switch between different languages for frontend and backend. - -The Leptos framework can be used either in CSR mode to just make a UI (like React), or you can use Leptos in full-stack SSR mode (like Next.js) so that you can build both your UI and your server with one language: Rust. - -``` - -## Hello World! Getting Set up for Leptos CSR Development - -First up, make sure Rust is installed and up-to-date ([see here if you need instructions](https://www.rust-lang.org/tools/install)). - -If you don’t have it installed already, you can install the "Trunk" tool for running Leptos CSR sites by running the following on the command-line: - -```bash -cargo install trunk -``` - -And then create a basic Rust project - -```bash -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 -rustup target add wasm32-unknown-unknown -``` - -Create a simple `index.html` in the root of the `leptos-tutorial` directory - -```html - - - - - -``` - -And add a simple “Hello, world!” to your `main.rs` - -```rust -use leptos::*; - -fn main() { - mount_to_body(|| view! {

    "Hello, world!"

    }) -} -``` - -Your directory structure should now look something like this - -``` -leptos_tutorial -├── src -│ └── main.rs -├── Cargo.toml -├── index.html -``` - -Now run `trunk serve --open` from the root of the `leptos-tutorial` directory. -Trunk should automatically compile your app and open it in your default browser. -If you make edits to `main.rs`, Trunk will recompile your source code and -live-reload the page. - - -Welcome to the world of UI development with Rust and WebAssembly (WASM), powered by Leptos and Trunk! - ---- - -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. \ No newline at end of file + + diff --git a/docs/book/src/getting_started/community_crates.md b/docs/book/src/getting_started/community_crates.md index 510c0632dc..b2f5cd5a0d 100644 --- a/docs/book/src/getting_started/community_crates.md +++ b/docs/book/src/getting_started/community_crates.md @@ -1,23 +1,2 @@ -# The Leptos Community and `leptos-*` Crates - -## The Community - -One final note before we get to building with Leptos: if you haven't already, feel free to join the growing community on the Leptos [Discord](https://discord.gg/YdRAhS7eQB) and on [Github](https://github.com/leptos-rs/leptos). Our Discord channel in particular is very active and friendly - we'd love to have you there! - -```admonish note -If you find a chapter or an explanation that isn't clear while you're working your way through the Leptos book, just mention it in the "docs-and-education" channel or ask a question in "help" so we can clear things up and update the book for others. -``` - -As you get further along in your Leptos journey and find that you have questions about "how to do 'x' with Leptos", then search the Discord "help" channel to see if a similar question has been asked before, or feel free to post your own question - the community is quite helpful and very responsive. - -The "[Discussions](https://github.com/leptos-rs/leptos/discussions)" on Github are also a great place for asking questions and keeping up with Leptos announcements. - -And of course, if you run into any bugs while developing with Leptos or would like to make a feature request (or contribute a bug fix / new feature), open up an issue on the [Github issue tracker](https://github.com/leptos-rs/leptos/issues). - - -## Leptos-* Crates - -The community has built a growing number of Leptos-related crates that will help you get productive with Leptos projects more quickly - check out the list of crates built on top of Leptos and contributed by the community on the [Awesome Leptos](https://github.com/leptos-rs/awesome-leptos) repo on Github. - -If you want to find the newest, up-and-coming Leptos-related crates, check out the "Tools and Libraries" section of the Leptos Discord. In that section, there are channels for the Leptos `view!` macro formatter (in the "leptosfmt" channel); there's a channel for the utility library "leptos-use"; another channel for the UI component libary "leptonic"; and a "libraries" channel where new `leptos-*` crates are discussed before making their way into the growing list of crates and resources available on [Awesome Leptos](https://github.com/leptos-rs/awesome-leptos). - + + diff --git a/docs/book/src/getting_started/leptos_dx.md b/docs/book/src/getting_started/leptos_dx.md index 39d799e0da..d7aa528335 100644 --- a/docs/book/src/getting_started/leptos_dx.md +++ b/docs/book/src/getting_started/leptos_dx.md @@ -1,92 +1,2 @@ -# Leptos Developer Experience Improvements - - -There are a couple of things you can do to improve your experience of developing websites and apps with Leptos. You may want to take a few minutes and set up your environment to optimize your development experience, especially if you want to code along with the examples in this book. - -## 1) Editor Autocompletion inside `#[component]` and `#[server]` - -Because of the nature of macros (they can expand from anything to anything, but only if the input is exactly correct at that instant) it can be hard for rust-analyzer to do proper autocompletion and other support. - - -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). -Note that this means that rust-analyzer doesn't know about your component props, which may generate its own set of errors or warnings in the IDE. - -VSCode `settings.json`: - -```json -"rust-analyzer.procMacro.ignored": { - "leptos_macro": [ - // optional: - // "component", - "server" - ], -} -``` - -neovim with lspconfig: - -```lua -require('lspconfig').rust_analyzer.setup { - -- Other Configs ... - settings = { - ["rust-analyzer"] = { - -- Other Settings ... - procMacro = { - ignored = { - leptos_macro = { - -- optional: -- - -- "component", - "server", - }, - }, - }, - }, - } -} -``` - -Helix, in `.helix/languages.toml`: - -```toml -[[language]] -name = "rust" - -[language-server.rust-analyzer] -config = { procMacro = { ignored = - { leptos_macro = - [ - # Optional: - # "component", - "server" - ] - } -} } -``` - -```admonish info -The Jetbrains `intellij-rust` plugin (RustRover as well) currently does not support dynamic config for macro exclusion. -However, the project currently maintains a hardcoded list of excluded macros. -As soon as [this open PR](https://github.com/intellij-rust/intellij-rust/pull/10873) is merged, the `component` and -`server` macro will be excluded automatically without additional configuration needed. - -Update (2023/10/02): -The `intellij-rust` plugin got deprecated in favor of RustRover at the same time the PR was opened, but an official -support request was made to integrate the contents of this PR. -``` - -## 2) Set up `leptosfmt` With Rust Analyzer (optional) - -"leptosfmt" is a formatter for the Leptos `view!` macro (inside of which you'll typically write your UI code). Because the view! macro enables an 'RSX' (like JSX) style of writing your UI's, cargo-fmt has a harder time auto-formatting your code that's inside the view! macro. leptosfmt is a crate that solves your formattting issues and keeps your RSX-style UI code looking nice and tidy! - -leptosfmt can be installed and used via the commandline or from within your code editor: - -First, install the tool with `cargo install leptosfmt`. - -If you just want to use the default options from the command line, just run `leptosfmt ./**/*.rs` from the root of your project to format all the rust files using leptosfmt. - -If you wish to set up your editor to work with leptosfmt, or if you wish to customize your leptosfmt experience, please see the instructions available on the [leptosfmt github repo's README.md page](https://github.com/bram209/leptosfmt). - -Just note that it's recommended to set up your editor with `leptosfmt` on a per-workspace basis for best results. - + + diff --git a/docs/book/src/interlude_projecting_children.md b/docs/book/src/interlude_projecting_children.md index bd46ec7006..ff9d7f6fa5 100644 --- a/docs/book/src/interlude_projecting_children.md +++ b/docs/book/src/interlude_projecting_children.md @@ -1,175 +1,2 @@ -# Projecting Children - -As you build components you may occasionally find yourself wanting to “project” children through multiple layers of components. - -## The Problem - -Consider the following: - -```rust -pub fn LoggedIn(fallback: F, children: ChildrenFn) -> impl IntoView -where - F: Fn() -> IV + 'static, - IV: IntoView, -{ - view! { - - - {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. - -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.” - -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 -``` - -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. - -## The Details - -> Feel free to skip ahead to the solution. - -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) - .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(), - ) - .into_view()), - ] - }) - } - }) - }) - .build(), -) -``` - -All components own their props; so the `` in this case can’t be called because it only has captured references to `fallback` and `children`. - -## Solution - -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. - -In this case, it’s really simple: - -```rust -pub fn LoggedIn(fallback: F, children: ChildrenFn) -> impl IntoView -where - F: Fn() -> IV + 'static, - IV: IntoView, -{ - let fallback = store_value(fallback); - let children = store_value(children); - view! { - - - {children.with_value(|children| children())} - - - } -} -``` - -At the top level, we store both `fallback` and `children` in the reactive scope owned by `LoggedIn`. Now we can simply move those references down through the other layers into the `` component and call them there. - -## 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. - -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. - -Consider this example - -```rust -#[component] -pub fn App() -> impl IntoView { - let name = "Alice".to_string(); - view! { - - - - - - } -} - -#[component] -pub fn Outer(children: ChildrenFn) -> impl IntoView { - children() -} - -#[component] -pub fn Inner(children: ChildrenFn) -> impl IntoView { - children() -} - -#[component] -pub fn Inmost(name: String) -> impl IntoView { - view! { -

    {name}

    - } -} -``` - -Even with `name=name.clone()`, this gives the error - -``` -cannot move out of `name`, a captured variable in an `Fn` closure -``` - -It’s captured through multiple levels of children that need to run more than once, and there’s no obvious way to clone it _into_ the children. - -In this case, the `clone:` syntax comes in handy. Calling `clone:name` will clone `name` _before_ moving it into ``’s children, which solves our ownership issue. - -```rust -view! { - - - - - -} -``` - -These issues can be a little tricky to understand or debug, because of the opacity of the `view` macro. But in general, they can always be solved. + + diff --git a/docs/book/src/interlude_styling.md b/docs/book/src/interlude_styling.md index eaba34226d..328fbdd990 100644 --- a/docs/book/src/interlude_styling.md +++ b/docs/book/src/interlude_styling.md @@ -1,112 +1,2 @@ -# Interlude: Styling - -Anyone creating a website or application soon runs into the question of styling. For a small app, a single CSS file is probably plenty to style your user interface. But as an application grows, many developers find that plain CSS becomes increasingly hard to manage. - -Some frontend frameworks (like Angular, Vue, and Svelte) provide built-in ways to scope your CSS to particular components, making it easier to manage styles across a whole application without styles meant to modify one small component having a global effect. Other frameworks (like React or Solid) don’t provide built-in CSS scoping, but rely on libraries in the ecosystem to do it for them. Leptos is in this latter camp: the framework itself has no opinions about CSS at all, but provides a few tools and primitives that allow others to build styling libraries. - -Here are a few different approaches to styling your Leptos app, other than plain CSS. - -## TailwindCSS: Utility-first CSS - -[TailwindCSS](https://tailwindcss.com/) is a popular utility-first CSS library. It allows you to style your application by using inline utility classes, with a custom CLI tool that scans your files for Tailwind class names and bundles the necessary CSS. - -This allows you to write components like this: - -```rust -#[component] -fn Home() -> impl IntoView { - let (count, set_count) = create_signal(0); - - view! { -
    -

    "Welcome to Leptos with Tailwind"

    -

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

    - -
    - } -} -``` - -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 - -[Stylers](https://github.com/abishekatp/stylers) is a compile-time scoped CSS library that lets you declare scoped CSS in the body of your component. Stylers will extract this CSS at compile time into CSS files that you can then import into your app, which means that it doesn’t add anything to the WASM binary size of your application. - -This allows you to write components like this: - -```rust -use stylers::style; - -#[component] -pub fn App() -> impl IntoView { - let styler_class = style! { "App", - ##two{ - color: blue; - } - div.one{ - color: red; - content: raw_str(r#"\hello"#); - font: "1.3em/1.2" Arial, Helvetica, sans-serif; - } - div { - border: 1px solid black; - margin: 25px 50px 75px 100px; - background-color: lightblue; - } - h2 { - color: purple; - } - @media only screen and (max-width: 1000px) { - h3 { - background-color: lightblue; - color: blue - } - } - }; - - view! { class = styler_class, -
    -

    "Hello"

    -

    "World"

    -

    "and"

    -

    "friends!"

    -
    - } -} -``` - -## 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/docs/book/src/islands.md b/docs/book/src/islands.md index ebaad678ba..da9012d25a 100644 --- a/docs/book/src/islands.md +++ b/docs/book/src/islands.md @@ -1,489 +1,2 @@ -# 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. - -## The Islands Architecture - -The dominant JavaScript frontend frameworks (React, Vue, Svelte, Solid, Angular) all originated as frameworks for building client-rendered single-page apps (SPAs). The initial page load is rendered to HTML, then hydrated, and subsequent navigations are handled directly in the client. (Hence “single page”: everything happens from a single page load from the server, even if there is client-side routing later.) Each of these frameworks later added server-side rendering to improve initial load times, SEO, and user experience. - -This means that by default, the entire app is interactive. It also means that the entire app has to be shipped to the client as JavaScript in order to be hydrated. Leptos has followed this same pattern. - -> You can read more in the chapters on [server-side rendering](./ssr/22_life_cycle.md). - -But it’s also possible to work in the opposite direction. Rather than taking an entirely-interactive app, rendering it to HTML on the server, and then hydrating it in the browser, you can begin with a plain HTML page and add small areas of interactivity. This is the traditional format for any website or app before the 2010s: your browser makes a series of requests to the server and returns the HTML for each new page in response. After the rise of “single-page apps” (SPA), this approach has sometimes become known as a “multi-page app” (MPA) by comparison. - -The phrase “islands architecture” has emerged recently to describe the approach of beginning with a “sea” of server-rendered HTML pages, and adding “islands” of interactivity throughout the page. - -> ### Additional Reading -> -> The rest of this guide will look at how to use islands with Leptos. For more background on the approach in general, check out some of the articles below: -> -> - Jason Miller, [“Islands Architecture”](https://jasonformat.com/islands-architecture/), Jason Miller -> - Ryan Carniato, [“Islands & Server Components & Resumability, Oh My!”](https://dev.to/this-is-learning/islands-server-components-resumability-oh-my-319d) -> - [“Islands Architectures”](https://www.patterns.dev/posts/islands-architecture) on patterns.dev -> - [Astro Islands](https://docs.astro.build/en/concepts/islands/) - -## Activating Islands Mode - -Let’s start with a fresh `cargo-leptos` app: - -```bash -cargo leptos new --git leptos-rs/start -``` - -> I’m using Actix because I like it. Feel free to use Axum; there should be approximately no server-specific differences in this guide. - -I’m just going to run - -```bash -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`: - -```toml -leptos = { version = "0.5", features = ["nightly", "experimental-islands"] } -leptos_actix = { version = "0.5", optional = true, features = [ - "experimental-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 - -```rust -leptos::leptos_dom::HydrationCtx::stop_hydrating(); -``` - -Each “island” we create will actually act as its own entrypoint, so our `hydrate()` function just says “okay, hydration’s done now.” - -Okay, now fire up your `cargo leptos watch` and go to [`http://localhost:3000`](http://localhost:3000) (or wherever). - -Click the button, and... - -Nothing happens! - -Perfect. - -## Using Islands - -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.) - -When we click the button, nothing happens, because our whole page is static. - -So how do we make something happen? - -Let’s turn the `HomePage` component into an island! - -Here was the non-interactive version: - -```rust -#[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); - - view! { -

    "Welcome to Leptos!"

    - - } -} -``` - -Here’s the interactive version: - -```rust -#[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); - - view! { -

    "Welcome to Leptos!"

    - - } -} -``` - -Now when I click the button, it works! - -The `#[island]` macro works exactly like the `#[component]` macro, except that in islands mode, it designates this as an interactive island. If we check the binary size again, this is 166kb uncompressed in release mode; much larger than the 24kb totally static version, but much smaller than the 355kb fully-hydrated version. - -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!

    - -
    -``` - -The typical Leptos hydration keys and markers are only present inside the island, only the island is hydrated. - -## Using Islands Effectively - -Remember that _only_ code within an `#[island]` needs to be compiled to WASM and shipped to the browser. This means that islands should be as small and specific as possible. My `HomePage`, for example, would be better broken apart into a regular component and an island: - -```rust -#[component] -fn HomePage() -> impl IntoView { - view! { -

    "Welcome to Leptos!"

    - - } -} - -#[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); - - view! { - - } -} -``` - -Now the `

    ` doesn’t need to be included in the client bundle, or hydrated. This seems like a silly distinction now; but note that you can now add as much inert HTML content as you want to the `HomePage` itself, and the WASM binary size will remain exactly the same. - -In regular hydration mode, your WASM binary size grows as a function of the size/complexity of your app. In islands mode, your WASM binary grows as a function of the amount of interactivity in your app. You can add as much non-interactive content as you want, outside islands, and it will not increase that binary size. - -## Unlocking Superpowers - -So, this 50% reduction in WASM binary size is nice. But really, what’s the point? - -The point comes when you combine two key facts: - -1. Code inside `#[component]` functions now _only_ runs on the server. -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. - -We’re going to rely on a third fact in the rest of this demo: - -3. Context can be passed between otherwise-independent islands. - -So, instead of our counter demo, let’s make something a little more fun: a tabbed interface that reads data from files on the server. - -## Passing Server Children to Islands - -One of the most powerful things about islands is that you can pass server-rendered children into an island, without the island needing to know anything about them. Islands hydrate their own content, but not children that are passed to them. - -As Dan Abramov of React put it (in the very similar context of RSCs), islands aren’t really islands: they’re donuts. You can pass server-only content directly into the “donut hole,” as it were, allowing you to create tiny atolls of interactivity, surrounded on _both_ sides by the sea of inert server HTML. - -> In the demo code included below, I added some styles to show all server content as a light-blue “sea,” and all islands as light-green “land.” Hopefully that will help picture what I’m talking about! - -To continue with the demo: I’m going to create a `Tabs` component. Switching between tabs will require some interactivity, so of course this will be an island. Let’s start simple for now: - -```rust -#[island] -fn Tabs(labels: Vec) -> impl IntoView { - let buttons = labels - .into_iter() - .map(|label| view! { }) - .collect_view(); - view! { -
    - {buttons} -
    - } -} -``` - -Oops. This gives me an error - -``` -error[E0463]: can't find crate for `serde` - --> src/app.rs:43:1 - | -43 | #[island] - | ^^^^^^^^^ can't find crate -``` - -Easy fix: let’s `cargo add serde --features=derive`. The `#[island]` macro wants to pull in `serde` here because it needs to serialize and deserialize the `labels` prop. - -Now let’s update the `HomePage` to use `Tabs`. - -```rust -#[component] -fn HomePage() -> impl IntoView { - // these are the files we’re going to read - let files = ["a.txt", "b.txt", "c.txt"]; - // the tab labels will just be the file names - let labels = files.iter().copied().map(Into::into).collect(); - view! { -

    "Welcome to Leptos!"

    -

    "Click any of the tabs below to read a recipe."

    - - } -} -``` - -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. - -Now let’s add some tabs. For the moment, a `Tab` island will be really simple: - -```rust -#[island] -fn Tab(index: usize, children: Children) -> impl IntoView { - view! { -
    {children()}
    - } -} -``` - -Each tab, for now will just be a `
    ` wrapping its children. - -Our `Tabs` component will also get some children: for now, let’s just show them all. - -```rust -#[island] -fn Tabs(labels: Vec, children: Children) -> impl IntoView { - let buttons = labels - .into_iter() - .map(|label| view! { }) - .collect_view(); - view! { -
    - {buttons} -
    - {children()} - } -} -``` - -Okay, now let’s go back into the `HomePage`. We’re going to create the list of tabs to put into our tab box. - -```rust -#[component] -fn HomePage() -> impl IntoView { - let files = ["a.txt", "b.txt", "c.txt"]; - let labels = files.iter().copied().map(Into::into).collect(); - let tabs = move || { - files - .into_iter() - .enumerate() - .map(|(index, filename)| { - let content = std::fs::read_to_string(filename).unwrap(); - view! { - -

    {filename.to_string()}

    -

    {content}

    -
    - } - }) - .collect_view() - }; - - view! { -

    "Welcome to Leptos!"

    -

    "Click any of the tabs below to read a recipe."

    - -
    {tabs()}
    -
    - } -} -``` - -Uh... What? - -If you’re used to using Leptos, you know that you just can’t do this. All code in the body of components has to run on the server (to be rendered to HTML) and in the browser (to hydrate), so you can’t just call `std::fs`; it will panic, because there’s no access to the local filesystem (and certainly not to the server filesystem!) in the browser. This would be a security nightmare! - -Except... wait. We’re in islands mode. This `HomePage` component _really does_ only run on the server. So we can, in fact, just use ordinary server code like this. - -> **Is this a dumb example?** Yes! Synchronously reading from three different local files in a `.map()` is not a good choice in real life. The point here is just to demonstrate that this is, definitely, server-only content. - -Go ahead and create three files in the root of the project called `a.txt`, `b.txt`, and `c.txt`, and fill them in with whatever content you’d like. - -Refresh the page and you should see the content in the browser. Edit the files and refresh again; it will be updated. - -You can pass server-only content from a `#[component]` into the children of an `#[island]`, without the island needing to know anything about how to access that data or render that content. - -**This is really important.** Passing server `children` to islands means that you can keep islands small. Ideally, you don’t want to slap and `#[island]` around a whole chunk of your page. You want to break that chunk out into an interactive piece, which can be an `#[island]`, and a bunch of additional server content that can be passed to that island as `children`, so that the non-interactive subsections of an interactive part of the page can be kept out of the WASM binary. - -## Passing Context Between Islands - -These aren’t really “tabs” yet: they just show every tab, all the time. So let’s add some simple logic to our `Tabs` and `Tab` components. - -We’ll modify `Tabs` to create a simple `selected` signal. We provide the read half via context, and set the value of the signal whenever someone clicks one of our buttons. - -```rust -#[island] -fn Tabs(labels: Vec, children: Children) -> impl IntoView { - let (selected, set_selected) = create_signal(0); - provide_context(selected); - - let buttons = labels - .into_iter() - .enumerate() - .map(|(index, label)| view! { - - }) - .collect_view(); -// ... -``` - -And let’s modify the `Tab` island to use that context to show or hide itself: - -```rust -#[island] -fn Tab(children: Children) -> impl IntoView { - let selected = expect_context::>(); - view! { -
    -// ... -``` - -Now the tabs behave exactly as I’d expect. `Tabs` passes the signal via context to each `Tab`, which uses it to determine whether it should be open or not. - -> 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. - -## 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. - -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: - -- add client-side routing for islands apps by fetching subsequent navigations from the server and replacing the HTML document with the new one -- add animated transitions between the old and new document using the View Transitions API -- support explicit persistent islands, i.e., islands that you can mark with unique IDs (something like `persist:searchbar` on the component in the view), which can be copied over from the old to the new document without losing their current state - -There are other, larger architectural changes that I’m [not sold on yet](https://github.com/leptos-rs/leptos/issues/1830). - -## 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/main/examples/hackernews_islands_axum) for additional discussion. - -## Demo Code - -```rust -use leptos::*; -use leptos_router::*; - -#[component] -pub fn App() -> impl IntoView { - view! { - -
    - - - -
    -
    - } -} - -/// Renders the home page of your application. -#[component] -fn HomePage() -> impl IntoView { - let files = ["a.txt", "b.txt", "c.txt"]; - let labels = files.iter().copied().map(Into::into).collect(); - let tabs = move || { - files - .into_iter() - .enumerate() - .map(|(index, filename)| { - let content = std::fs::read_to_string(filename).unwrap(); - view! { - -
    -

    {filename.to_string()}

    -

    {content}

    -
    -
    - } - }) - .collect_view() - }; - - view! { -

    "Welcome to Leptos!"

    -

    "Click any of the tabs below to read a recipe."

    - -
    {tabs()}
    -
    - } -} - -#[island] -fn Tabs(labels: Vec, children: Children) -> impl IntoView { - let (selected, set_selected) = create_signal(0); - provide_context(selected); - - let buttons = labels - .into_iter() - .enumerate() - .map(|(index, label)| { - view! { - - } - }) - .collect_view(); - view! { -
    - {buttons} -
    - {children()} - } -} - -#[island] -fn Tab(index: usize, children: Children) -> impl IntoView { - let selected = expect_context::>(); - view! { -
    - {children()} -
    - } -} -``` + + diff --git a/docs/book/src/metadata.md b/docs/book/src/metadata.md index 0de45f7f5e..24a2c480ba 100644 --- a/docs/book/src/metadata.md +++ b/docs/book/src/metadata.md @@ -1,49 +1,2 @@ -# Metadata - -So far, everything we’ve rendered has been inside the `` of the HTML document. And this makes sense. After all, everything you can see on a web page lives inside the ``. - -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. - -## 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`. - -[`<Link/>`](https://docs.rs/leptos_meta/latest/leptos_meta/fn.Link.html) takes the standard attributes of the `<link>` element. - -[`<Stylesheet/>`](https://docs.rs/leptos_meta/latest/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>`. - -[`<Meta/>`](https://docs.rs/leptos_meta/latest/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. - -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: - -```rust -<Html - lang="he" - dir="rtl" - attr:data-theme="dark" -/> -``` - -## Metadata and Server Rendering - -Now, some of this is useful in any scenario, but some of it is especially important for search-engine optimization (SEO). Making sure you have things like appropriate `<title>` and `<meta>` tags is crucial. Modern search engine crawlers do handle client-side rendering, i.e., apps that are shipped as an empty `index.html` and rendered entirely in JS/WASM. But they prefer to receive pages in which your app has been rendered to actual HTML, with metadata in the `<head>`. - -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. As a matter of fact... Let’s do that next! +<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/metadata.html"> +<link rel="canonical" href="https://book.leptos.dev/metadata.html"> diff --git a/docs/book/src/progressive_enhancement/README.md b/docs/book/src/progressive_enhancement/README.md index 8a992a5f37..a6cf93624f 100644 --- a/docs/book/src/progressive_enhancement/README.md +++ b/docs/book/src/progressive_enhancement/README.md @@ -1,36 +1,2 @@ -# 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. - -“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: -- 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. -- They have JavaScript turned off for security or privacy reasons. -- JavaScript isn’t supported on their device (for example, some accessibility devices only support HTML browsing) -- The JavaScript (or WASM) never arrived at their device because they walked outside and lost WiFi. -- 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? - -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. - -**Graceful degradation is especially important for WASM apps,** because WASM is the newest and least-likely-to-be-supported of the four languages that run in the browser (HTML, CSS, JS, WASM). - -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. -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 +<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/progressive_enhancement/index.html"> +<link rel="canonical" href="https://book.leptos.dev/progressive_enhancement/index.html"> diff --git a/docs/book/src/progressive_enhancement/action_form.md b/docs/book/src/progressive_enhancement/action_form.md index 25cf4d9521..6c48fd1b03 100644 --- a/docs/book/src/progressive_enhancement/action_form.md +++ b/docs/book/src/progressive_enhancement/action_form.md @@ -1,100 +1,2 @@ -# `<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. - -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. -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")] -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> - } -} -``` - -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. - -## 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. - -```rust -let on_submit = move |ev| { - let data = AddTodo::from_event(&ev); - // silly example of validation: if the todo is "nope!", nope it - if data.is_err() || data.unwrap().title == "nope!" { - // ev.prevent_default() will prevent form submission - ev.prevent_default(); - } -} -``` - -## Complex Inputs - -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, - last_name: String, -} - -#[component] -fn ComplexInput() -> impl IntoView { - let submit = Action::<VeryImportantFn, _>::server(); - - view! { - <ActionForm action=submit> - <input type="text" name="hefty_arg[first_name]" value="leptos"/> - <input - type="text" - name="hefty_arg[last_name]" - value="closures-everywhere" - /> - <input type="submit"/> - </ActionForm> - } -} - -#[server] -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(()) -} - -``` +<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/progressive_enhancement/action_form.html"> +<link rel="canonical" href="https://book.leptos.dev/progressive_enhancement/action_form.html"> diff --git a/docs/book/src/reactivity/14_create_effect.md b/docs/book/src/reactivity/14_create_effect.md index b1fc25dc43..2650abd10b 100644 --- a/docs/book/src/reactivity/14_create_effect.md +++ b/docs/book/src/reactivity/14_create_effect.md @@ -1,327 +1,2 @@ -# Responding to Changes with `create_effect` - -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`. - -[`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. - -```rust -let (a, set_a) = create_signal(0); -let (b, set_b) = create_signal(0); - -create_effect(move |_| { - // immediately prints "Value: 0" and subscribes to `a` - log::debug!("Value: {}", a()); -}); -``` - -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). - -## Autotracking and Dynamic Dependencies - -If you’re familiar with a framework like React, you might notice one key difference. React and similar frameworks typically require you to pass a “dependency array,” an explicit set of variables that determine when the effect should rerun. - -Because Leptos comes from the tradition of synchronous reactive programming, we don’t need this explicit dependency list. Instead, we automatically track dependencies depending on which signals are accessed within the effect. - -This has two effects (no pun intended). Dependencies are: - -1. **Automatic**: You don’t need to maintain a dependency list, or worry about what should or shouldn’t be included. The framework simply tracks which signals might cause the effect to rerun, and handles it for you. -2. **Dynamic**: The dependency list is cleared and updated every time the effect runs. If your effect contains a conditional (for example), only signals that are used in the current branch are tracked. This means that effects rerun the absolute minimum number of times. - -> If this sounds like magic, and if you want a deep dive into how automatic dependency tracking works, [check out this video](https://www.youtube.com/watch?v=GWB3vTWeLd4). (Apologies for the low volume!) - -## Effects as Zero-Cost-ish Abstraction - -While they’re not a “zero-cost abstraction” in the most technical sense—they require some additional memory use, exist at runtime, etc.—at a higher level, from the perspective of whatever expensive API calls or other work you’re doing within them, effects are a zero-cost abstraction. They rerun the absolute minimum number of times necessary, given how you’ve described them. - -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); - -// this will add the name to the log -// any time one of the source signals changes -create_effect(move |_| { - log( - if use_last() { - format!("{} {}", first(), last()) - } else { - first() - }, - ) -}); -``` - -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`? - -Effects are intended to run _side-effects_ of the system, not to synchronize state _within_ the system. In other words: don’t write to signals within effects. - -If you need to define a signal that depends on the value of other signals, use a derived signal or [`create_memo`](https://docs.rs/leptos_reactive/latest/leptos_reactive/fn.create_memo.html). - -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—create an effect. - -> If you’re curious for more information about when you should and shouldn’t use `create_effect`, [check out this video](https://www.youtube.com/watch?v=aQOFJQ2JkvQ) for a more in-depth consideration! - -## Effects and Rendering - -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); - -view! { - <p>{count}</p> -} -``` - -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); - -// create a DOM element -let p = create_element("p"); - -// create an effect to reactively update the text -create_effect(move |prev_value| { - // first, access the signal’s value and convert it to a string - let text = count().to_string(); - - // if this is different from the previous value, update the node - if prev_value != Some(text) { - p.set_text_content(&text); - } - - // return this value so we can memoize the next update - text -}); -``` - -Every time `count` is updated, this effect wil rerun. This is what allows reactive, fine-grained updates to the DOM. - -## Explicit, Cancelable Tracking with `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: - -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. - -```rust -let (num, set_num) = create_signal(0); - -let stop = watch( - move || num.get(), - move |num, prev_num, _| { - log::debug!("Number: {}; Prev: {:?}", num, prev_num); - }, - false, -); - -set_num.set(1); // > "Number: 1; Prev: Some(0)" - -stop(); // stop watching - -set_num.set(2); // (nothing happens) -``` - -[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/14-effect-0-5-d6hkch?file=%2Fsrc%2Fmain.rs%3A1%2C1) - -<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> - -<details> -<summary>CodeSandbox Source</summary> - -```rust -use leptos::html::Input; -use leptos::*; - -#[derive(Copy, Clone)] -struct LogContext(RwSignal<Vec<String>>); - -#[component] -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"); - - // the newtype pattern isn't *necessary* here but is a good practice - // it avoids confusion with other possible future `RwSignal<Vec<String>>` contexts - // and makes it easier to refer to it - provide_context(LogContext(log)); - - view! { - <CreateAnEffect/> - <pre>{logged}</pre> - } -} - -#[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); - - // 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}")) - } else { - first() - }) - }); - - view! { - <h1> - <code>"create_effect"</code> - " Version" - </h1> - <form> - <label> - "First Name" - <input - type="text" - name="first" - prop:value=first - on:change=move |ev| set_first(event_target_value(&ev)) - /> - </label> - <label> - "Last Name" - <input - type="text" - name="last" - prop:value=last - on:change=move |ev| set_last(event_target_value(&ev)) - /> - </label> - <label> - "Show Last Name" - <input - type="checkbox" - name="use_last" - prop:checked=use_last - on:change=move |ev| set_use_last(event_target_checked(&ev)) - /> - </label> - </form> - } -} - -#[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 mut prev_name = String::new(); - let on_change = move |_| { - log(" listener"); - let first = first.get().unwrap(); - let last = last.get().unwrap(); - let use_last = use_last.get().unwrap(); - let this_one = if use_last.checked() { - format!("{} {}", first.value(), last.value()) - } else { - first.value() - }; - - if this_one != prev_name { - log(&this_one); - prev_name = this_one; - } - }; - - view! { - <h1>"Manual Version"</h1> - <form on:change=on_change> - <label>"First Name" <input type="text" name="first" node_ref=first/></label> - <label>"Last Name" <input type="text" name="last" node_ref=last/></label> - <label> - "Show Last Name" <input type="checkbox" name="use_last" checked node_ref=use_last/> - </label> - </form> - } -} - -#[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) -} -``` - -</details> -</preview> +<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/reactivity/14_create_effect.html"> +<link rel="canonical" href="https://book.leptos.dev/reactivity/14_create_effect.html"> diff --git a/docs/book/src/reactivity/README.md b/docs/book/src/reactivity/README.md index 29b7794b7d..66da2468cf 100644 --- a/docs/book/src/reactivity/README.md +++ b/docs/book/src/reactivity/README.md @@ -1,5 +1,2 @@ -# Reactivity - -Leptos is built on top of a fine-grained reactive system, designed to run expensive side effects (like rendering something in a browser, or making a network request) as infrequently as possible in response to change, reactive values. - -So far we’ve seen signals in action. These chapters will go into a bit more depth, and look at effects, which are the other half of the story. +<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/reactivity/index.html"> +<link rel="canonical" href="https://book.leptos.dev/reactivity/index.html"> diff --git a/docs/book/src/reactivity/interlude_functions.md b/docs/book/src/reactivity/interlude_functions.md index 0091c01ab3..0543fc1129 100644 --- a/docs/book/src/reactivity/interlude_functions.md +++ b/docs/book/src/reactivity/interlude_functions.md @@ -1,76 +1,2 @@ -# Interlude: Reactivity and Functions - -One of our core contributors said to me recently: “I never used closures this often -until I started using Leptos.” And it’s true. Closures are at the heart of any Leptos -application. It sometimes looks a little silly: - -```rust -// a signal holds a value, and can be updated -let (count, set_count) = create_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 text = move || if count_is_odd() { - "odd" -} else { - "even" -}; - -// an effect automatically tracks the signals it depends on -// and reruns when they change -create_effect(move |_| { - logging::log!("text = {}", text()); -}); - -view! { - <p>{move || text().to_uppercase()}</p> -} -``` - -Closures, closures everywhere! - -But why? - -## Functions and UI Frameworks - -Functions are at the heart of every UI framework. And this makes perfect sense. Creating a user interface is basically divided into two phases: - -1. initial rendering -2. updates - -In a web framework, the framework does some kind of initial rendering. Then it hands control back over to the browser. When certain events fire (like a mouse click) or asynchronous tasks finish (like an HTTP request finishing), the browser wakes the framework back up to update something. The framework runs some kind of code to update your user interface, and goes back asleep until the browser wakes it up again. - -The key phrase here is “runs some kind of code.” The natural way to “run some kind of code” at an arbitrary point in time—in Rust or in any other programming language—is to call a function. And in fact every UI framework is based on rerunning some kind of function over and over: - -1. virtual DOM (VDOM) frameworks like React, Yew, or Dioxus rerun a component or render function over and over, to generate a virtual DOM tree that can be reconciled with the previous result to patch the DOM -2. compiled frameworks like Angular and Svelte divide your component templates into “create” and “update” functions, rerunning the update function when they detect a change to the component’s state -3. in fine-grained reactive frameworks like SolidJS, Sycamore, or Leptos, _you_ define the functions that rerun - -That’s what all our components are doing. - -Take our typical `<SimpleCounter/>` example in its simplest form: - -```rust -#[component] -pub fn SimpleCounter() -> impl IntoView { - let (value, set_value) = create_signal(0); - - let increment = move |_| set_value.update(|value| *value += 1); - - view! { - <button on:click=increment> - {value} - </button> - } -} -``` - -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. - -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. +<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/reactivity/interlude_functions.html"> +<link rel="canonical" href="https://book.leptos.dev/reactivity/interlude_functions.html"> diff --git a/docs/book/src/reactivity/working_with_signals.md b/docs/book/src/reactivity/working_with_signals.md index 89556c9170..187d22295a 100644 --- a/docs/book/src/reactivity/working_with_signals.md +++ b/docs/book/src/reactivity/working_with_signals.md @@ -1,140 +1,2 @@ -# 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. - -## Getting and Setting - -There are four 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.) - -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 (count, set_count) = create_signal(0); -set_count(1); -logging::log!(count()); -``` - -is the same as - -```rust -let (count, set_count) = create_signal(0); -set_count.set(1); -logging::log!(count.get()); -``` - -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)`. - -But of course, `.get()` and `.set()` (or the plain function-call forms!) are much nicer syntax. - -However, there are some very good use cases for `.with()` and `.update()`. - -For example, consider a signal that holds a `Vec<String>`. - -```rust -let (names, set_names) = create_signal(Vec::new()); -if names().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. - -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())); -} -``` - -Now our function simply takes `names` by reference to run `is_empty()`, avoiding that clone. - -And if you have Clippy on, or if you have sharp eyes, you may notice we can make this even neater: - -```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: - -```rust -let name = move || { - first.with(|first| { - middle.with(|middle| last.with(|last| format!("{first} {middle} {last}"))) - }) -}; -``` - -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. - -```rust -let name = move || with!(|first, middle, last| format!("{first} {middle} {last}")); -``` - -This expands to the same thing as above. Take a look at the `with!` docs for more info, and the corresponding macros `update!`, `with_value!` and `update_value!`. - -## Making signals depend on each other - -Often people ask about situations in which some signal needs to change based on some other signal’s value. There are three good ways to do this, and one that’s less than ideal but okay under controlled circumstances. - -### Good Options - -**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); -let derived_signal_double_count = move || count() * 2; -let memoized_double_count = create_memo(move |_| count() * 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) - -**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()); -let (last_name, set_last_name) = create_signal("Jones".to_string()); -let full_name = move || with!(|first_name, last_name| format!("{first_name} {last_name}")); -``` - -**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); -let (favorite_number, set_favorite_number) = create_signal(42); -// use this to handle a click on a `Clear` button -let clear_handler = move |_| { - set_age(0); - set_favorite_number(0); -}; -``` - -### If you really must... - -**4) Create an effect to write to B whenever A changes.** This is officially discouraged, for several reasons: -a) It will always be less efficient, as it means every time A updates you do two full trips through the reactive process. (You set A, which causes the effect to run, as well as any other effects that depend on A. Then you set B, which causes any effects that depend on B to run.) -b) It increases your chances of accidentally creating things like infinite loops or over-re-running effects. This is the kind of ping-ponging, reactive spaghetti code that was common in the early 2010s and that we try to avoid with things like read-write segregation and discouraging writing to signals from effects. - -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. +<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/reactivity/working_with_signals.html"> +<link rel="canonical" href="https://book.leptos.dev/reactivity/working_with_signals.html"> diff --git a/docs/book/src/router/16_routes.md b/docs/book/src/router/16_routes.md index e84fbefab2..9818979e32 100644 --- a/docs/book/src/router/16_routes.md +++ b/docs/book/src/router/16_routes.md @@ -1,144 +1,2 @@ -# Defining Routes - -## Getting Started - -It’s easy to get started with the router. - -First things first, make sure you’ve added the `leptos_router` package to your dependencies. - -> 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::*; -``` - -## 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, 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::*; - -#[component] -pub fn App() -> impl IntoView { - 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. - -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::*; - -#[component] -pub fn App() -> impl IntoView { - view! { - <Router> - <nav> - /* ... */ - </nav> - <main> - // all our routes will appear inside <main> - <Routes> - /* ... */ - </Routes> - </main> - </Router> - } -} -``` - -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 - -- a static path (`/users`), -- dynamic, named parameters beginning with a colon (`/:id`), -- and/or a wildcard beginning with an asterisk (`/user/*any`) - -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> -``` - -> `view` takes a `Fn() -> impl IntoView`. If a component has no props, it can be passed directly into the `view`. In this case, `view=Home` is just a shorthand for `|| view! { <Home/> }`. - -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. +<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/router/16_routes.html"> +<link rel="canonical" href="https://book.leptos.dev/router/16_routes.html"> diff --git a/docs/book/src/router/17_nested_routing.md b/docs/book/src/router/17_nested_routing.md index 88444cce61..bb214073e6 100644 --- a/docs/book/src/router/17_nested_routing.md +++ b/docs/book/src/router/17_nested_routing.md @@ -1,324 +1,2 @@ -# Nested Routing - -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> -``` - -There’s a certain amount of duplication here: `/users` and `/users/:id`. This is fine for a small app, but you can probably already tell it won’t scale well. Wouldn’t it be nice if we could nest these routes? - -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> -``` - -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. - -# Nested Routes as Layout - -Nested routes are a form of layout, not a method of route definition. - -Let me put that another way: The goal of defining nested routes is not primarily to avoid repeating yourself when typing out the paths in your route definitions. It is actually to tell the router to display multiple `<Route/>`s on the page at the same time, side by side. - -Let’s look back at our practical example. - -```rust -<Routes> - <Route path="/users" view=Users/> - <Route path="/users/:id" view=UserProfile/> -</Routes> -``` - -This means: - -- If I go to `/users`, I get the `<Users/>` component. -- If I go to `/users/3`, I get the `<UserProfile/>` component (with the parameter `id` set to `3`; more on that later) - -Let’s say I use nested routes instead: - -```rust -<Routes> - <Route path="/users" view=Users> - <Route path=":id" view=UserProfile/> - </Route> -</Routes> -``` - -This means: - -- If I go to `/users/3`, the path matches two `<Route/>`s: `<Users/>` and `<UserProfile/>`. -- If I go to `/users`, the path is not matched. - -I actually need to add a fallback route - -```rust -<Routes> - <Route path="/users" view=Users> - <Route path=":id" view=UserProfile/> - <Route path="" view=NoUser/> - </Route> -</Routes> -``` - -Now: - -- If I go to `/users/3`, the path matches `<Users/>` and `<UserProfile/>`. -- If I go to `/users`, the path matches `<Users/>` and `<NoUser/>`. - -When I use nested routes, in other words, each **path** can match multiple **routes**: each URL can render the views provided by multiple `<Route/>` components, at the same time, on the same page. - -This may be counter-intuitive, but it’s very powerful, for reasons you’ll hopefully see in a few minutes. - -## Why Nested Routing? - -Why bother with this? - -Most web applications contain levels of navigation that correspond to different parts of the layout. For example, in an email app you might have a URL like `/contacts/greg`, which shows a list of contacts on the left of the screen, and contact details for Greg on the right of the screen. The contact list and the contact details should always appear on the screen at the same time. If there’s no contact selected, maybe you want to show a little instructional text. - -You can easily define this with nested routes - -```rust -<Routes> - <Route path="/contacts" view=ContactList> - <Route path=":id" view=ContactInfo/> - <Route path="" view=|| view! { - <p>"Select a contact to view more info."</p> - }/> - </Route> -</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! { - <p>"Select a contact to view more info."</p> - }/> - </Route> -</Routes> -``` - -> The main page of the [Remix website](https://remix.run/), a React framework from the creators of React Router, has a great visual example if you scroll down, with three levels of nested routing: Sales > Invoices > an invoice. - -## `<Outlet/>` - -Parent routes do not automatically render their nested routes. After all, they are just components; they don’t know exactly where they should render their children, and “just stick it at the end of the parent component” is not a great answer. - -Instead, you tell a parent component where to render any nested components with an `<Outlet/>` component. The `<Outlet/>` simply renders one of two things: - -- if there is no nested route that has been matched, it shows nothing -- if there is a nested route that has been matched, it shows its `view` - -That’s all! But it’s important to know and to remember, because it’s a common source of “Why isn’t this working?” frustration. If you don’t provide an `<Outlet/>`, the nested route won’t be displayed. - -```rust -#[component] -pub fn ContactList() -> impl IntoView { - let contacts = todo!(); - - view! { - <div style="display: flex"> - // the contact list - <For each=contacts - key=|contact| contact.id - children=|contact| todo!() - /> - // the nested child, if any - // don’t forget this! - <Outlet/> - </div> - } -} -``` - -## Refactoring Route Definitions - -You don’t need to define all your routes in one place if you don’t want to. You can refactor any `<Route/>` and its children out into a separate component. - -For example, you can refactor the example above to use two separate components: - -```rust -#[component] -fn App() -> impl IntoView { - view! { - <Router> - <Routes> - <Route path="/contacts" view=ContactList> - <ContactInfoRoutes/> - <Route path="" view=|| view! { - <p>"Select a contact to view more info."</p> - }/> - </Route> - </Routes> - </Router> - } -} - -#[component(transparent)] -fn ContactInfoRoutes() -> impl IntoView { - view! { - <Route path=":id" view=ContactInfo> - <Route path="" view=EmailAndPhone/> - <Route path="address" view=Address/> - <Route path="messages" view=Messages/> - </Route> - } -} -``` - -This second component is a `#[component(transparent)]`, meaning it just returns its data, not a view: in this case, it's a [`RouteDefinition`](https://docs.rs/leptos_router/latest/leptos_router/struct.RouteDefinition.html) struct, which is what the `<Route/>` returns. As long as it is marked `#[component(transparent)]`, this sub-route can be defined wherever you want, and inserted as a component into your tree of route definitions. - -## Nested Routing and Performance - -All of this is nice, conceptually, but again—what’s the big deal? - -Performance. - -In a fine-grained reactive library like Leptos, it’s always important to do the least amount of rendering work you can. Because we’re working with real DOM nodes and not diffing a virtual DOM, we want to “rerender” components as infrequently as possible. Nested routing makes this extremely easy. - -Imagine my contact list example. If I navigate from Greg to Alice to Bob and back to Greg, the contact information needs to change on each navigation. But the `<ContactList/>` should never be rerendered. Not only does this save on rendering performance, it also maintains state in the UI. For example, if I have a search bar at the top of `<ContactList/>`, navigating from Greg to Alice to Bob won’t clear the search. - -In fact, in this case, we don’t even need to rerender the `<Contact/>` component when moving between contacts. The router will just reactively update the `:id` parameter as we navigate, allowing us to make fine-grained updates. As we navigate between contacts, we’ll update single text nodes to change the contact’s name, address, and so on, without doing _any_ additional rerendering. - -> This sandbox includes a couple features (like nested routing) discussed in this section and the previous one, and a couple we’ll cover in the rest of this chapter. The router is such an integrated system that it makes sense to provide a single example, so don’t be surprised if there’s anything you don’t understand. - -[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/16-router-0-5-4xp4zz?file=%2Fsrc%2Fmain.rs%3A102%2C2) - -<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> - -<details> -<summary>CodeSandbox Source</summary> - -```rust -use leptos::*; -use leptos_router::*; - -#[component] -fn App() -> impl IntoView { - view! { - <Router> - <h1>"Contact App"</h1> - // this <nav> will show on every routes, - // because it's outside the <Routes/> - // 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> - // / just has an un-nested "Home" - <Route path="/" view=|| view! { - <h3>"Home"</h3> - }/> - // /contacts has nested routes - <Route - path="/contacts" - view=ContactList - > - // if no id specified, fall back - <Route path=":id" view=ContactInfo> - <Route path="" view=|| view! { - <div class="tab"> - "(Contact Info)" - </div> - }/> - <Route path="conversations" view=|| view! { - <div class="tab"> - "(Conversations)" - </div> - }/> - </Route> - // if no id specified, fall back - <Route path="" view=|| view! { - <div class="select-user"> - "Select a user to view contact info." - </div> - }/> - </Route> - </Routes> - </main> - </Router> - } -} - -#[component] -fn ContactList() -> impl IntoView { - view! { - <div class="contact-list"> - // here's our contact list component itself - <div class="contact-list-contacts"> - <h3>"Contacts"</h3> - <A href="alice">"Alice"</A> - <A href="bob">"Bob"</A> - <A href="steve">"Steve"</A> - </div> - - // <Outlet/> will show the nested child route - // we can position this outlet wherever we want - // within the layout - <Outlet/> - </div> - } -} - -#[component] -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()); - - // imagine we're loading data from an API here - let name = move || match id().as_str() { - "alice" => "Alice", - "bob" => "Bob", - "steve" => "Steve", - _ => "User not found.", - }; - - view! { - <div class="contact-info"> - <h4>{name}</h4> - <div class="tabs"> - <A href="" exact=true>"Contact Info"</A> - <A href="conversations">"Conversations"</A> - </div> - - // <Outlet/> here is the tabs that are nested - // underneath the /contacts/:id route - <Outlet/> - </div> - } -} - -fn main() { - leptos::mount_to_body(App) -} -``` - -</details> -</preview> +<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/router/17_nested_routing.html"> +<link rel="canonical" href="https://book.leptos.dev/router/17_nested_routing.html"> diff --git a/docs/book/src/router/18_params_and_queries.md b/docs/book/src/router/18_params_and_queries.md index bd64eb436d..71c0abdd8e 100644 --- a/docs/book/src/router/18_params_and_queries.md +++ b/docs/book/src/router/18_params_and_queries.md @@ -1,202 +1,2 @@ -# Params and Queries - -Static paths are useful for distinguishing between different pages, but almost every application wants to pass data through the URL at some point. - -There are two ways you can do this: - -1. named route **params** like `id` in `/users/:id` -2. named route **queries** like `q` in `/search?q=Foo` - -Because of the way URLs are built, you can access the query from _any_ `<Route/>` view. You can access route params from the `<Route/>` that defines them or any of its nested children. - -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_query_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. - -> `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. - -```rust -use leptos::*; -use leptos_router::*; - -#[derive(Params)] -struct ContactParams { - id: usize -} - -#[derive(Params)] -struct ContactSearch { - q: 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. -> -> 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`. - -Now we can use them in a component. Imagine a URL that has both params and a query, like `/contacts/:id?q=Search`. - -The typed versions return `Memo<Result<T, _>>`. It’s a Memo so it reacts to changes in the URL. It’s a `Result` because the params or query need to be parsed from the URL, and may or may not be valid. - -```rust -let params = use_params::<ContactParams>(); -let query = use_query::<ContactSearch>(); - -// id: || -> usize -let id = move || { - params.with(|params| { - params - .map(|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>`. - -```rust -let params = use_params_map(); -let query = use_query_map(); - -// id: || -> Option<String> -let id = move || { - params.with(|params| params.get("id").cloned()) -}; -``` - -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: - -1. It’s correct, i.e., it forces you to consider the cases, “What if the user doesn’t pass a value for this query field? What if they pass an invalid value?” -2. It’s performant. Specifically, when you navigate between different paths that match the same `<Route/>` with only params or the query changing, you can get fine-grained updates to different parts of your app without rerendering. For example, navigating between different contacts in our contact-list example does a targeted update to the name field (and eventually contact info) without needing to replace or rerender the wrapping `<Contact/>`. This is what fine-grained reactivity is for. - -> This is the same example from the previous section. The router is such an integrated system that it makes sense to provide a single example highlighting multiple features, even if we haven’t explained them all yet. - -[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/16-router-0-5-4xp4zz?file=%2Fsrc%2Fmain.rs%3A102%2C2) - -<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> - -<details> -<summary>CodeSandbox Source</summary> - -```rust -use leptos::*; -use leptos_router::*; - -#[component] -fn App() -> impl IntoView { - view! { - <Router> - <h1>"Contact App"</h1> - // this <nav> will show on every routes, - // because it's outside the <Routes/> - // 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> - // / just has an un-nested "Home" - <Route path="/" view=|| view! { - <h3>"Home"</h3> - }/> - // /contacts has nested routes - <Route - path="/contacts" - view=ContactList - > - // if no id specified, fall back - <Route path=":id" view=ContactInfo> - <Route path="" view=|| view! { - <div class="tab"> - "(Contact Info)" - </div> - }/> - <Route path="conversations" view=|| view! { - <div class="tab"> - "(Conversations)" - </div> - }/> - </Route> - // if no id specified, fall back - <Route path="" view=|| view! { - <div class="select-user"> - "Select a user to view contact info." - </div> - }/> - </Route> - </Routes> - </main> - </Router> - } -} - -#[component] -fn ContactList() -> impl IntoView { - view! { - <div class="contact-list"> - // here's our contact list component itself - <div class="contact-list-contacts"> - <h3>"Contacts"</h3> - <A href="alice">"Alice"</A> - <A href="bob">"Bob"</A> - <A href="steve">"Steve"</A> - </div> - - // <Outlet/> will show the nested child route - // we can position this outlet wherever we want - // within the layout - <Outlet/> - </div> - } -} - -#[component] -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()); - - // imagine we're loading data from an API here - let name = move || match id().as_str() { - "alice" => "Alice", - "bob" => "Bob", - "steve" => "Steve", - _ => "User not found.", - }; - - view! { - <div class="contact-info"> - <h4>{name}</h4> - <div class="tabs"> - <A href="" exact=true>"Contact Info"</A> - <A href="conversations">"Conversations"</A> - </div> - - // <Outlet/> here is the tabs that are nested - // underneath the /contacts/:id route - <Outlet/> - </div> - } -} - -fn main() { - leptos::mount_to_body(App) -} -``` - -</details> -</preview> +<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/router/18_params_and_queries.html"> +<link rel="canonical" href="https://book.leptos.dev/router/18_params_and_queries.html"> diff --git a/docs/book/src/router/19_a.md b/docs/book/src/router/19_a.md index ea384387e3..3d77b0a0a9 100644 --- a/docs/book/src/router/19_a.md +++ b/docs/book/src/router/19_a.md @@ -1,155 +1,2 @@ -# The `<A/>` Component - -Client-side navigation works perfectly fine with ordinary HTML `<a>` elements. The router adds a listener that handles every click on a `<a>` element and tries to handle it on the client side, i.e., without doing another round trip to the server to request HTML. This is what enables the snappy “single-page app” navigations you’re probably familiar with from most modern web apps. - -The router will bail out of handling an `<a>` click under a number of situations - -- the click event has had `prevent_default()` called on it -- the <kbd>Meta</kbd>, <kbd>Alt</kbd>, <kbd>Ctrl</kbd>, or <kbd>Shift</kbd> keys were held during click -- the `<a>` has a `target` or `download` attribute, or `rel="external"` -- the link has a different origin from the current location - -In other words, the router will only try to do a client-side navigation when it’s pretty sure it can handle it, and it will upgrade every `<a>` element to get this special behavior. - -> 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: - -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. - -## Navigating Programmatically - -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. - -```rust -let navigate = leptos_router::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. - -> 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. - -[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/16-router-0-5-4xp4zz?file=%2Fsrc%2Fmain.rs%3A102%2C2) - -<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> - -<details> -<summary>CodeSandbox Source</summary> - -```rust -use leptos::*; -use leptos_router::*; - -#[component] -fn App() -> impl IntoView { - view! { - <Router> - <h1>"Contact App"</h1> - // this <nav> will show on every routes, - // because it's outside the <Routes/> - // 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> - // / just has an un-nested "Home" - <Route path="/" view=|| view! { - <h3>"Home"</h3> - }/> - // /contacts has nested routes - <Route - path="/contacts" - view=ContactList - > - // if no id specified, fall back - <Route path=":id" view=ContactInfo> - <Route path="" view=|| view! { - <div class="tab"> - "(Contact Info)" - </div> - }/> - <Route path="conversations" view=|| view! { - <div class="tab"> - "(Conversations)" - </div> - }/> - </Route> - // if no id specified, fall back - <Route path="" view=|| view! { - <div class="select-user"> - "Select a user to view contact info." - </div> - }/> - </Route> - </Routes> - </main> - </Router> - } -} - -#[component] -fn ContactList() -> impl IntoView { - view! { - <div class="contact-list"> - // here's our contact list component itself - <div class="contact-list-contacts"> - <h3>"Contacts"</h3> - <A href="alice">"Alice"</A> - <A href="bob">"Bob"</A> - <A href="steve">"Steve"</A> - </div> - - // <Outlet/> will show the nested child route - // we can position this outlet wherever we want - // within the layout - <Outlet/> - </div> - } -} - -#[component] -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()); - - // imagine we're loading data from an API here - let name = move || match id().as_str() { - "alice" => "Alice", - "bob" => "Bob", - "steve" => "Steve", - _ => "User not found.", - }; - - view! { - <div class="contact-info"> - <h4>{name}</h4> - <div class="tabs"> - <A href="" exact=true>"Contact Info"</A> - <A href="conversations">"Conversations"</A> - </div> - - // <Outlet/> here is the tabs that are nested - // underneath the /contacts/:id route - <Outlet/> - </div> - } -} - -fn main() { - leptos::mount_to_body(App) -} -``` - -</details> -</preview> +<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/router/19_a.html"> +<link rel="canonical" href="https://book.leptos.dev/router/19_a.html"> diff --git a/docs/book/src/router/20_form.md b/docs/book/src/router/20_form.md index 58e2b4ed41..775079a734 100644 --- a/docs/book/src/router/20_form.md +++ b/docs/book/src/router/20_form.md @@ -1,180 +1,2 @@ -# The `<Form/>` Component - -Links and forms sometimes seem completely unrelated. But, in fact, they work in very similar ways. - -In plain HTML, there are three ways to navigate to another page: - -1. An `<a>` element that links to another page: Navigates to the URL in its `href` attribute with the `GET` HTTP method. -2. A `<form method="GET">`: Navigates to the URL in its `action` attribute with the `GET` HTTP method and the form data from its inputs encoded in the URL query string. -3. A `<form method="POST">`: Navigates to the URL in its `action` attribute with the `POST` HTTP method and the form data from its inputs encoded in the body of the request. - -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. - -`<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. - -For example, imagine that you want to create a search field that updates search results in real time as the user searches, without a page reload, but that also stores the search in the URL so a user can copy and paste it to share results with someone else. - -It turns out that the patterns we’ve learned so far make this easy to implement. - -```rust -async fn fetch_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> - } -} -``` - -Whenever you click `Submit`, the `<Form/>` will “navigate” to `?q={search}`. But because this navigation is done on the client side, there’s no page flicker or reload. The URL query string changes, which triggers `search` to update. Because `search` is the source signal for the `search_results` resource, this triggers `search_results` to reload its resource. The `<Transition/>` continues displaying the current search results until the new ones have loaded. When they are complete, it switches to displaying the new result. - -This is a great pattern. The data flow is extremely clear: all data flows from the URL to the resource into the UI. The current state of the application is stored in the URL, which means you can refresh the page or text the link to a friend and it will show exactly what you’re expecting. And once we introduce server rendering, this pattern will prove to be really fault-tolerant, too: because it uses a `<form>` element and URLs under the hood, it actually works really well without even loading your WASM on the client. - -We can actually take it a step further and do something kind of clever: - -```rust -view! { - <Form method="GET" action=""> - <input type="search" name="q" value=search - oninput="this.form.requestSubmit()" - /> - </Form> -} -``` - -You’ll notice that this version drops the `Submit` button. Instead, we add an `oninput` attribute to the input. Note that this is _not_ `on:input`, which would listen for the `input` event and run some Rust code. Without the colon, `oninput` is the plain HTML attribute. So the string is actually a JavaScript string. `this.form` gives us the form the input is attached to. `requestSubmit()` fires the `submit` event on the `<form>`, which is caught by `<Form/>` just as if we had clicked a `Submit` button. Now the form will “navigate” on every keystroke or input to keep the URL (and therefore the search) perfectly in sync with the user’s input as they type. - -[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/20-form-0-5-9g7v9p?file=%2Fsrc%2Fmain.rs%3A1%2C1) - -<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> - -<details> -<summary>CodeSandbox Source</summary> - -```rust -use leptos::*; -use leptos_router::*; - -#[component] -fn App() -> impl IntoView { - view! { - <Router> - <h1><code>"<Form/>"</code></h1> - <main> - <Routes> - <Route path="" view=FormExample/> - </Routes> - </main> - </Router> - } -} - -#[component] -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(); - - view! { - // read out the URL query strings - <table> - <tr> - <td><code>"name"</code></td> - <td>{name}</td> - </tr> - <tr> - <td><code>"number"</code></td> - <td>{number}</td> - </tr> - <tr> - <td><code>"select"</code></td> - <td>{select}</td> - </tr> - </table> - // <Form/> will navigate whenever submitted - <h2>"Manual Submission"</h2> - <Form method="GET" action=""> - // input names determine query string key - <input type="text" name="name" value=name/> - <input type="number" name="number" value=number/> - <select name="select"> - // `selected` will set which starts as selected - <option selected=move || select() == "A"> - "A" - </option> - <option selected=move || select() == "B"> - "B" - </option> - <option selected=move || select() == "C"> - "C" - </option> - </select> - // submitting should cause a client-side - // navigation, not a full reload - <input type="submit"/> - </Form> - // This <Form/> uses some JavaScript to submit - // on every input - <h2>"Automatic Submission"</h2> - <Form method="GET" action=""> - <input - type="text" - name="name" - value=name - // this oninput attribute will cause the - // form to submit on every input to the field - oninput="this.form.requestSubmit()" - /> - <input - type="number" - name="number" - value=number - oninput="this.form.requestSubmit()" - /> - <select name="select" - onchange="this.form.requestSubmit()" - > - <option selected=move || select() == "A"> - "A" - </option> - <option selected=move || select() == "B"> - "B" - </option> - <option selected=move || select() == "C"> - "C" - </option> - </select> - // submitting should cause a client-side - // navigation, not a full reload - <input type="submit"/> - </Form> - } -} - -fn main() { - leptos::mount_to_body(App) -} -``` - -</details> -</preview> +<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/router/20_form.html"> +<link rel="canonical" href="https://book.leptos.dev/router/20_form.html"> diff --git a/docs/book/src/router/README.md b/docs/book/src/router/README.md index 60d84900cd..594feff4d7 100644 --- a/docs/book/src/router/README.md +++ b/docs/book/src/router/README.md @@ -1,23 +1,2 @@ -# Routing - -## The Basics - -Routing drives most websites. A router is the answer to the question, “Given this URL, what should appear on the page?” - -A URL consists of many parts. For example, the URL `https://my-cool-blog.com/blog/search?q=Search#results` consists of - -- a _scheme_: `https` -- a _domain_: `my-cool-blog.com` -- a **path**: `/blog/search` -- a **query** (or **search**): `?q=Search` -- a _hash_: `#results` - -The Leptos Router works with the path and query (`/blog/search?q=Search`). Given this piece of the URL, what should the app render on the page? - -## The Philosophy - -In most cases, the path should drive what is displayed on the page. From the user’s perspective, for most applications, most major changes in the state of the app should be reflected in the URL. If you copy and paste the URL and open it in another tab, you should find yourself more or less in the same place. - -In this sense, the router is really at the heart of the global state management for your application. More than anything else, it drives what is displayed on the page. - -The router handles most of this work for you by mapping the current location to particular components. +<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/router/index.html"> +<link rel="canonical" href="https://book.leptos.dev/router/index.html"> diff --git a/docs/book/src/server/25_server_functions.md b/docs/book/src/server/25_server_functions.md index 924eec2fad..d5150bc781 100644 --- a/docs/book/src/server/25_server_functions.md +++ b/docs/book/src/server/25_server_functions.md @@ -1,150 +1,2 @@ -# Server Functions - -If you’re creating anything beyond a toy app, you’ll need to run code on the server all the time: reading from or writing to a database that only runs on the server, running expensive computations using libraries you don’t want to ship down to the client, accessing APIs that need to be called from the server rather than the client for CORS reasons or because you need a secret API key that’s stored on the server and definitely shouldn’t be shipped down to a user’s browser. - -Traditionally, this is done by separating your server and client code, and by setting up something like a REST API or GraphQL API to allow your client to fetch and mutate data on the server. This is fine, but it requires you to write and maintain your code in multiple separate places (client-side code for fetching, server-side functions to run), as well as creating a third thing to manage, which is the API contract between the two. - -Leptos is one of a number of modern frameworks that introduce the concept of **server functions**. Server functions have two key characteristics: - -1. Server functions are **co-located** with your component code, so that you can organize your work by feature, not by technology. For example, you might have a “dark mode” feature that should persist a user’s dark/light mode preference across sessions, and be applied during server rendering so there’s no flicker. This requires a component that needs to be interactive on the client, and some work to be done on the server (setting a cookie, maybe even storing a user in a database.) Traditionally, this feature might end up being split between two different locations in your code, one in your “frontend” and one in your “backend.” With server functions, you’ll probably just write them both in one `dark_mode.rs` and forget about it. -2. Server functions are **isomorphic**, i.e., they can be called either from the server or the browser. This is done by generating code differently for the two platforms. On the server, a server function simply runs. In the browser, the server function’s body is replaced with a stub that actually makes a fetch request to the server, serializing the arguments into the request and deserializing the return value from the response. But on either end, the function can simply be called: you can create an `add_todo` function that writes to your database, and simply call it from a click handler on a button in the browser! - -## Using Server Functions - -Actually, I kind of like that example. What would it look like? It’s pretty simple, actually. - -```rust -// todo.rs - -#[server(AddTodo, "/api")] -pub async fn add_todo(title: String) -> Result<(), ServerFnError> { - let mut conn = db().await?; - - match sqlx::query("INSERT INTO todos (title, completed) VALUES ($1, false)") - .bind(title) - .execute(&mut conn) - .await - { - Ok(_row) => Ok(()), - Err(e) => Err(ServerFnError::ServerError(e.to_string())), - } -} - -#[component] -pub fn BusyButton() -> impl IntoView { - view! { - <button on:click=move |_| { - spawn_local(async { - add_todo("So much to do!".to_string()).await; - }); - }> - "Add Todo" - </button> - } -} -``` - -You’ll notice a couple things here right away: - -- Server functions can use server-only dependencies, like `sqlx`, and can access server-only resources, like our database. -- Server functions are `async`. Even if they only did synchronous work on the server, the function signature would still need to be `async`, because calling them from the browser _must_ be asynchronous. -- Server functions return `Result<T, ServerFnError>`. Again, even if they only do infallible work on the server, this is true, because `ServerFnError`’s variants include the various things that can be wrong during the process of making a network request. -- Server functions can be called from the client. Take a look at our click handler. This is code that will _only ever_ run on the client. But it can call the function `add_todo` (using `spawn_local` to run the `Future`) as if it were an ordinary async function: - -```rust -move |_| { - spawn_local(async { - add_todo("So much to do!".to_string()).await; - }); -} -``` - -- 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. - -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. (See examples for [Actix](https://github.com/leptos-rs/leptos/blob/main/examples/todo_app_sqlite/src/main.rs#L44) and [Axum](https://github.com/leptos-rs/leptos/blob/598523cd9d0d775b017cb721e41ebae9349f01e2/examples/todo_app_sqlite_axum/src/main.rs#L51).) -- 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 Function URL Prefixes - -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: - -```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")] -``` - -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)? - -**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! - -> **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 suported 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. - - -## 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 it’s return value is just a JSON or similar blob. You should _never_ return something sensitive from a server function. - -## 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: - -- 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. -- Create **actions** that call the server function to mutate data on the server - -The final section of this book will make this a little more concrete by introducing patterns that use progressively-enhanced HTML forms to run these server actions. - -But in the next few chapters, we’ll actually take a look at some of the details of what you might want to do with your server functions, including the best ways to integrate with the powerful extractors provided by the Actix and Axum server frameworks. +<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/server/25_server_functions.html"> +<link rel="canonical" href="https://book.leptos.dev/server/25_server_functions.html"> diff --git a/docs/book/src/server/26_extractors.md b/docs/book/src/server/26_extractors.md index c854202921..3663b45e61 100644 --- a/docs/book/src/server/26_extractors.md +++ b/docs/book/src/server/26_extractors.md @@ -1,87 +1,2 @@ -# Extractors - -The server functions we looked at in the last chapter showed how to run code on the server, and integrate it with the user interface you’re rendering in the browser. But they didn’t show you much about how to actually use your server to its full potential. - -## Server Frameworks - -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. - -> 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. - -## Using Extractors - -Both Actix and Axum handlers are built on the same powerful idea of **extractors**. Extractors “extract” typed data from an HTTP request, allowing you to access server-specific data easily. - -Leptos provides `extract` helper functions to let you use these extractors directly in your server functions, with a convenient syntax very similar to handlers for each framework. - -### 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. - -```rust - -#[server(ActixExtract, "/api")] -pub async fn actix_extract() -> Result<String, ServerFnError> { - use leptos_actix::extract; - use actix_web::dev::ConnectionInfo; - use actix_web::web::{Data, Query}; - - extract( - |search: Query<Search>, connection: ConnectionInfo| async move { - format!( - "search = {}\nconnection = {:?}", - search.q, - connection - ) - }, - ) - .await -} -``` - -## Axum Extractors - -The syntax for the [`leptos_axum::extract`](https://docs.rs/leptos_axum/latest/leptos_axum/fn.extract.html) function is very similar. (**Note**: This is available on the git main branch, but has not been released as of writing.) Note that Axum extractors return a `Result`, so you’ll need to add something to handle the error case. - -```rust -#[server(AxumExtract, "/api")] -pub async fn axum_extract() -> Result<String, ServerFnError> { - use axum::{extract::Query, http::Method}; - use leptos_axum::extract; - - extract(|method: Method, res: Query<MyQuery>| async move { - format!("{method:?} and {}", res.q) - }, - ) - .await - .map_err(|e| ServerFnError::ServerError("Could not extract method and query...".to_string())) -} -``` - -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. - -```rust -use axum::extract::FromRef; - -/// Derive FromRef to allow multiple items in state, using Axum’s -/// SubStates pattern. -#[derive(FromRef, Debug, Clone)] -pub struct AppState{ - pub leptos_options: LeptosOptions, - pub pool: SqlitePool -} -``` - -[Click here for an example of providing context in custom handlers](https://github.com/leptos-rs/leptos/blob/19ea6fae6aec2a493d79cc86612622d219e6eebb/examples/session_auth_axum/src/main.rs#L24-L44). - -## A Note about Data-Loading Patterns - -Because Actix and (especially) Axum are built on the idea of a single round-trip HTTP request and response, you typically run extractors near the “top” of your application (i.e., before you start rendering) and use the extracted data to determine how that should be rendered. Before you render a `<button>`, you load all the data your app could need. And any given route handler needs to know all the data that will need to be extracted by that route. - -But Leptos integrates both the client and the server, and it’s important to be able to refresh small pieces of your UI with new data from the server without forcing a full reload of all the data. So Leptos likes to push data loading “down” in your application, as far towards the leaves of your user interface as possible. When you click a `<button>`, it can refresh just the data it needs. This is exactly what server functions are for: they give you granular access to data to be loaded and reloaded. - -The `extract()` functions let you combine both models by using extractors in your server functions. You get access to the full power of route extractors, while decentralizing knowledge of what needs to be extracted down to your individual components. This makes it easier to refactor and reorganize routes: you don’t need to specify all the data a route needs up front. +<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/server/26_extractors.html"> +<link rel="canonical" href="https://book.leptos.dev/server/26_extractors.html"> diff --git a/docs/book/src/server/27_response.md b/docs/book/src/server/27_response.md index 05094fb74c..bef12c9e02 100644 --- a/docs/book/src/server/27_response.md +++ b/docs/book/src/server/27_response.md @@ -1,73 +1,2 @@ -# 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)). - -## `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)] -pub async fn tea_and_cookies() -> Result<(), ServerFnError> { - use actix_web::{cookie::Cookie, http::header, http::header::HeaderValue}; - use leptos_actix::ResponseOptions; - - // pull ResponseOptions from context - let response = expect_context::<ResponseOptions>(); - - // 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()) { - res.insert_header(header::SET_COOKIE, cookie); - } -} -``` - -## `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). - -```rust -#[server(Login, "/api")] -pub async fn login( - username: String, - password: String, - remember: Option<String>, -) -> Result<(), ServerFnError> { - // pull the DB pool and auth provider from context - let pool = pool()?; - let auth = auth()?; - - // 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 - match verify(password, &user.password)? { - // if the password is correct... - true => { - // log the user in - auth.login_user(user.id); - auth.remember_user(remember.is_some()); - - // and redirect to the home page - leptos_axum::redirect("/"); - Ok(()) - } - // if not, return an error - false => Err(ServerFnError::ServerError( - "Password does not match.".to_string(), - )), - } -} -``` - -This server function can then be used from your application. This `redirect` works well with the progressively-enhanced `<ActionForm/>` component: without JS/WASM, the server response will redirect because of the status code and header. With JS/WASM, the `<ActionForm/>` will detect the redirect in the server function response, and use client-side navigation to redirect to the new page. +<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/server/27_response.html"> +<link rel="canonical" href="https://book.leptos.dev/server/27_response.html"> diff --git a/docs/book/src/server/README.md b/docs/book/src/server/README.md index fea5ead693..5d8722b7d6 100644 --- a/docs/book/src/server/README.md +++ b/docs/book/src/server/README.md @@ -1,11 +1,2 @@ -# Working with the Server - -The previous section described the process of server-side rendering, using the server to generate an HTML version of the page that will become interactive in the browser. So far, everything has been “isomorphic”; in other words, your app has had the “same (_iso_) shape (_morphe_)” on the client and the server. - -But a server can do a lot more than just render HTML! In fact, a server can do a whole bunch of things your browser _can’t,_ like reading from and writing to a SQL database. - -If you’re used to building JavaScript frontend apps, you’re probably used to calling out to some kind of REST API to do this sort of server work. If you’re used to building sites with PHP or Python or Ruby (or Java or C# or...), this server-side work is your bread and butter, and it’s the client-side interactivity that tends to be an afterthought. - -With Leptos, you can do both: not only in the same language, not only sharing the same types, but even in the same files! - -This section will talk about how to build the uniquely-server-side parts of your application. +<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/server/index.html"> +<link rel="canonical" href="https://book.leptos.dev/server/index.html"> diff --git a/docs/book/src/ssr/21_cargo_leptos.md b/docs/book/src/ssr/21_cargo_leptos.md index 05d6676e7f..bed28b96bb 100644 --- a/docs/book/src/ssr/21_cargo_leptos.md +++ b/docs/book/src/ssr/21_cargo_leptos.md @@ -1,37 +1,2 @@ -# Introducing `cargo-leptos` - -So far, we’ve just been running code in the browser and using Trunk to coordinate the build process and run a local development process. If we’re going to add server-side rendering, we’ll need to run our application code on the server as well. This means we’ll need to build two separate binaries, one compiled to native code and running the server, the other compiled to WebAssembly (WASM) and running in the user’s browser. Additionally, the server needs to know how to serve this WASM version (and the JavaScript required to initialize it) to the browser. - -This is not an insurmountable task but it adds some complication. For convenience and an easier developer experience, we built the [`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos) build tool. `cargo-leptos` basically exists to coordinate the build process for your app, handling recompiling the server and client halves when you make changes, and adding some built-in support for things like Tailwind, SASS, and testing. - -Getting started is pretty easy. Just run - -```bash -cargo install cargo-leptos -``` - -And then to create a new project, you can run either - -```bash -# for an Actix template -cargo leptos new --git leptos-rs/start -``` - -or - -```bash -# for an Axum template -cargo leptos new --git leptos-rs/start-axum -``` - -Now `cd` into the directory you’ve created and run - -```bash -cargo leptos watch -``` - -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). - -But what exactly is happening when you open our browser to `localhost:3000`? Well, read on to find out. +<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/ssr/21_cargo_leptos.html"> +<link rel="canonical" href="https://book.leptos.dev/ssr/21_cargo_leptos.html"> diff --git a/docs/book/src/ssr/22_life_cycle.md b/docs/book/src/ssr/22_life_cycle.md index 0d20d9b8f4..365916aad0 100644 --- a/docs/book/src/ssr/22_life_cycle.md +++ b/docs/book/src/ssr/22_life_cycle.md @@ -1,43 +1,2 @@ -# The Life of a Page Load - -Before we get into the weeds it might be helpful to have a higher-level overview. What exactly happens between the moment you type in the URL of a server-rendered Leptos app, and the moment you click a button and a counter increases? - -I’m assuming some basic knowledge of how the Internet works here, and won’t get into the weeds about HTTP or whatever. Instead, I’ll try to show how different parts of the Leptos APIs map onto each part of the process. - -This description also starts from the premise that your app is being compiled for two separate targets: - -1. A server version, often running on Actix or Axum, compiled with the Leptos `ssr` feature -2. A browser version, compiled to WebAssembly (WASM) with the Leptos `hydrate` feature - -The [`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos) build tool exists to coordinate the process of compiling your app for these two different targets. - -## 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 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. - -> The HTML page that’s returned is essentially your app, “dehydrated” or “freeze-dried”: it is HTML without any of the reactivity or event listeners you’ve added. The browser will “rehydrate” this HTML page by adding the reactive system and attaching event listeners to that server-rendered HTML. Hence the two feature flags that apply to the two halves of this process: `ssr` on the server for “server-side rendering”, and `hydrate` in the browser for that process of rehydration. - -## In the Browser - -- The browser receives this HTML page from the server. It immediately goes back to the server to begin loading the JS and WASM necessary to run the interactive, client side version of the app. -- In the meantime, it renders the HTML version. -- When the WASM version has reloaded, it does the same route-matching process that the server did. Because the `<Routes/>` component is identical on the server and in the client, the browser version will read the URL and render the same page that was already returned by the server. -- During this initial “hydration” phase, the WASM version of your app doesn’t re-create the DOM nodes that make up your application. Instead, it walks over the existing HTML tree, “picking up” existing elements and adding the necessary interactivity. - -> Note that there are some trade-offs here. Before this hydration process is complete, the page will _appear_ interactive but won’t actually respond to interactions. For example, if you have a counter button and click it before WASM has loaded, the count will not increment, because the necessary event listeners and reactivity have not been added yet. We’ll look at some ways to build in “graceful degradation” in future chapters. - -## Client-Side Navigation - -The next step is very important. Imagine that the user now clicks a link to navigate to another page in your application. - -The browser will _not_ make another round trip to the server, reloading the full page as it would for navigating between plain HTML pages or an application that uses server rendering (for example with PHP) but without a client-side half. - -Instead, the WASM version of your app will load the new page, right there in the browser, without requesting another page from the server. Essentially, your app upgrades itself from a server-loaded “multi-page app” into a browser-rendered “single-page app.” This yields the best of both worlds: a fast initial load time due to the server-rendered HTML, and fast secondary navigations because of the client-side routing. - -Some of what will be described in the following chapters—like the interactions between server functions, resources, and `<Suspense/>`—may seem overly complicated. You might find yourself asking, “If my page is being rendered to HTML on the server, why can’t I just `.await` this on the server? If I can just call library X in a server function, why can’t I call it in my component?” The reason is pretty simple: to enable the upgrade from server rendering to client rendering, everything in your application must be able to run either on the server or in the browser. - -This is not the only way to create a website or web framework, of course. But it’s the most common way, and we happen to think it’s quite a good way, to create the smoothest possible experience for your users. +<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/ssr/22_life_cycle.html"> +<link rel="canonical" href="https://book.leptos.dev/ssr/22_life_cycle.html"> diff --git a/docs/book/src/ssr/23_ssr_modes.md b/docs/book/src/ssr/23_ssr_modes.md index 4979eea0ea..f1d8a23a02 100644 --- a/docs/book/src/ssr/23_ssr_modes.md +++ b/docs/book/src/ssr/23_ssr_modes.md @@ -1,156 +1,2 @@ -# Async Rendering and SSR “Modes” - -Server-rendering a page that uses only synchronous data is pretty simple: You just walk down the component tree, rendering each element to an HTML string. But this is a pretty big caveat: it doesn’t answer the question of what we should do with pages that includes asynchronous data, i.e., the sort of stuff that would be rendered under a `<Suspense/>` node on the client. - -When a page loads async data that it needs to render, what should we do? Should we wait for all the async data to load, and then render everything at once? (Let’s call this “async” rendering) Should we go all the way in the opposite direction, just sending the HTML we have immediately down to the client and letting the client load the resources and fill them in? (Let’s call this “synchronous” rendering) Or is there some middle-ground solution that somehow beats them both? (Hint: There is.) - -If you’ve ever listened to streaming music or watched a video online, I’m sure you realize that HTTP supports streaming, allowing a single connection to send chunks of data one after another without waiting for the full content to load. You may not realize that browsers are also really good at rendering partial HTML pages. Taken together, this means that you can actually enhance your users’ experience by **streaming HTML**: and this is something that Leptos supports out of the box, with no configuration at all. And there’s actually more than one way to stream HTML: you can stream the chunks of HTML that make up your page in order, like frames of a video, or you can stream them... well, out of order. - -Let me say a little more about what I mean. - -Leptos supports all the major ways of rendering HTML that includes asynchronous data: - -1. [Synchronous Rendering](#synchronous-rendering) -1. [Async Rendering](#async-rendering) -1. [In-Order streaming](#in-order-streaming) -1. [Out-of-Order Streaming](#out-of-order-streaming) (and a partially-blocked variant) - -## Synchronous Rendering - -1. **Synchronous**: Serve an HTML shell that includes `fallback` for any `<Suspense/>`. Load data on the client using `create_local_resource`, replacing `fallback` once resources are loaded. - -- _Pros_: App shell appears very quickly: great TTFB (time to first byte). -- _Cons_ - - Resources load relatively slowly; you need to wait for JS + WASM to load before even making a request. - - No ability to include data from async resources in the `<title>` or other `<meta>` tags, hurting SEO and things like social media link previews. - -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. - -## Async Rendering - -<video controls> - <source src="https://github.com/leptos-rs/leptos/blob/main/docs/video/async.mov?raw=true" type="video/mp4"> -</video> - -2. **`async`**: Load all resources on the server. Wait until all data are loaded, and render HTML in one sweep. - -- _Pros_: Better handling for meta tags (because you know async data even before you render the `<head>`). Faster complete load than **synchronous** because async resources begin loading on server. -- _Cons_: Slower load time/TTFB: you need to wait for all async resources to load before displaying anything on the client. The page is totally blank until everything is loaded. - -## In-Order Streaming - -<video controls> - <source src="https://github.com/leptos-rs/leptos/blob/main/docs/video/in-order.mov?raw=true" type="video/mp4"> -</video> - -3. **In-order streaming**: Walk through the component tree, rendering HTML until you hit a `<Suspense/>`. Send down all the HTML you’ve got so far as a chunk in the stream, wait for all the resources accessed under the `<Suspense/>` to load, then render it to HTML and keep walking until you hit another `<Suspense/>` or the end of the page. - -- _Pros_: Rather than a blank screen, shows at least _something_ before the data are ready. -- _Cons_ - - Loads the shell more slowly than synchronous rendering (or out-of-order streaming) because it needs to pause at every `<Suspense/>`. - - Unable to show fallback states for `<Suspense/>`. - - Can’t begin hydration until the entire page has loaded, so earlier pieces of the page will not be interactive until the suspended chunks have loaded. - -## Out-of-Order Streaming - -<video controls> - <source src="https://github.com/leptos-rs/leptos/blob/main/docs/video/out-of-order.mov?raw=true" type="video/mp4"> -</video> - -4. **Out-of-order streaming**: Like synchronous rendering, serve an HTML shell that includes `fallback` for any `<Suspense/>`. But load data on the **server**, streaming it down to the client as it resolves, and streaming down HTML for `<Suspense/>` nodes, which is swapped in to replace the fallback. - -- _Pros_: Combines the best of **synchronous** and **`async`**. - - Fast initial response/TTFB because it immediately sends the whole synchronous shell - - Fast total time because resources begin loading on the server. - - Able to show the fallback loading state and dynamically replace it, instead of showing blank sections for un-loaded data. -- _Cons_: Requires JavaScript to be enabled for suspended fragments to appear in correct order. (This small chunk of JS streamed down in a `<script>` tag alongside the `<template>` tag that contains the rendered `<Suspense/>` fragment, so it does not need to load any additional JS files.) - -5. **Partially-blocked streaming**: “Partially-blocked” streaming is useful when you have multiple separate `<Suspense/>` components on the page. It is triggered by setting `ssr=SsrMode::PartiallyBlocked` on a route, and depending on blocking resources within the view. If one of the `<Suspense/>` components reads from one or more “blocking resources” (see below), the fallback will not be sent; rather, the server will wait until that `<Suspense/>` has resolved and then replace the fallback with the resolved fragment on the server, which means that it is included in the initial HTML response and appears even if JavaScript is disabled or not supported. Other `<Suspense/>` stream in out of order, similar to the `SsrMode::OutOfOrder` default. - -This is useful when you have multiple `<Suspense/>` on the page, and one is more important than the other: think of a blog post and comments, or product information and reviews. It is _not_ useful if there’s only one `<Suspense/>`, or if every `<Suspense/>` reads from blocking resources. In those cases it is a slower form of `async` rendering. - -- _Pros_: Works if JavaScript is disabled or not supported on the user’s device. -- _Cons_ - - Slower initial response time than out-of-order. - - Marginally overall response due to additional work on the server. - - No fallback state shown. - -## Using SSR Modes - -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> - // We’ll load the home page with out-of-order streaming and <Suspense/> - <Route 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" - view=Post - ssr=SsrMode::Async - /> -</Routes> -``` - -For a path that includes multiple nested routes, the most restrictive mode will be used: i.e., if even a single nested route asks for `async` rendering, the whole initial request will be rendered `async`. `async` is the most restricted requirement, followed by in-order, and then out-of-order. (This probably makes sense if you think about it for a few minutes.) - -## 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. - -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. - -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. - -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 comment_data = create_resource(/* load blog post */); - 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 comment data here */ - </Suspense> - } -} -``` - -The first `<Suspense/>`, with the body of the blog post, will block my HTML stream, because it reads from a blocking resource. Meta tags and other head elements awaiting the blocking resource will be rendered before the stream is sent. - -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> - // We’ll load the home page with out-of-order streaming and <Suspense/> - <Route 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" - view=Post - ssr=SsrMode::PartiallyBlocked - /> -</Routes> -``` - -The second `<Suspense/>`, with the comments, will not block the stream. Blocking resources gave me exactly the power and granularity I needed to optimize my page for SEO and user experience. +<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/ssr/23_ssr_modes.html"> +<link rel="canonical" href="https://book.leptos.dev/ssr/23_ssr_modes.html"> diff --git a/docs/book/src/ssr/24_hydration_bugs.md b/docs/book/src/ssr/24_hydration_bugs.md index c6a8c89b58..c044c304e0 100644 --- a/docs/book/src/ssr/24_hydration_bugs.md +++ b/docs/book/src/ssr/24_hydration_bugs.md @@ -1,161 +1,2 @@ -# Hydration Bugs _(and how to avoid them)_ - -## A Thought Experiment - -Let’s try an experiment to test your intuitions. Open up an app you’re server-rendering with `cargo-leptos`. (If you’ve just been using `trunk` so far to play with examples, go [clone a `cargo-leptos` template](./21_cargo_leptos.md) just for the sake of this exercise.) - -Put a log somewhere in your root component. (I usually call mine `<App/>`, but anything will do.) - -```rust -#[component] -pub fn App() -> impl IntoView { - logging::log!("where do I run?"); - // ... whatever -} -``` - -And let’s fire it up - -```bash -cargo leptos watch -``` - -Where do you expect `where do I run?` to log? - -- In the command line where you’re running the server? -- In the browser console when you load the page? -- Neither? -- Both? - -Try it out. - -... - -... - -... - -Okay, consider the spoiler alerted. - -You’ll notice of course that it logs in both places, assuming everything goes according to plan. In fact on the server it logs twice—first during the initial server startup, when Leptos renders your app once to extract the route tree, then a second time when you make a request. Each time you reload the page, `where do I run?` should log once on the server and once on the client. - -If you think about the description in the last couple sections, hopefully this makes sense. Your application runs once on the server, where it builds up a tree of HTML which is sent to the client. During this initial render, `where do I run?` logs on the server. - -Once the WASM binary has loaded in the browser, your application runs a second time, walking over the same user interface tree and adding interactivity. - -> Does that sound like a waste? It is, in a sense. But reducing that waste is a genuinely hard problem. It’s what some JS frameworks like Qwik are intended to solve, although it’s probably too early to tell whether it’s a net performance gain as opposed to other approaches. - -## The Potential for Bugs - -Okay, hopefully all of that made sense. But what does it have to do with the title of this chapter, which is “Hydration bugs (and how to avoid them)”? - -Remember that the application needs to run on both the server and the client. This generates a few different sets of potential issues you need to know how to avoid. - -### Mismatches between server and client code - -One way to create a bug is by creating a mismatch between the HTML that’s sent down by the server and what’s rendered on the client. It’s actually fairly hard to do this unintentionally, I think (at least judging by the bug reports I get from people.) But imagine I do something like this - -```rust -#[component] -pub fn App() -> impl IntoView { - let data = if cfg!(target_arch = "wasm32") { - vec![0, 1, 2] - } else { - vec![] - }; - data.into_iter() - .map(|value| view! { <span>{value}</span> }) - .collect_view() -} -``` - -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: - -``` -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 -``` - -The WASM version of your app, running in the browser, expects to find three items; but the HTML has none. - -#### Solution - -It’s pretty rare that you do this intentionally, but it could happen from somehow running different logic on the server and in the browser. If you’re seeing warnings like this and you don’t think it’s your fault, it’s much more likely that it’s a bug with `<Suspense/>` or something. Feel free to go ahead and open an [issue](https://github.com/leptos-rs/leptos/issues) or [discussion](https://github.com/leptos-rs/leptos/discussions) on GitHub for help. - -#### Solution - -You can simply tell the effect to wait a tick before updating the signal, by using something like `request_animation_frame`, which will set a short timeout and then update the signal before the next frame. - -```rust -create_effect(move |_| { - // do something like reading from localStorage - request_animation_frame(move || set_loaded(true)); -}); -``` - -This allows the browser to hydrate with the correct, matching state (`loaded` is `false` when it reaches the view), then immediately update it to `true` once hydration is complete. - -### Not all client code can run on the server - -Imagine you happily import a dependency like `gloo-net` that you’ve been used to using to make requests in the browser, and use it in a `create_resource` in a server-rendered app. - -You’ll probably instantly see the dreaded message - -``` -panicked at 'cannot call wasm-bindgen imported functions on non-wasm targets' -``` - -Uh-oh. - -But of course this makes sense. We’ve just said that your app needs to run on the client and the server. - -#### Solution - -There are a few ways to avoid this: - -1. Only use libraries that can run on both the server and the client. `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. - -For example, say that I want to store something in the browser’s `localStorage` whenever a signal changes. - -```rust -#[component] -pub fn App() -> impl IntoView { - use gloo_storage::Storage; - let storage = gloo_storage::LocalStorage::raw(); - logging::log!("{storage:?}"); -} -``` - -This panics because I can’t access `LocalStorage` during server rendering. - -But if I wrap it in an effect... - -```rust -#[component] -pub fn App() -> impl IntoView { - use gloo_storage::Storage; - create_effect(move |_| { - let storage = gloo_storage::LocalStorage::raw(); - logging::log!("{storage:?}"); - }); -} -``` - -It’s fine! This will render appropriately on the server, ignoring the client-only code, and then access the storage and log a message on the browser. - -### Not all server code can run on the client - -WebAssembly running in the browser is a pretty limited environment. You don’t have access to a file-system or to many of the other things the standard library may be used to having. Not every crate can even be compiled to WASM, let alone run in a WASM environment. - -In particular, you’ll sometimes see errors about the crate `mio` or missing things from `core`. This is generally a sign that you are trying to compile something to WASM that can’t be compiled to WASM. If you’re adding server-only dependencies, you’ll want to mark them `optional = true` in your `Cargo.toml` and then enable them in the `ssr` feature definition. (Check out one of the template `Cargo.toml` files to see more details.) - -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).) +<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/ssr/24_hydration_bugs.html"> +<link rel="canonical" href="https://book.leptos.dev/ssr/24_hydration_bugs.html"> diff --git a/docs/book/src/ssr/README.md b/docs/book/src/ssr/README.md index 7fae30a5f7..2ca5189421 100644 --- a/docs/book/src/ssr/README.md +++ b/docs/book/src/ssr/README.md @@ -1,17 +1,2 @@ -# 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. - - -```admonish info - -When working with Leptos on the server side, you're free to choose either the Actix-web or the Axum integrations - the full feature set of Leptos is available with either option. - -If, however, you need deploy to a WinterCG-compatible runtime like Deno, Cloudflare, etc., then choose the Axum integration as this deployment option is only available with Axum on the server. Lastly, if you'd like to go full-stack WASM/WASI and deploy to WASM-based serverless runtimes, then Axum is your go-to choice here too. - -NB: this is a limitation of the web frameworks themselves, not Leptos. - -``` \ No newline at end of file +<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/ssr/index.html"> +<link rel="canonical" href="https://book.leptos.dev/ssr/index.html"> diff --git a/docs/book/src/testing.md b/docs/book/src/testing.md index 7e2efbc923..c024ee7998 100644 --- a/docs/book/src/testing.md +++ b/docs/book/src/testing.md @@ -1,219 +1,2 @@ -# Testing Your Components - -Testing user interfaces can be relatively tricky, but really important. This article -will discuss a couple principles and approaches for testing a Leptos app. - -## 1. Test business logic with ordinary Rust tests - -In many cases, it makes sense to pull the logic out of your components and test -it separately. For some simple components, there’s no particular logic to test, but -for many it’s worth using a testable wrapping type and implementing the logic in -ordinary Rust `impl` blocks. - -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 { /* ... */ }]); - // ⚠️ 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() - }); -} -``` - -You could pull that logic out into a separate data structure and test it: - -```rust -pub struct Todos(Vec<Todo>); - -impl Todos { - pub fn num_remaining(&self) -> usize { - self.0.iter().filter(|todo| !todo.completed).sum() - } -} - -#[cfg(test)] -mod tests { - #[test] - fn test_remaining() { - // ... - } -} - -#[component] -pub fn TodoApp() -> impl IntoView { - let (todos, set_todos) = create_signal(Todos(vec![Todo { /* ... */ }])); - // ✅ this has a test associated with it - let num_remaining = move || todos.with(Todos::num_remaining); -} -``` - -In general, the less of your logic is wrapped into your components themselves, the -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/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: - -### `wasm-bindgen-test` with [`counter`](https://github.com/leptos-rs/leptos/blob/main/examples/counter/tests/web.rs) - -This is a fairly simple manual testing setup that uses the [`wasm-pack test`](https://rustwasm.github.io/wasm-pack/book/commands/test.html) command. - -#### Sample Test - -````rust -#[wasm_bindgen_test] -fn clear() { - let document = leptos::document(); - let test_wrapper = document.create_element("section").unwrap(); - let _ = document.body().unwrap().append_child(&test_wrapper); - - mount_to( - test_wrapper.clone().unchecked_into(), - || view! { <SimpleCounter initial_value=10 step=1/> }, - ); - - let div = test_wrapper.query_selector("div").unwrap().unwrap(); - let clear = test_wrapper - .query_selector("button") - .unwrap() - .unwrap() - .unchecked_into::<web_sys::HtmlElement>(); - - clear.click(); - -assert_eq!( - div.outer_html(), - // here we spawn a mini reactive system to render the test case - run_scope(create_runtime(), || { - // it's as if we're creating it with a value of 0, right? - let (value, set_value) = create_signal(0); - - // we can remove the event listeners because they're not rendered to HTML - view! { - <div> - <button>"Clear"</button> - <button>"-1"</button> - <span>"Value: " {value} "!"</span> - <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() - .outer_html() - }) -); -} -```` - -### [`wasm-bindgen-test` with `counters_stable`](https://github.com/leptos-rs/leptos/tree/main/examples/counters_stable/tests/web) - -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); -} -``` - -### [Playwright with `counters_stable`](https://github.com/leptos-rs/leptos/tree/main/examples/counters_stable/e2e) - -These tests use the common JavaScript testing tool Playwright to run end-to-end tests on the same example, using a library and testing approach familiar to may who have done frontend development before. - -#### 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); - await ui.goto(); - await ui.addCounter(); - - await ui.incrementCount(); - await ui.incrementCount(); - await ui.incrementCount(); - - await expect(ui.total).toHaveText("3"); - }); -}); -``` - -### [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. - -``` -@add_todo -Feature: Add Todo - - Background: - Given I see the app - - @add_todo-see - Scenario: Should see the todo - Given I set the todo as Buy Bread - When I click the Add button - Then I see the todo named Buy Bread - - # @allow.skipped - @add_todo-style - Scenario: Should see the pending todo - When I add a todo as Buy Oranges - Then I see the pending todo -``` - -The definitions for these actions are defined in Rust code. - -```rust -use crate::fixtures::{action, world::AppWorld}; -use anyhow::{Ok, Result}; -use cucumber::{given, when}; - -#[given("I see the app")] -#[when("I open the app")] -async fn i_open_the_app(world: &mut AppWorld) -> Result<()> { - let client = &world.client; - action::goto_path(client, "").await?; - - Ok(()) -} - -#[given(regex = "^I add a todo as (.*)$")] -#[when(regex = "^I add a todo as (.*)$")] -async fn i_add_a_todo_titled(world: &mut AppWorld, text: String) -> Result<()> { - let client = &world.client; - action::add_todo(client, text.as_str()).await?; - - Ok(()) -} - -// etc. -``` - -### Learning More - -Feel free to check out the CI setup in the Leptos repo to learn more about how to use these tools in your own application. All of these testing methods are run regularly against actual Leptos example apps. +<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/testing.html"> +<link rel="canonical" href="https://book.leptos.dev/testing.html"> diff --git a/docs/book/src/view/01_basic_component.md b/docs/book/src/view/01_basic_component.md index 1f0c39a676..24fc564e1f 100644 --- a/docs/book/src/view/01_basic_component.md +++ b/docs/book/src/view/01_basic_component.md @@ -1,225 +1,2 @@ -# A Basic Component - -That “Hello, world!” was a _very_ simple example. Let’s move on to something a -little more like an ordinary app. - -First, let’s edit the `main` function so that, instead of rendering the whole -app, it just renders an `<App/>` component. Components are the basic unit of -composition and design in most web frameworks, and Leptos is no exception. -Conceptually, they are similar to HTML elements: they represent a section of the -DOM, with self-contained, defined behavior. Unlike HTML elements, they are in -`PascalCase`, so most Leptos applications will start with something like an -`<App/>` component. - -```rust -fn main() { - leptos::mount_to_body(|| view! { <App/> }) -} -``` - -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 -#[component] -fn App() -> impl IntoView { - let (count, set_count) = create_signal(0); - - view! { - <button - on:click=move |_| { - set_count(3); - } - > - "Click me: " - {move || count.get()} - </button> - } -} -``` - -## 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 -used as a component in your Leptos application. We’ll see some of the other features of -this macro in a couple chapters. - -```rust -fn App() -> impl IntoView -``` - -Every component is a function with the following characteristics - -1. It takes zero or more arguments of any type. -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. - -## The Component Body - -The body of the component function is a set-up function that runs once, not a -render function that reruns multiple times. You’ll typically use it to create a -few reactive variables, define any side effects that run in response to those values -changing, and describe the user interface. - -```rust -let (count, set_count) = create_signal(0); -``` - -[`create_signal`](https://docs.rs/leptos/latest/leptos/fn.create_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(...)`). - -> `.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. - -## The View - -Leptos defines user interfaces using a JSX-like format via the [`view`](https://docs.rs/leptos/latest/leptos/macro.view.html) macro. - -```rust -view! { - <button - // define an event listener with on: - on:click=move |_| { - // on stable, this is set_count.set(3); - set_count(3); - } - > - // text nodes are wrapped in quotation marks - "Click me: " - // blocks can include Rust code - {move || count.get()} - </button> -} -``` - -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... - -```rust -{move || count.get()} -``` - -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.” - -When we click the button and call `set_count`, the `count` signal is updated. This -`move || count.get()` closure, whose value depends on the value of `count`, reruns, -and the framework makes a targeted update to that one 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: - -```rust -view! { - <button /* ... */> - "Click me: " - // identical to {move || count.get()} - {count} - </button> -} -``` - -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! - -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”: - -```rust -move |_| { - set_count.update(|n| *n += 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. - -> Throughout this tutorial, we’ll use CodeSandbox to show interactive examples. To -> show the browser in the sandbox, you may need to click `Add DevTools > -Other Previews > 8080.` Hover over any of the variables to show Rust-Analyzer details -> and docs for what’s going on. Feel free to fork the examples to play with them yourself! - -[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/1-basic-component-3d74p3?file=%2Fsrc%2Fmain.rs%3A1%2C1) - -<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> - -<details> -<summary>CodeSandbox Source</summary> - -```rust -use leptos::*; - -// The #[component] macro marks a function as a reusable component -// Components are the building blocks of your user interface -// They define a reusable unit of behavior -#[component] -fn App() -> impl IntoView { - // here we create a reactive signal - // 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); - - // the `view` macro is how we define the user interface - // it uses an HTML-like format that can accept certain Rust values - view! { - <button - // on:click will run whenever the `click` event fires - // every event handler is defined as `on:{eventname}` - - // 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); - } - > - // text nodes in RSX should be wrapped in quotes, - // like a normal Rust string - "Click me" - </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.get()} - </p> - <p> - <strong>"Reactive shorthand: "</strong> - // signals are functions, so we can remove the wrapping closure - {count} - </p> - <p> - <strong>"Not reactive: "</strong> - // NOTE: if you write {count()}, this will *not* be reactive - // it simply gets the value of count once - {count()} - </p> - } -} - -// This `main` function is the entry point into the app -// It just mounts our component to the <body> -// 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/> }) -} -``` +<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/view/01_basic_component.html"> +<link rel="canonical" href="https://book.leptos.dev/view/01_basic_component.html"> diff --git a/docs/book/src/view/02_dynamic_attributes.md b/docs/book/src/view/02_dynamic_attributes.md index c095b6d706..dda0c8d440 100644 --- a/docs/book/src/view/02_dynamic_attributes.md +++ b/docs/book/src/view/02_dynamic_attributes.md @@ -1,262 +1,2 @@ -# `view`: Dynamic Classes, Styles and Attributes - -So far we’ve seen how to use the `view` macro to create event listeners and to -create dynamic text by passing a function (such as a signal) into the view. - -But of course there are other things you might want to update in your user interface. -In this section, we’ll look at how to update classes, styles and attributes dynamically, -and we’ll introduce the concept of a **derived signal**. - -Let’s start with a simple component that should be familiar: click a button to -increment a counter. - -```rust -#[component] -fn App() -> impl IntoView { - let (count, set_count) = create_signal(0); - - view! { - <button - on:click=move |_| { - set_count.update(|n| *n += 1); - } - > - "Click me: " - {move || count()} - </button> - } -} -``` - -So far, this is just the example from the last chapter. - -## Dynamic Classes - -Now let’s say I’d like to update the list of CSS classes on this element dynamically. -For example, let’s say I want to add the class `red` when the count is odd. I can -do this using the `class:` syntax. - -```rust -class:red=move || count() % 2 == 1 -``` - -`class:` attributes take - -1. the class name, following the colon (`red`) -2. a value, which can be a `bool` or a function that returns a `bool` - -When the value is `true`, the class is added. When the value is `false`, the class -is removed. And if the value is a function that accesses a signal, the class will -reactively update when the signal changes. - -Now every time I click the button, the text should toggle between red and black as -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) -``` - -> If you’re following along, make sure you go into your `index.html` and add something like this: -> -> ```html -> <style> -> .red { -> color: red; -> } -> </style> -> ``` - -## Dynamic Styles - -Individual CSS properties can be directly updated with a similar `style:` syntax. - -```rust -let (x, set_x) = create_signal(0); -let (y, set_y) = create_signal(0); -view! { - <div - style="position: absolute" - style:left=move || format!("{}px", x() + 100) - style:top=move || format!("{}px", y() + 100) - style:background-color=move || format!("rgb({}, {}, 100)", x(), y()) - style=("--columns", x) - > - "Moves when coordinates change" - </div> -} -``` - -## Dynamic Attributes - -The same applies to plain attributes. Passing a plain string or primitive value to -an attribute gives it a static value. Passing a function (including a signal) to -an attribute causes it to update its value reactively. Let’s add another element -to our view: - -```rust -<progress - max="50" - // signals are functions, so this <=> `move || count.get()` - value=count -/> -``` - -Now every time we set the count, not only will the `class` of the `<button>` be -toggled, but the `value` of the `<progress>` bar will increase, which means that -our progress bar will move forward. - -## Derived Signals - -Let’s go one layer deeper, just for fun. - -You already know that we create reactive interfaces just by passing functions into -the `view`. This means that we can easily change our progress bar. For example, -suppose we want it to move twice as fast: - -```rust -<progress - max="50" - value=move || count() * 2 -/> -``` - -But imagine we want to reuse that calculation in more than one place. You can do this -using a **derived signal**: a closure that accesses a signal. - -```rust -let double_count = move || count() * 2; - -/* insert the rest of the view */ -<progress - max="50" - // we use it once here - value=double_count -/> -<p> - "Double Count: " - // and again here - {double_count} -</p> -``` - -Derived signals let you create reactive computed values that can be used in multiple -places in your application with minimal overhead. - -Note: Using a derived signal like this means that the calculation runs once per -signal change (when `count()` changes) and once per place we access `double_count`; -in other words, twice. This is a very cheap calculation, so that’s fine. -We’ll look at memos in a later chapter, which re designed to solve this problem -for expensive calculations. - -> #### Advanced Topic: Injecting Raw HTML -> -> The `view` macro provides support for an additional attribute, `inner_html`, which -> can be used to directly set the HTML contents of any element, wiping out any other -> children you’ve given it. Note that this does _not_ escape the HTML you provide. You -> should make sure that it only contains trusted input or that any HTML entities are -> escaped, to prevent cross-site scripting (XSS) attacks. -> -> ```rust -> let html = "<p>This HTML will be injected.</p>"; -> view! { -> <div inner_html=html/> -> } -> ``` -> -> [Click here for the full `view` macros docs](https://docs.rs/leptos/latest/leptos/macro.view.html). - -[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/2-dynamic-attributes-0-5-lwdrpm?file=%2Fsrc%2Fmain.rs%3A1%2C1) - -<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> - -<details> -<summary>Code Sandbox Source</summary> - -```rust -use leptos::*; - -#[component] -fn App() -> impl IntoView { - let (count, set_count) = create_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; - - view! { - <button - on:click=move |_| { - set_count.update(|n| *n += 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 - > - "Click me" - </button> - // NOTE: self-closing tags like <br> need an explicit / - <br/> - - // We'll update this progress bar every time `count` changes - <progress - // static attributes work as in HTML - max="50" - - // passing a function to an attribute - // reactively sets that attribute - // signals are functions, so this <=> `move || count.get()` - value=count - > - </progress> - <br/> - - // This progress bar will use `double_count` - // so it should move twice as fast! - <progress - max="50" - // derived signals are functions, so they can also - // reactive update the DOM - value=double_count - > - </progress> - <p>"Count: " {count}</p> - <p>"Double Count: " {double_count}</p> - } -} - -fn main() { - leptos::mount_to_body(App) -} - - // passing a function to an attribute - // reactively sets that attribute - // signals are functions, so this <=> `move || count.get()` - value=count - > - </progress> - <br/> - - // This progress bar will use `double_count` - // so it should move twice as fast! - <progress - max="50" - // derived signals are functions, so they can also - // reactive update the DOM - value=double_count - > - </progress> - <p>"Count: " {count}</p> - <p>"Double Count: " {double_count}</p> - } -} - -fn main() { - leptos::mount_to_body(App) -} -``` - -</details> -</preview> +<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/view/02_dynamic_attributes.html"> +<link rel="canonical" href="https://book.leptos.dev/view/02_dynamic_attributes.html"> diff --git a/docs/book/src/view/03_components.md b/docs/book/src/view/03_components.md index 481e22c061..0986d04396 100644 --- a/docs/book/src/view/03_components.md +++ b/docs/book/src/view/03_components.md @@ -1,481 +1,2 @@ -# Components and Props - -So far, we’ve been building our whole application in a single component. This -is fine for really tiny examples, but in any real application you’ll need to -break the user interface out into multiple components, so you can break your -interface down into smaller, reusable, composable chunks. - -Let’s take our progress bar example. Imagine that you want two progress bars -instead of one: one that advances one tick per click, one that advances two ticks -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; - -view! { - <progress - max="50" - value=count - /> - <progress - max="50" - value=double_count - /> -} -``` - -But of course, this doesn’t scale very well. If you want to add a third progress -bar, you need to add this code another time. And if you want to edit anything -about it, you need to edit it in triplicate. - -Instead, let’s create a `<ProgressBar/>` component. - -```rust -#[component] -fn ProgressBar() -> impl IntoView { - view! { - <progress - max="50" - // hmm... where will we get this from? - value=progress - /> - } -} -``` - -There’s just one problem: `progress` is not defined. Where should it come from? -When we were defining everything manually, we just used the local variable names. -Now we need some way to pass an argument into the component. - -## Component Props - -We do this using component properties, or “props.” If you’ve used another frontend -framework, this is probably a familiar idea. Basically, properties are to components -as attributes are to HTML elements: they let you pass additional information into -the component. - -In Leptos, you define props by giving additional arguments to the component function. - -```rust -#[component] -fn ProgressBar( - progress: ReadSignal<i32> -) -> impl IntoView { - view! { - <progress - max="50" - // now this works - value=progress - /> - } -} -``` - -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); - view! { - <button on:click=move |_| { set_count.update(|n| *n += 1); }> - "Click me" - </button> - // now we use our component! - <ProgressBar progress=count/> - } -} -``` - -Using a component in the view looks a lot like using an HTML element. You’ll -notice that you can easily tell the difference between an element and a component -because components always have `PascalCase` names. You pass the `progress` prop -in as if it were an HTML element attribute. Simple. - -### Reactive and Static Props - -You’ll notice that throughout this example, `progress` takes a reactive -`ReadSignal<i32>`, and not a plain `i32`. This is **very important**. - -Component props have no special meaning attached to them. A component is simply -a function that runs once to set up the user interface. The only way to tell the -interface to respond to changing is to pass it a signal type. So if you have a -component property that will change over time, like our `progress`, it should -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)]`. - -```rust -#[component] -fn ProgressBar( - // mark this prop optional - // you can specify it or not when you use <ProgressBar/> - #[prop(optional)] - max: u16, - progress: ReadSignal<i32> -) -> impl IntoView { - view! { - <progress - max=max - value=progress - /> - } -} -``` - -Now, we can use `<ProgressBar max=50 value=count/>`, or we can omit `max` -to use the default value (i.e., `<ProgressBar value=count/>`). The default value -on an `optional` is its `Default::default()` value, which for a `u16` is going to -be `0`. In the case of a progress bar, a max value of `0` is not very useful. - -So let’s give it a particular default value instead. - -### `default` props - -You can specify a default value other than `Default::default()` pretty simply -with `#[prop(default = ...)`. - -```rust -#[component] -fn ProgressBar( - #[prop(default = 100)] - max: u16, - progress: ReadSignal<i32> -) -> impl IntoView { - view! { - <progress - max=max - value=progress - /> - } -} -``` - -### Generic Props - -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 -#[component] -fn App() -> impl IntoView { - let (count, set_count) = create_signal(0); - let double_count = move || count() * 2; - - view! { - <button on:click=move |_| { set_count.update(|n| *n += 1); }> - "Click me" - </button> - <ProgressBar progress=count/> - // add a second progress bar - <ProgressBar progress=double_count/> - } -} -``` - -Hm... this won’t compile. It should be pretty easy to understand why: we’ve declared -that the `progress` prop takes `ReadSignal<i32>`, and `double_count` is not -`ReadSignal<i32>`. As rust-analyzer will tell you, its type is `|| -> i32`, i.e., -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 -implement the trait `Fn() -> i32`. So you could use a generic component: - -```rust -#[component] -fn ProgressBar<F>( - #[prop(default = 100)] - max: u16, - progress: F -) -> impl IntoView -where - F: Fn() -> i32 + 'static, -{ - view! { - <progress - max=max - value=progress - /> - } -} -``` - -This is a perfectly reasonable way to write this component: `progress` now takes -any value that implements this `Fn()` trait. - -This generic can also be specified inline: - -```rust -#[component] -fn ProgressBar<F: Fn() -> i32 + 'static>( - #[prop(default = 100)] max: u16, - progress: F, -) -> impl IntoView { - view! { - <progress - max=max - value=progress - /> - } -} -``` - -> Note that generic component props _can’t_ be specified with an `impl` yet (`progress: impl Fn() -> i32 + 'static,`), in part because they’re actually used to generate a `struct ProgressBarProps`, and struct fields cannot be `impl` types. The `#[component]` macro may be further improved in the future to allow inline `impl` generic props. - -### `into` Props - -There’s one more way we could implement this, and it would be to use `#[prop(into)]`. -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. - -```rust -#[component] -fn ProgressBar( - #[prop(default = 100)] - max: u16, - #[prop(into)] - progress: Signal<i32> -) -> impl IntoView -{ - view! { - <progress - max=max - value=progress - /> - } -} - -#[component] -fn App() -> impl IntoView { - let (count, set_count) = create_signal(0); - let double_count = move || count() * 2; - - view! { - <button on:click=move |_| { set_count.update(|n| *n += 1); }> - "Click me" - </button> - // .into() converts `ReadSignal` to `Signal` - <ProgressBar progress=count/> - // use `Signal::derive()` to wrap a derived signal - <ProgressBar progress=Signal::derive(double_count)/> - } -} -``` - -### Optional Generic Props - -Note that you can’t specify optional generic props for a component. Let’s see what would happen if you try: - -```rust,compile_fail -#[component] -fn ProgressBar<F: Fn() -> i32 + 'static>( - #[prop(optional)] progress: Option<F>, -) -> impl IntoView { - progress.map(|progress| { - view! { - <progress - max=100 - value=progress - /> - } - }) -} - -#[component] -pub fn App() -> impl IntoView { - view! { - <ProgressBar/> - } -} -``` - -Rust helpfully gives the error - -``` -xx | <ProgressBar/> - | ^^^^^^^^^^^ cannot infer type of the type parameter `F` declared on the function `ProgressBar` - | -help: consider specifying the generic argument - | -xx | <ProgressBar::<F>/> - | +++++ -``` - -There are just two problems: - -1. Leptos’s view macro doesn’t support specifying a generic on a component with this turbofish syntax. -2. Even if you could, specifying the correct type here is not possible; closures and functions in general are unnameable types. The compiler can display them with a shorthand, but you can’t specify them. - -However, you can get around this by providing a concrete type using `Box<dyn _>` or `&dyn _`: - -```rust -#[component] -fn ProgressBar( - #[prop(optional)] progress: Option<Box<dyn Fn() -> i32>>, -) -> impl IntoView { - progress.map(|progress| { - view! { - <progress - max=100 - value=progress - /> - } - }) -} - -#[component] -pub fn App() -> impl IntoView { - view! { - <ProgressBar/> - } -} -``` - -Because the Rust compiler now knows the concrete type of the prop, and therefore its size in memory even in the `None` case, this compiles fine. - -> In this particular case, `&dyn Fn() -> i32` will cause lifetime issues, but in other cases, it may be a possibility. - -## Documenting Components - -This is one of the least essential but most important sections of this book. -It’s not strictly necessary to document your components and their props. It may -be very important, depending on the size of your team and your app. But it’s very -easy, and bears immediate fruit. - -To document a component and its props, you can simply add doc comments on the -component function, and each one of the props: - -```rust -/// Shows progress toward a goal. -#[component] -fn ProgressBar( - /// The maximum value of the progress bar. - #[prop(default = 100)] - max: u16, - /// How much progress should be displayed. - #[prop(into)] - progress: Signal<i32>, -) -> impl IntoView { - /* ... */ -} -``` - -That’s all you need to do. These behave like ordinary Rust doc comments, except -that you can document individual component props, which can’t be done with Rust -function arguments. - -This will automatically generate documentation for your component, its `Props` -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. - -[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/3-components-0-5-5vvl69?file=%2Fsrc%2Fmain.rs%3A1%2C1) - -<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> - -<details> -<summary>CodeSandbox Source</summary> - -```rust -use leptos::*; - -// Composing different components together is how we build -// user interfaces. Here, we'll define a resuable <ProgressBar/>. -// You'll see how doc comments can be used to document components -// and their properties. - -/// Shows progress toward a goal. -#[component] -fn ProgressBar( - // Marks this as an optional prop. It will default to the default - // value of its type, i.e., 0. - #[prop(default = 100)] - /// The maximum value of the progress bar. - max: u16, - // Will run `.into()` on the value passed into the prop. - #[prop(into)] - // `Signal<T>` is a wrapper for several reactive types. - // It can be helpful in component APIs like this, where we - // might want to take any kind of reactive value - /// How much progress should be displayed. - progress: Signal<i32>, -) -> impl IntoView { - view! { - <progress - max={max} - value=progress - /> - <br/> - } -} - -#[component] -fn App() -> impl IntoView { - let (count, set_count) = create_signal(0); - - let double_count = move || count() * 2; - - view! { - <button - on:click=move |_| { - set_count.update(|n| *n += 1); - } - > - "Click me" - </button> - <br/> - // If you have this open in CodeSandbox or an editor with - // rust-analyzer support, try hovering over `ProgressBar`, - // `max`, or `progress` to see the docs we defined above - <ProgressBar max=50 progress=count/> - // Let's use the default max value on this one - // the default is 100, so it should move half as fast - <ProgressBar progress=count/> - // Signal::derive creates a Signal wrapper from our derived signal - // using double_count means it should move twice as fast - <ProgressBar max=50 progress=Signal::derive(double_count)/> - } -} - -fn main() { - leptos::mount_to_body(App) -} -``` - -</details> -</preview> +<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/view/03_components.html"> +<link rel="canonical" href="https://book.leptos.dev/view/03_components.html"> diff --git a/docs/book/src/view/04_iteration.md b/docs/book/src/view/04_iteration.md index 4756500202..911e8a38a9 100644 --- a/docs/book/src/view/04_iteration.md +++ b/docs/book/src/view/04_iteration.md @@ -1,265 +1,2 @@ -# Iteration - -Whether you’re listing todos, displaying a table, or showing product images, -iterating over a list of items is a common task in web applications. Reconciling -the differences between changing sets of items can also be one of the trickiest -tasks for a framework to handle well. - -Leptos supports two different patterns for iterating over items: - -1. For static views: `Vec<_>` -2. For dynamic lists: `<For/>` - -## Static Views with `Vec<_>` - -Sometimes you need to show an item repeatedly, but the list you’re drawing from -does not often change. In this case, it’s important to know that you can insert -any `Vec<IV> where IV: IntoView` into your view. In other words, if you can render -`T`, you can render `Vec<T>`. - -```rust -let values = vec![0, 1, 2]; -view! { - // this will just render "012" - <p>{values.clone()}</p> - // or we can wrap them in <li> - <ul> - {values.into_iter() - .map(|n| view! { <li>{n}</li>}) - .collect::<Vec<_>>()} - </ul> -} -``` - -Leptos also provides a `.collect_view()` helper function that allows you to collect any iterator of `T: IntoView` into `Vec<View>`. - -```rust -let values = vec![0, 1, 2]; -view! { - // this will just render "012" - <p>{values.clone()}</p> - // or we can wrap them in <li> - <ul> - {values.into_iter() - .map(|n| view! { <li>{n}</li>}) - .collect_view()} - </ul> -} -``` - -The fact that the _list_ is static doesn’t mean the interface needs to be static. -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)); - -// each item manages a reactive view -// but the list itself will never change -let counter_buttons = counters - .map(|(count, set_count)| { - view! { - <li> - <button - on:click=move |_| set_count.update(|n| *n += 1) - > - {count} - </button> - </li> - } - }) - .collect_view(); - -view! { - <ul>{counter_buttons}</ul> -} -``` - -You _can_ render a `Fn() -> Vec<_>` reactively as well. But note that every time -it changes, this will rerender every item in the list. This is quite inefficient! -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 -keyed dynamic list. It takes three props: - -- `each`: a function (such as a signal) 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 - -`key` is, well, the key. You can add, remove, and move items within the list. As -long as each item’s key is stable over time, the framework does not need to rerender -any of the items, unless they are new additions, and it can very efficiently add, -remove, and move items as they change. This allows for extremely efficient updates -to the list as it changes, with minimal additional work. - -Creating a good `key` can be a little tricky. You generally do _not_ want to use -an index for this purpose, as it is not stable—if you remove or move items, their -indices change. - -But it’s a great idea to do something like generating a unique ID for each row as -it is generated, and using that as an ID for the key function. - -Check out the `<DynamicList/>` component below for an example. - -[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/4-iteration-0-5-pwdn2y?file=%2Fsrc%2Fmain.rs%3A1%2C1) - -<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> - -<details> -<summary>CodeSandbox Source</summary> - -```rust -use leptos::*; - -// Iteration is a very common task in most applications. -// So how do you take a list of data and render it in the DOM? -// This example will show you the two ways: -// 1) for mostly-static lists, using Rust iterators -// 2) for lists that grow, shrink, or move items, using <For/> - -#[component] -fn App() -> impl IntoView { - view! { - <h1>"Iteration"</h1> - <h2>"Static List"</h2> - <p>"Use this pattern if the list itself is static."</p> - <StaticList length=5/> - <h2>"Dynamic List"</h2> - <p>"Use this pattern if the rows in your list will change."</p> - <DynamicList initial_length=5/> - } -} - -/// A list of counters, without the ability -/// to add or remove any. -#[component] -fn StaticList( - /// How many counters to include in this list. - length: usize, -) -> impl IntoView { - // create counter signals that start at incrementing numbers - let counters = (1..=length).map(|idx| create_signal(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)| { - view! { - <li> - <button - on:click=move |_| set_count.update(|n| *n += 1) - > - {count} - </button> - </li> - } - }) - .collect::<Vec<_>>(); - - // Note that if `counter_buttons` were a reactive list - // and its value changed, this would be very inefficient: - // it would rerender every row every time the list changed. - view! { - <ul>{counter_buttons}</ul> - } -} - -/// A list of counters that allows you to add or -/// remove counters. -#[component] -fn DynamicList( - /// The number of counters to begin with. - initial_length: usize, -) -> impl IntoView { - // This dynamic list will use the <For/> component. - // <For/> is a keyed list. This means that each row - // has a defined key. If the key does not change, the row - // will not be re-rendered. When the list changes, only - // the minimum number of changes will be made to the DOM. - - // `next_counter_id` will let us generate unique IDs - // we do this by simply incrementing the ID by one - // each time we create a counter - let mut next_counter_id = initial_length; - - // we generate an initial list as in <StaticList/> - // but this time we include the ID along with the signal - let initial_counters = (0..initial_length) - .map(|id| (id, create_signal(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 add_counter = move |_| { - // create a signal for the new counter - let sig = create_signal(next_counter_id + 1); - // add this counter to the list of counters - set_counters.update(move |counters| { - // since `.update()` gives us `&mut T` - // we can just use normal Vec methods like `push` - counters.push((next_counter_id, sig)) - }); - // increment the ID so it's always unique - next_counter_id += 1; - }; - - view! { - <div> - <button on:click=add_counter> - "Add Counter" - </button> - <ul> - // The <For/> component is central here - // This allows for efficient, key list rendering - <For - // `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 - // 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 - // means their indices will change and they will all rerender - key=|counter| counter.0 - // `children` receives each item from your `each` iterator - // and returns a view - children=move |(id, (count, set_count))| { - view! { - <li> - <button - on:click=move |_| set_count.update(|n| *n += 1) - > - {count} - </button> - <button - on:click=move |_| { - set_counters.update(|counters| { - counters.retain(|(counter_id, _)| counter_id != &id) - }); - } - > - "Remove" - </button> - </li> - } - } - /> - </ul> - </div> - } -} - -fn main() { - leptos::mount_to_body(App) -} -``` - -</details> -</preview> +<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/view/04_iteration.html"> +<link rel="canonical" href="https://book.leptos.dev/view/04_iteration.html"> diff --git a/docs/book/src/view/04b_iteration.md b/docs/book/src/view/04b_iteration.md index 672892266d..f152025198 100644 --- a/docs/book/src/view/04b_iteration.md +++ b/docs/book/src/view/04b_iteration.md @@ -1,278 +1,2 @@ -# Iterating over More Complex Data with `<For/>` - -This chapter goes into iteration over nested data structures in a bit -more depth. It belongs here with the other chapter on iteration, but feel -free to skip it and come back if you’d like to stick with simpler subjects -for now. - -## The Problem - -I just said that the framework does not rerender any of the items in one of the -rows, unless the key has changed. This probably makes sense at first, but it can -easily trip you up. - -Let’s consider an example in which each of the items in our row is some data structure. -Imagine, for example, that the items come from some JSON array of keys and values: - -```rust -#[derive(Debug, Clone)] -struct DatabaseEntry { - key: String, - value: i32, -} -``` - -Let’s define a simple component that will iterate over the rows and display each one: - -```rust -#[component] -pub fn App() -> impl IntoView { - // start with a set of three rows - let (data, set_data) = create_signal(vec![ - DatabaseEntry { - key: "foo".to_string(), - value: 10, - }, - DatabaseEntry { - key: "bar".to_string(), - value: 20, - }, - DatabaseEntry { - key: "baz".to_string(), - value: 15, - }, - ]); - view! { - // 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()); - }> - "Update Values" - </button> - // iterate over the rows and display each value - <For - each=data - key=|state| state.key.clone() - let:child - > - <p>{child.value}</p> - </For> - } -} -``` - -> Note the `let:child` syntax here. In the previous chapter we introduced `<For/>` -> with a `children` prop. We can actually create this value directly in the children -> of the `<For/>` component, without breaking out of the `view` macro: the `let:child` -> combined with `<p>{child.value}</p>` above is the equivalent of -> -> ```rust -> children=|child| view! { <p>{child.value}</p> } -> ``` - -When you click the `Update Values` button... nothing happens. Or rather: -the signal is updated, the new value is logged, but the `{child.value}` -for each row doesn’t update. - -Let’s see: is that because we forgot to add a closure to make it reactive? -Let’s try `{move || child.value}`. - -...Nope. Still nothing. - -Here’s the problem: as I said, each row is only rerendered when the key changes. -We’ve updated the value for each row, but not the key for any of the rows, so -nothing has rerendered. And if you look at the type of `child.value`, it’s a plain -`i32`, not a reactive `ReadSignal<i32>` or something. This means that even if we -wrap a closure around it, the value in this row will never update. - -We have three possible solutions: - -1. change the `key` so that it always updates when the data structure changes -2. change the `value` so that it’s reactive -3. take a reactive slice of the data structure instead of using each row directly - -## Option 1: Change the Key - -Each row is only rerendered when the key changes. Our rows above didn’t rerender, -because the key didn’t change. So: why not just force the key to change? - -```rust -<For - each=data - key=|state| (state.key.clone(), state.value) - let:child -> - <p>{child.value}</p> -</For> -``` - -Now we include both the key and the value in the `key`. This means that whenever the -value of a row changes, `<For/>` will treat it as if it’s an entirely new row, and -replace the previous one. - -### Pros - -This is very easy. We can make it even easier by deriving `PartialEq`, `Eq`, and `Hash` -on `DatabaseEntry`, in which case we could just `key=|state| state.clone()`. - -### Cons - -**This is the least efficient of the three options.** Every time the value of a row -changes, it throws out the previous `<p>` element and replaces it with an entirely new -one. Rather than making a fine-grained update to the text node, in other words, it really -does rerender the entire row on every change, and this is expensive in proportion to how -complex the UI of the row is. - -You’ll notice we also end up cloning the whole data structure so that `<For/>` can hold -onto a copy of the key. For more complex structures, this can become a bad idea fast! - -## Option 2: Nested Signals - -If we do want that fine-grained reactivity for the value, one option is to wrap the `value` -of each row in a signal. - -```rust -#[derive(Debug, Clone)] -struct DatabaseEntry { - key: String, - value: RwSignal<i32>, -} -``` - -`RwSignal<_>` is a “read-write signal,” which combines the getter and setter in one object. -I’m using it here because it’s a little easier to store in a struct than separate getters -and setters. - -```rust -#[component] -pub fn App() -> impl IntoView { - // start with a set of three rows - let (data, set_data) = create_signal(vec![ - DatabaseEntry { - key: "foo".to_string(), - value: create_rw_signal(10), - }, - DatabaseEntry { - key: "bar".to_string(), - value: create_rw_signal(20), - }, - DatabaseEntry { - key: "baz".to_string(), - value: create_rw_signal(15), - }, - ]); - view! { - // 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()); - }> - "Update Values" - </button> - // iterate over the rows and display each value - <For - each=data - key=|state| state.key.clone() - let:child - > - <p>{child.value}</p> - </For> - } -} -``` - -This version works! And if you look in the DOM inspector in your browser, you’ll -see that unlike in the previous version, in this version only the individual text -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 -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 -own state, we don’t actual need to update the `data` signal at all, so the immutable -`.with()` 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/>` -> is useful if we want to add or remove rows in the future. - -### Pros - -This is the most efficient option, and fits directly with the rest of the mental model -of the framework: values that change over time are wrapped in signals so the interface -can respond to them. - -### Cons - -Nested reactivity can be cumbersome if you’re receiving data from an API or another -data source you don’t control, and you don’t want to create a different struct wrapping -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), -which creates a derived computation that only triggers a reactive update when its value -has changed. - -This allows you to create reactive values for subfields of a larger data structure, -without needing to wrap the fields of that structure in signals. - -Most of the application can remain the same as the initial (broken) version, but the `<For/>` -will be updated to this: - -```rust -<For - each=move || data().into_iter().enumerate() - key=|(_, state)| state.key.clone() - children=move |(index, _)| { - let value = create_memo(move |_| { - data.with(|data| data.get(index).map(|d| d.value).unwrap_or(0)) - }); - view! { - <p>{value}</p> - } - } -/> -``` - -You’ll notice a few differences here: - -- we convert the `data` signal into an enumerated iterator -- we use the `children` prop explicitly, to make it easier to run some non-`view` code -- we define a `value` memo and use that in the view. This `value` field doesn’t actually - use the `child` being passed into each row. Instead, it uses the index and reaches back - into the original `data` to get the value. - -Every time `data` changes, now, each memo will be recalculated. If its value has changed, -it will update its text node, without rerendering the whole row. - -### Pros - -We get the same fine-grained reactivity of the signal-wrapped version, without needing to -wrap the data in signals. - -### Cons - -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 -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.) - -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. +<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/view/04b_iteration.html"> +<link rel="canonical" href="https://book.leptos.dev/view/04b_iteration.html"> diff --git a/docs/book/src/view/05_forms.md b/docs/book/src/view/05_forms.md index 2b7ac218ed..4bdad1fe4c 100644 --- a/docs/book/src/view/05_forms.md +++ b/docs/book/src/view/05_forms.md @@ -1,250 +1,2 @@ -# Forms and Inputs - -Forms and form inputs are an important part of interactive apps. There are two -basic patterns for interacting with inputs in Leptos, which you may recognize -if you’re familiar with React, SolidJS, or a similar framework: using **controlled** -or **uncontrolled** inputs. - -## Controlled Inputs - -In a "controlled input," the framework controls the state of the input -element. On every `input` event, it updates a local signal that holds the current -state, which in turn updates the `value` prop of the input. - -There are two important things to remember: - -1. The `input` event fires on (almost) every change to the element, while the - `change` event fires (more or less) when you unfocus the input. You probably - want `on:input`, but we give you the freedom to choose. -2. The `value` _attribute_ only sets the initial value of the input, i.e., it - only updates the input up to the point that you begin typing. The `value` - _property_ continues updating the input after that. You usually want to set - `prop:value` for this reason. (The same is true for `checked` and `prop:checked` - on an `<input type="checkbox">`.) - -```rust -let (name, set_name) = create_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)); - } - - // the `prop:` syntax lets you update a DOM property, - // rather than an attribute. - prop:value=name - /> - <p>"Name is: " {name}</p> -} -``` - -> #### Why do you need `prop:value`? -> -> Web browsers are the most ubiquitous and stable platform for rendering graphical user interfaces in existence. They have also maintained an incredible backwards compatibility over their three decades of existence. Inevitably, this means there are some quirks. -> -> One odd quirk is that there is a distinction between HTML attributes and DOM element properties, i.e., between something called an “attribute” which is parsed from HTML and can be set on a DOM element with `.setAttribute()`, and something called a “property” which is a field of the JavaScript class representation of that parsed HTML element. -> -> In the case of an `<input value=...>`, setting the `value` _attribute_ is defined as setting the initial value for the input, and setting `value` _property_ sets its current value. It maybe easiest to understand this by opening `about:blank` and running the following JavaScript in the browser console, line by line: -> -> ```js -> // create an input and append it to the DOM -> const el = document.createElement("input"); -> document.body.appendChild(el); -> -> el.setAttribute("value", "test"); // updates the input -> el.setAttribute("value", "another test"); // updates the input again -> -> // now go and type into the input: delete some characters, etc. -> -> el.setAttribute("value", "one more time?"); -> // nothing should have changed. setting the "initial value" does nothing now -> -> // however... -> el.value = "But this works"; -> ``` -> -> Many other frontend frameworks conflate attributes and properties, or create a special case for inputs that sets the value correctly. Maybe Leptos should do this too; but for now, I prefer giving users the maximum amount of control over whether they’re setting an attribute or a property, and doing my best to educate people about the actual underlying browser behavior rather than obscuring it. - -## Uncontrolled Inputs - -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 -the input once when we want to get its value. - -In this example, we only notify the framework when the `<form>` fires a `submit` -event. - -```rust -let (name, set_name) = create_signal("Uncontrolled".to_string()); - -let input_element: NodeRef<Input> = create_node_ref(); -``` - -`NodeRef` is a kind of reactive smart pointer: we can use it to access the -underlying DOM node. Its value will be set when the element is rendered. - -```rust -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() - // event handlers can only fire after the view - // is mounted to the DOM, so the `NodeRef` will be `Some` - .expect("<input> to exist") - // `NodeRef` implements `Deref` for the DOM element type - // this means we can call`HtmlInputElement::value()` - // to get the current value of the input - .value(); - set_name(value); -}; -``` - -Our `on_submit` handler will access the input’s value and use it to call `set_name`. -To access the DOM node stored in the `NodeRef`, we can simply call it as a function -(or using `.get()`). This will return `Option<web_sys::HtmlInputElement>`, but we -know it will already have been filled when we rendered the view, so it’s safe to -unwrap here. - -We can then call `.value()` to get the value out of the input, because `NodeRef` -gives us access to a correctly-typed HTML element. - -```rust -view! { - <form on:submit=on_submit> - <input type="text" - value=name - node_ref=input_element - /> - <input type="submit" value="Submit"/> - </form> - <p>"Name is: " {name}</p> -} -``` - -The view should be pretty self-explanatory by now. Note two things: - -1. Unlike in the controlled input example, we use `value` (not `prop:value`). - This is because we’re just setting the initial value of the input, and letting - the browser control its state. (We could use `prop:value` instead.) -2. We use `node_ref` to fill the `NodeRef`. (Older examples sometimes use `_ref`. - They are the same thing, but `node_ref` has better rust-analyzer support.) - -[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/5-forms-0-5-rf2t7c?file=%2Fsrc%2Fmain.rs%3A1%2C1) - -<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> - -<details> -<summary>CodeSandbox Source</summary> - -```rust -use leptos::{ev::SubmitEvent, *}; - -#[component] -fn App() -> impl IntoView { - view! { - <h2>"Controlled Component"</h2> - <ControlledComponent/> - <h2>"Uncontrolled Component"</h2> - <UncontrolledComponent/> - } -} - -#[component] -fn ControlledComponent() -> impl IntoView { - // create a signal to hold the value - let (name, set_name) = create_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)); - } - - // the `prop:` syntax lets you update a DOM property, - // rather than an attribute. - // - // IMPORTANT: the `value` *attribute* only sets the - // initial value, until you have made a change. - // The `value` *property* sets the current value. - // This is a quirk of the DOM; I didn't invent it. - // Other frameworks gloss this over; I think it's - // more important to give you access to the browser - // as it really works. - // - // tl;dr: use prop:value for form inputs - prop:value=name - /> - <p>"Name is: " {name}</p> - } -} - -#[component] -fn UncontrolledComponent() -> impl IntoView { - // import the type for <input> - use leptos::html::Input; - - let (name, set_name) = create_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(); - - // fires when the form `submit` event happens - // this will store the value of the <input> in our signal - 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() - // event handlers can only fire after the view - // is mounted to the DOM, so the `NodeRef` will be `Some` - .expect("<input> to exist") - // `NodeRef` implements `Deref` for the DOM element type - // this means we can call`HtmlInputElement::value()` - // to get the current value of the input - .value(); - set_name(value); - }; - - view! { - <form on:submit=on_submit> - <input type="text" - // here, we use the `value` *attribute* to set only - // the initial value, letting the browser maintain - // the state after that - value=name - - // store a reference to this input in `input_element` - node_ref=input_element - /> - <input type="submit" value="Submit"/> - </form> - <p>"Name is: " {name}</p> - } -} - -// This `main` function is the entry point into the app -// It just mounts our component to the <body> -// Because we defined it as `fn App`, we can now use it in a -// template as <App/> -fn main() { - leptos::mount_to_body(App) -} -``` - -</details> -</preview> +<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/view/05_forms.html"> +<link rel="canonical" href="https://book.leptos.dev/view/05_forms.html"> diff --git a/docs/book/src/view/06_control_flow.md b/docs/book/src/view/06_control_flow.md index b3f1af6e20..94769fe9a2 100644 --- a/docs/book/src/view/06_control_flow.md +++ b/docs/book/src/view/06_control_flow.md @@ -1,384 +1,2 @@ -# Control Flow - -In most applications, you sometimes need to make a decision: Should I render this -part of the view, or not? Should I render `<ButtonA/>` or `<WidgetB/>`? This is -**control flow**. - -## A Few Tips - -When thinking about how to do this with Leptos, it’s important to remember a few -things: - -1. Rust is an expression-oriented language: control-flow expressions like - `if x() { y } else { z }` and `match x() { ... }` return their values. This - makes them very useful for declarative user interfaces. -2. For any `T` that implements `IntoView`—in other words, for any type that Leptos - knows how to render—`Option<T>` and `Result<T, impl Error>` _also_ implement - `IntoView`. And just as `Fn() -> T` renders a reactive `T`, `Fn() -> Option<T>` - and `Fn() -> Result<T, impl Error>` are reactive. -3. Rust has lots of handy helpers like [Option::map](https://doc.rust-lang.org/std/option/enum.Option.html#method.map), - [Option::and_then](https://doc.rust-lang.org/std/option/enum.Option.html#method.and_then), - [Option::ok_or](https://doc.rust-lang.org/std/option/enum.Option.html#method.ok_or), - [Result::map](https://doc.rust-lang.org/std/result/enum.Result.html#method.map), - [Result::ok](https://doc.rust-lang.org/std/result/enum.Result.html#method.ok), and - [bool::then](https://doc.rust-lang.org/std/primitive.bool.html#method.then) that - allow you to convert, in a declarative way, between a few different standard types, - all of which can be rendered. Spending time in the `Option` and `Result` docs in particular - is one of the best ways to level up your Rust game. -4. And always remember: to be reactive, values must be functions. You’ll see me constantly - wrap things in a `move ||` closure, below. This is to ensure that they actually rerun - when the signal they depend on changes, keeping the UI reactive. - -## So What? - -To connect the dots a little: this means that you can actually implement most of -your control flow with native Rust code, without any control-flow components or -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; -``` - -> 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 - -Let’s say I want to render some text if the number is odd, and some other text -if it’s even. Well, how about this? - -```rust -view! { - <p> - {move || if is_odd() { - "Odd" - } else { - "Even" - }} - </p> -} -``` - -An `if` expression returns its value, and a `&str` implements `IntoView`, so a -`Fn() -> &str` implements `IntoView`, so this... just works! - -### `Option<T>` - -Let’s say we want to render some text if it’s odd, and nothing if it’s even. - -```rust -let message = move || { - if is_odd() { - Some("Ding ding ding!") - } else { - None - } -}; - -view! { - <p>{message}</p> -} -``` - -This works fine. We can make it a little shorter if we’d like, using `bool::then()`. - -```rust -let message = move || is_odd().then(|| "Ding ding ding!"); -view! { - <p>{message}</p> -} -``` - -You could even inline this if you’d like, although personally I sometimes like the -better `cargo fmt` and `rust-analyzer` support I get by pulling things out of the `view`. - -### `match` statements - -We’re still just writing ordinary Rust code, right? So you have all the power of Rust’s -pattern matching at your disposal. - -```rust -let message = move || { - match value() { - 0 => "Zero", - 1 => "One", - n if is_odd() => "Odd", - _ => "Even" - } -}; -view! { - <p>{message}</p> -} -``` - -And why not? YOLO, right? - -## Preventing Over-Rendering - -Not so YOLO. - -Everything we’ve just done is basically fine. But there’s one thing you should remember -and try to be careful with. Each one of the control-flow functions we’ve created so far -is basically a derived signal: it will rerun every time the value changes. In the examples -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 message = move || if value() > 5 { - "Big" -} else { - "Small" -}; - -view! { - <p>{message}</p> -} -``` - -This _works_, for sure. But if you added a log, you might be surprised - -```rust -let message = move || if value() > 5 { - logging::log!("{}: rendering Big", value()); - "Big" -} else { - logging::log!("{}: rendering Small", value()); - "Small" -}; -``` - -As a user clicks a button, you’d see something like this: - -``` -1: rendering Small -2: rendering Small -3: rendering Small -4: rendering Small -5: rendering Small -6: rendering Big -7: rendering Big -8: rendering Big -... ad infinitum -``` - -Every time `value` changes, it reruns the `if` statement. This makes sense, with -how reactivity works. But it has a downside. For a simple text node, rerunning -the `if` statement and rerendering isn’t a big deal. But imagine it were -like this: - -```rust -let message = move || if value() > 5 { - <Big/> -} else { - <Small/> -}; -``` - -This rerenders `<Small/>` five times, then `<Big/>` infinitely. If they’re -loading resources, creating signals, or even just creating DOM nodes, this is -unnecessary work. - -### `<Show/>` - -The [`<Show/>`](https://docs.rs/leptos/latest/leptos/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); - -view! { - <Show - when=move || { value() > 5 } - fallback=|| view! { <Small/> } - > - <Big/> - </Show> -} -``` - -`<Show/>` memoizes the `when` condition, so it only renders its `<Small/>` once, -continuing to show the same component until `value` is greater than five; -then it renders `<Big/>` once, continuing to show it indefinitely or until `value` -goes below five and then renders `<Small/>` again. - -This is a helpful tool to avoid rerendering when using dynamic `if` expressions. -As always, there's some overhead: for a very simple node (like updating a single -text node, or updating a class or attribute), a `move || if ...` will be more -efficient. But if it’s at all expensive to render either branch, reach for -`<Show/>`. - -## Note: Type Conversions - -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: - -```rust,compile_error -view! { - <main> - {move || match is_odd() { - true if value() == 1 => { - // returns HtmlElement<Pre> - view! { <pre>"One"</pre> } - }, - false if value() == 2 => { - // returns HtmlElement<P> - view! { <p>"Two"</p> } - } - // returns HtmlElement<Textarea> - _ => view! { <textarea>{value()}</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. - -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). - -Here’s the same example, with the conversion added: - -```rust,compile_error -view! { - <main> - {move || match is_odd() { - true if value() == 1 => { - // returns HtmlElement<Pre> - view! { <pre>"One"</pre> }.into_any() - }, - false if value() == 2 => { - // returns HtmlElement<P> - view! { <p>"Two"</p> }.into_any() - } - // returns HtmlElement<Textarea> - _ => view! { <textarea>{value()}</textarea> }.into_any() - }} - </main> -} -``` - -[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/6-control-flow-0-5-4yn7qz?file=%2Fsrc%2Fmain.rs%3A1%2C1) - -<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> - -<details> -<summary>CodeSandbox Source</summary> - -```rust -use leptos::*; - -#[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 }; - - view! { - <h1>"Control Flow"</h1> - - // Simple UI to update and show a value - <button on:click=move |_| set_value.update(|n| *n += 1)> - "+1" - </button> - <p>"Value is: " {value}</p> - - <hr/> - - <h2><code>"Option<T>"</code></h2> - // For any `T` that implements `IntoView`, - // so does `Option<T>` - - <p>{odd_text}</p> - // This means you can use `Option` methods on it - <p>{move || odd_text().map(|text| text.len())}</p> - - <h2>"Conditional Logic"</h2> - // You can do dynamic conditional if-then-else - // logic in several ways - // - // a. An "if" expression in a function - // This will simply re-render every time the value - // changes, which makes it good for lightweight UI - <p> - {move || if is_odd() { - "Odd" - } else { - "Even" - }} - </p> - - // b. Toggling some kind of class - // This is smart for an element that's going to - // toggled often, because it doesn't destroy - // it in between states - // (you can find the `hidden` class in `index.html`) - <p class:hidden=is_odd>"Appears if even."</p> - - // c. The <Show/> component - // This only renders the fallback and the child - // once, lazily, and toggles between them when - // needed. This makes it more efficient in many cases - // than a {move || if ...} block - <Show when=is_odd - fallback=|| view! { <p>"Even steven"</p> } - > - <p>"Oddment"</p> - </Show> - - // d. Because `bool::then()` converts a `bool` to - // `Option`, you can use it to create a show/hide toggled - {move || is_odd().then(|| view! { <p>"Oddity!"</p> })} - - <h2>"Converting between Types"</h2> - // e. Note: if branches return different types, - // you can convert between them with - // `.into_any()` (for different HTML element types) - // or `.into_view()` (for all view types) - {move || match is_odd() { - true if value() == 1 => { - // <pre> returns HtmlElement<Pre> - view! { <pre>"One"</pre> }.into_any() - }, - false if value() == 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() - }} - } -} - -fn main() { - leptos::mount_to_body(App) -} -``` - -</details> -</preview> +<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/view/06_control_flow.html"> +<link rel="canonical" href="https://book.leptos.dev/view/06_control_flow.html"> diff --git a/docs/book/src/view/07_errors.md b/docs/book/src/view/07_errors.md index 375fcb469f..3f86005ac9 100644 --- a/docs/book/src/view/07_errors.md +++ b/docs/book/src/view/07_errors.md @@ -1,175 +1,2 @@ -# Error Handling - -[In the last chapter](./06_control_flow.md), we saw that you can render `Option<T>`: -in the `None` case, it will render nothing, and in the `T` case, it will render `T` -(that is, if `T` implements `IntoView`). You can actually do something very similar -with a `Result<T, E>`. In the `Err(_)` case, it will render nothing. In the `Ok(T)` -case, it will render the `T`. - -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>()); - - view! { - <label> - "Type a number (or not!)" - <input type="number" on:input=on_input/> - <p> - "You entered " - <strong>{value}</strong> - </p> - </label> - } -} -``` - -Every time you change the input, `on_input` will attempt to parse its value into a 32-bit -integer (`i32`), and store it in our `value` signal, which is a `Result<i32, _>`. If you -type the number `42`, the UI will display - -``` -You entered 42 -``` - -But if you type the string`foo`, it will display - -``` -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) -component. - -## `<ErrorBoundary/>` - -An `<ErrorBoundary/>` is a little like the `<Show/>` component we saw in the last chapter. -If everything’s okay—which is to say, if everything is `Ok(_)`—it renders its children. -But if there’s an `Err(_)` rendered among those children, it will trigger the -`<ErrorBoundary/>`’s `fallback`. - -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>()); - - view! { - <h1>"Error Handling"</h1> - <label> - "Type a number (or something that's not a number!)" - <input type="number" on:input=on_input/> - <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 - <ul> - {move || errors.get() - .into_iter() - .map(|(_, e)| view! { <li>{e.to_string()}</li>}) - .collect_view() - } - </ul> - </div> - } - > - <p>"You entered " <strong>{value}</strong></p> - </ErrorBoundary> - </label> - } -} -``` - -Now, if you type `42`, `value` is `Ok(42)` and you’ll see - -``` -You entered 42 -``` - -If you type `foo`, value is `Err(_)` and the `fallback` will render. We’ve chosen to render -the list of errors as a `String`, so you’ll see something like - -``` -Not a number! Errors: -- cannot parse integer from empty string -``` - -If you fix the error, the error message will disappear and the content you’re wrapping in -an `<ErrorBoundary/>` will appear again. - -[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/7-errors-0-5-5mptv9?file=%2Fsrc%2Fmain.rs%3A1%2C1) - -<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> - -<details> -<summary>CodeSandbox Source</summary> - -```rust -use leptos::*; - -#[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>()); - - view! { - <h1>"Error Handling"</h1> - <label> - "Type a number (or something that's not a number!)" - <input type="number" on:input=on_input/> - // 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 - <ul> - {move || errors.get() - .into_iter() - .map(|(_, e)| view! { <li>{e.to_string()}</li>}) - .collect::<Vec<_>>() - } - </ul> - </div> - } - > - <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> - } -} - -fn main() { - leptos::mount_to_body(App) -} -``` - -</details> -</preview> +<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/view/07_errors.html"> +<link rel="canonical" href="https://book.leptos.dev/view/07_errors.html"> diff --git a/docs/book/src/view/08_parent_child.md b/docs/book/src/view/08_parent_child.md index b9c7789f9a..dee2fc1419 100644 --- a/docs/book/src/view/08_parent_child.md +++ b/docs/book/src/view/08_parent_child.md @@ -1,464 +1,2 @@ -# Parent-Child Communication - -You can think of your application as a nested tree of components. Each component -handles its own local state and manages a section of the user interface, so -components tend to be relatively self-contained. - -Sometimes, though, you’ll want to communicate between a parent component and its -child. For example, imagine you’ve defined a `<FancyButton/>` component that adds -some styling, logging, or something else to a `<button/>`. You want to use a -`<FancyButton/>` in your `<App/>` component. But how can you communicate between -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. - -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) - -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. - -```rust -#[component] -pub fn App() -> impl IntoView { - let (toggled, set_toggled) = create_signal(false); - view! { - <p>"Toggled? " {toggled}</p> - <ButtonA setter=set_toggled/> - } -} - -#[component] -pub fn ButtonA(setter: WriteSignal<bool>) -> impl IntoView { - view! { - <button - on:click=move |_| setter.update(|value| *value = !*value) - > - "Toggle" - </button> - } -} -``` - -This pattern is simple, but you should be careful with it: passing around a `WriteSignal` -can make it hard to reason about your code. In this example, it’s pretty clear when you -read `<App/>` that you are handing off the ability to mutate `toggled`, but it’s not at -all clear when or how it will change. In this small, local example it’s easy to understand, -but if you find yourself passing around `WriteSignal`s like this throughout your code, -you should really consider whether this is making it too easy to write spaghetti code. - -## 2. Use a Callback - -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); - 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 -{ - view! { - <button on:click=on_click> - "Toggle" - </button> - } -} -``` - -You’ll notice that whereas `<ButtonA/>` was given a `WriteSignal` and decided how to mutate it, -`<ButtonB/>` simply fires an event: the mutation happens back in `<App/>`. This has the advantage -of keeping local state local, preventing the problem of spaghetti mutation. But it also means -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 -a native DOM event, you can add an `on:` listener directly to the place you use the component -in your `view` macro in `<App/>`. - -```rust -#[component] -pub fn App() -> impl IntoView { - let (toggled, set_toggled) = create_signal(false); - view! { - <p>"Toggled? " {toggled}</p> - // note the on:click instead of on_click - // this is the same syntax as an HTML element event listener - <ButtonC on:click=move |_| set_toggled.update(|value| *value = !*value)/> - } -} - - -#[component] -pub fn ButtonC() -> impl IntoView { - view! { - <button>"Toggle"</button> - } -} -``` - -This lets you write way less code in `<ButtonC/>` than you did for `<ButtonB/>`, -and still gives a correctly-typed event to the listener. This works by adding an -`on:` event listener to each element that `<ButtonC/>` returns: in this case, just -the one `<button>`. - -Of course, this only works for actual DOM events that you’re passing directly through -to the elements you’re rendering in the component. For more complex logic that -doesn’t map directly onto an element (say you create `<ValidatedForm/>` and want an -`on_valid_form_submit` callback) you should use Option 2. - -## 4. Providing a Context - -This version is actually a variant on Option 1. Say you have a deeply-nested component -tree: - -```rust -#[component] -pub fn App() -> impl IntoView { - let (toggled, set_toggled) = create_signal(false); - view! { - <p>"Toggled? " {toggled}</p> - <Layout/> - } -} - -#[component] -pub fn Layout() -> impl IntoView { - view! { - <header> - <h1>"My Page"</h1> - </header> - <main> - <Content/> - </main> - } -} - -#[component] -pub fn Content() -> impl IntoView { - view! { - <div class="content"> - <ButtonD/> - </div> - } -} - -#[component] -pub fn ButtonD<F>() -> impl IntoView { - todo!() -} -``` - -Now `<ButtonD/>` is no longer a direct child of `<App/>`, so you can’t simply -pass your `WriteSignal` to its props. You could do what’s sometimes called -“prop drilling,” adding a prop to each layer between the two: - -```rust -#[component] -pub fn App() -> impl IntoView { - let (toggled, set_toggled) = create_signal(false); - view! { - <p>"Toggled? " {toggled}</p> - <Layout set_toggled/> - } -} - -#[component] -pub fn Layout(set_toggled: WriteSignal<bool>) -> impl IntoView { - view! { - <header> - <h1>"My Page"</h1> - </header> - <main> - <Content set_toggled/> - </main> - } -} - -#[component] -pub fn Content(set_toggled: WriteSignal<bool>) -> impl IntoView { - view! { - <div class="content"> - <ButtonD set_toggled/> - </div> - } -} - -#[component] -pub fn ButtonD<F>(set_toggled: WriteSignal<bool>) -> impl IntoView { - todo!() -} -``` - -This is a mess. `<Layout/>` and `<Content/>` don’t need `set_toggled`; they just -pass it through to `<ButtonD/>`. But I need to declare the prop in triplicate. -This is not only annoying but hard to maintain: imagine we add a “half-toggled” -option and the type of `set_toggled` needs to change to an `enum`. We have to change -it in three places! - -Isn’t there some way to skip levels? - -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 -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. - -```rust -#[component] -pub fn App() -> impl IntoView { - let (toggled, set_toggled) = create_signal(false); - - // share `set_toggled` with all children of this component - provide_context(set_toggled); - - view! { - <p>"Toggled? " {toggled}</p> - <Layout/> - } -} - -// <Layout/> and <Content/> omitted -// To work in this version, drop their references to set_toggled - -#[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"); - - view! { - <button - on:click=move |_| setter.update(|value| *value = !*value) - > - "Toggle" - </button> - } -} -``` - -The same caveats apply to this as to `<ButtonA/>`: passing a `WriteSignal` -around should be done with caution, as it allows you to mutate state from -arbitrary parts of your code. But when done carefully, this can be one of -the most effective techniques for global state management in Leptos: simply -provide the state at the highest level you’ll need it, and use it wherever -you need it lower down. - -Note that there are no performance downsides to this approach. Because you -are passing a fine-grained reactive signal, _nothing happens_ in the intervening -components (`<Layout/>` and `<Content/>`) when you update it. You are communicating -directly between `<ButtonD/>` and `<App/>`. In fact—and this is the power of -fine-grained reactivity—you are communicating directly between a button click -in `<ButtonD/>` and a single text node in `<App/>`. It’s as if the components -themselves don’t exist at all. And, well... at runtime, they don’t. It’s just -signals and effects, all the way down. - -[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/8-parent-child-0-5-7rz7qd?file=%2Fsrc%2Fmain.rs%3A1%2C2) - -<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> - -<details> -<summary>CodeSandbox Source</summary> - -```rust -use leptos::{ev::MouseEvent, *}; - -// This highlights four different ways that child components can communicate -// with their parent: -// 1) <ButtonA/>: passing a WriteSignal as one of the child component props, -// for the child component to write into and the parent to read -// 2) <ButtonB/>: passing a closure as one of the child component props, for -// the child component to call -// 3) <ButtonC/>: adding an `on:` event listener to a component -// 4) <ButtonD/>: providing a context that is used in the component (rather than prop drilling) - -#[derive(Copy, Clone)] -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); - - // the newtype pattern isn't *necessary* here but is a good practice - // it avoids confusion with other possible future `WriteSignal<bool>` contexts - // and makes it easier to refer to it in ButtonC - provide_context(SmallcapsContext(set_smallcaps)); - - view! { - <main> - <p - // class: attributes take F: Fn() => bool, and these signals all implement Fn() - class:red=red - class:right=right - class:italics=italics - class:smallcaps=smallcaps - > - "Lorem ipsum sit dolor amet." - </p> - - // Button A: pass the signal setter - <ButtonA setter=set_red/> - - // Button B: pass a closure - <ButtonB on_click=move |_| set_right.update(|value| *value = !*value)/> - - // Button B: use a regular event listener - // setting an event listener on a component like this applies it - // to each of the top-level elements the component returns - <ButtonC on:click=move |_| set_italics.update(|value| *value = !*value)/> - - // Button D gets its setter from context rather than props - <ButtonD/> - </main> - } -} - -/// Button A receives a signal setter and updates the signal itself -#[component] -pub fn ButtonA( - /// Signal that will be toggled when the button is clicked. - setter: WriteSignal<bool>, -) -> impl IntoView { - view! { - <button - on:click=move |_| setter.update(|value| *value = !*value) - > - "Toggle Red" - </button> - } -} - -/// Button B receives a closure -#[component] -pub fn ButtonB<F>( - /// Callback that will be invoked when the button is clicked. - on_click: F, -) -> impl IntoView -where - F: Fn(MouseEvent) + 'static, -{ - view! { - <button - on:click=on_click - > - "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 -/// its click. Instead, the parent component adds an event listener. -#[component] -pub fn ButtonC() -> impl IntoView { - view! { - <button> - "Toggle Italics" - </button> - } -} - -/// Button D is very similar to Button A, but instead of passing the setter as a prop -/// we get it from the context -#[component] -pub fn ButtonD() -> impl IntoView { - let setter = use_context::<SmallcapsContext>().unwrap().0; - - view! { - <button - on:click=move |_| setter.update(|value| *value = !*value) - > - "Toggle Small Caps" - </button> - } -} - -fn main() { - leptos::mount_to_body(App) -} -``` - -</details> -</preview> +<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/view/08_parent_child.html"> +<link rel="canonical" href="https://book.leptos.dev/view/08_parent_child.html"> diff --git a/docs/book/src/view/09_component_children.md b/docs/book/src/view/09_component_children.md index 0da9e64b47..c463e6b544 100644 --- a/docs/book/src/view/09_component_children.md +++ b/docs/book/src/view/09_component_children.md @@ -1,229 +1,2 @@ -# Component Children - -It’s pretty common to want to pass children into a component, just as you can pass -children into an HTML element. For example, imagine I have a `<FancyForm/>` component -that enhances an HTML `<form>`. I need some way to pass all its inputs. - -```rust -view! { - <Form> - <fieldset> - <label> - "Some Input" - <input type="text" name="something"/> - </label> - </fieldset> - <button>"Submit"</button> - </Form> -} -``` - -How can you do this in Leptos? There are basically two ways to pass components to -other components: - -1. **render props**: properties that are functions that return a view -2. the **`children`** prop: a special component property that includes anything - you pass as a child to the component. - -In fact, you’ve already seen these both in action in the [`<Show/>`](/view/06_control_flow.html#show) component: - -```rust -view! { - <Show - // `when` is a normal prop - when=move || value() > 5 - // `fallback` is a "render prop": a function that returns a view - fallback=|| view! { <Small/> } - > - // `<Big/>` (and anything else here) - // will be given to the `children` prop - <Big/> - </Show> -} -``` - -Let’s define a component that takes some children and a render prop. - -```rust -#[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: Children, -) -> impl IntoView -where - F: Fn() -> IV, - IV: IntoView, -{ - view! { - <h2>"Render Prop"</h2> - {render_prop()} - - <h2>"Children"</h2> - {children()} - } -} -``` - -`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?) - -> If you need a `Fn` or `FnMut` here because you need to call `children` more than once, -> we also provide `ChildrenFn` and `ChildrenMut` aliases. - -We can use the component like this: - -```rust -view! { - <TakesChildren render_prop=|| view! { <p>"Hi, there!"</p> }> - // these get passed to `children` - "Some text" - <span>"A span"</span> - </TakesChildren> -} -``` - -## 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. - -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 -#[component] -pub fn WrapsChildren(children: Children) -> impl IntoView { - // Fragment has `nodes` field that contains a Vec<View> - let children = children() - .nodes - .into_iter() - .map(|child| view! { <li>{child}</li> }) - .collect_view(); - - view! { - <ul>{children}</ul> - } -} -``` - -Calling it like this will create a list: - -```rust -view! { - <WrapsChildren> - "A" - "B" - "C" - </WrapsChildren> -} -``` - -[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/9-component-children-0-5-m4jwhp?file=%2Fsrc%2Fmain.rs%3A1%2C1) - -<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> - -<details> -<summary>CodeSandbox Source</summary> - -```rust -use leptos::*; - -// Often, you want to pass some kind of child view to another -// component. There are two basic patterns for doing this: -// - "render props": creating a component prop that takes a function -// that creates a view -// - the `children` prop: a special property that contains content -// passed as the children of a component in your view, not as a -// property - -#[component] -pub fn App() -> impl IntoView { - let (items, set_items) = create_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); - view! { - <p>"Length: " {len}</p> - } - }; - - view! { - // This component just displays the two kinds of children, - // embedding them in some other markup - <TakesChildren - // for component props, you can shorthand - // `render_prop=render_prop` => `render_prop` - // (this doesn't work for HTML element attributes) - render_prop - > - // these look just like the children of an HTML element - <p>"Here's a child."</p> - <p>"Here's another child."</p> - </TakesChildren> - <hr/> - // This component actually iterates over and wraps the children - <WrapsChildren> - <p>"Here's a child."</p> - <p>"Here's another child."</p> - </WrapsChildren> - } -} - -/// 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 - /// this is an alias for `Box<dyn FnOnce() -> Fragment>` - /// ... aren't you glad we named it `Children` instead? - children: Children, -) -> impl IntoView -where - F: Fn() -> IV, - IV: IntoView, -{ - view! { - <h1><code>"<TakesChildren/>"</code></h1> - <h2>"Render Prop"</h2> - {render_prop()} - <hr/> - <h2>"Children"</h2> - {children()} - } -} - -/// Wraps each child in an `<li>` and embeds them in a `<ul>`. -#[component] -pub fn WrapsChildren(children: Children) -> 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::<Vec<_>>(); - - view! { - <h1><code>"<WrapsChildren/>"</code></h1> - // wrap our wrapped children in a UL - <ul>{children}</ul> - } -} - -fn main() { - leptos::mount_to_body(App) -} -``` - -</details> -</preview> +<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/view/09_component_children.html"> +<link rel="canonical" href="https://book.leptos.dev/view/09_component_children.html"> diff --git a/docs/book/src/view/README.md b/docs/book/src/view/README.md index 2101d65905..c00c5cc828 100644 --- a/docs/book/src/view/README.md +++ b/docs/book/src/view/README.md @@ -1,6 +1,2 @@ -# Part 1: Building User Interfaces - -In the first part of the book, we're going to look at building user interfaces on the client-side using Leptos. Under the hood, Leptos and Trunk are bundling up a snippet of Javascript which will load up the Leptos UI, which has been compiled to WebAssembly to drive the interactivity in your CSR (client-side rendered) website. - -Part 1 will introduce you to the basic tools you need to build a reactive user interface powered by Leptos and Rust. By the end of Part 1, you should be able to -build a snappy synchronous website that's rendered in the browser and which you can deploy on any static-site hosting service, like Github Pages or Vercel. \ No newline at end of file +<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/view/index.html"> +<link rel="canonical" href="https://book.leptos.dev/view/index.html"> diff --git a/docs/book/src/view/builder.md b/docs/book/src/view/builder.md index 7eb54d13c0..987f461398 100644 --- a/docs/book/src/view/builder.md +++ b/docs/book/src/view/builder.md @@ -1,98 +1,2 @@ -# No Macros: The View Builder Syntax - -> If you’re perfectly happy with the `view!` macro syntax described so far, you’re welcome to skip this chapter. The builder syntax described in this section is always available, but never required. - -For one reason or another, many developers would prefer to avoid macros. Perhaps you don’t like the limited `rustfmt` support. (Although, you should check out [`leptosfmt`](https://github.com/bram209/leptosfmt), which is an excellent tool!) Perhaps you worry about the effect of macros on compile time. Perhaps you prefer the aesthetics of pure Rust syntax, or you have trouble context-switching between an HTML-like syntax and your Rust code. Or perhaps you want more flexibility in how you create and manipulate HTML elements than the `view` macro provides. - -If you fall into any of those camps, the builder syntax may be for you. - -The `view` macro expands an HTML-like syntax to a series of Rust functions and method calls. If you’d rather not use the `view` macro, you can simply use that expanded syntax yourself. And it’s actually pretty nice! - -First off, if you want you can even drop the `#[component]` macro: a component is just a setup function that creates your view, so you can define a component as a simple function call: - -```rust -pub fn counter(initial_value: i32, step: u32) -> impl IntoView { } -``` - -Elements are created by calling a function with the same name as the HTML element: - -```rust -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). - -```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)). - -```rust -p().attr("id", "foo").attr("data-count", move || count().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. - -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 -button() - .on(ev::click, move |_| set_count.update(|count| count.clear())) - .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); - - 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.clear())) - .child("Clear"), - button() - .on(ev::click, move |_| { - set_count.update(|count| count.decrease()) - }) - .child("-1"), - span().child(("Value: ", move || count.get().value(), "!")), - button() - .on(ev::click, move |_| { - set_count.update(|count| count.increase()) - }) - .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. +<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/view/builder.html"> +<link rel="canonical" href="https://book.leptos.dev/view/builder.html">