mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 10:11:56 -05:00
Compare commits
5 Commits
server-fn-
...
suspense-l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90f4090845 | ||
|
|
6b2133f46f | ||
|
|
0482aa30b9 | ||
|
|
b68e39f9c9 | ||
|
|
3cb9c04c08 |
@@ -19,7 +19,6 @@
|
||||
- [Suspense](./async/11_suspense.md)
|
||||
- [Transition](./async/12_transition.md)
|
||||
- [Actions](./async/13_actions.md)
|
||||
- [Interlude: Projecting Children](./interlude_projecting_children.md)
|
||||
- [Responding to Changes with `create_effect`](./14_create_effect.md)
|
||||
- [Global State Management](./15_global_state.md)
|
||||
- [Router](./router/README.md)
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
# 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<F, IV>(cx: Scope, fallback: F, children: ChildrenFn) -> impl IntoView
|
||||
where
|
||||
F: Fn(Scope) -> IV + 'static,
|
||||
IV: IntoView,
|
||||
{
|
||||
view! { cx,
|
||||
<Suspense
|
||||
fallback=|| ()
|
||||
>
|
||||
<Show
|
||||
// check whether user is verified
|
||||
// by reading from the resource
|
||||
when=move || todo!()
|
||||
fallback=fallback
|
||||
>
|
||||
{children(cx)}
|
||||
</Show>
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is pretty straightforward: when the user is logged in, we want to show `children`. Until 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 `<WhenLoaded>/` _through_ the `<Suspense/>` component to become the children of the `<Show/>`. 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 `<Suspense/>` and `<Show/>` need to be able to construct their `children` multiple names. The first time you construct `<Suspense/>`’s children, it would take ownership of `fallback` and `children` to move them into the invocation of `<Show/>`, but then they're not available for future `<Suspense/>` 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(
|
||||
cx,
|
||||
::leptos::component_props_builder(&Suspense)
|
||||
.fallback(|| ())
|
||||
.children({
|
||||
// fallback and children are moved into this closure
|
||||
Box::new(move |cx| {
|
||||
{
|
||||
// fallback and children captured here
|
||||
leptos::Fragment::lazy(|| {
|
||||
vec![
|
||||
(Show(
|
||||
cx,
|
||||
::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(cx)),
|
||||
]
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
.build(),
|
||||
)
|
||||
```
|
||||
|
||||
All components own their props; so the `<Show/>` in this case can’t be called, because it only has captured references to `fallback` and `children`.
|
||||
|
||||
## Solution
|
||||
|
||||
However, both `<Suspense/>` and `<Show/>` 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`, and which we can access or modify through certain methods.
|
||||
|
||||
In this case, it’s really simple:
|
||||
|
||||
```rust
|
||||
pub fn LoggedIn<F, IV>(cx: Scope, fallback: F, children: ChildrenFn) -> impl IntoView
|
||||
where
|
||||
F: Fn(Scope) -> IV + 'static,
|
||||
IV: IntoView,
|
||||
{
|
||||
let fallback = store_value(cx, fallback);
|
||||
let children = store_value(cx, children);
|
||||
view! { cx,
|
||||
<Suspense
|
||||
fallback=|| ()
|
||||
>
|
||||
<Show
|
||||
when=|| todo!()
|
||||
fallback=move |cx| fallback.with_value(|fallback| fallback(cx))
|
||||
>
|
||||
{children.with_value(|children| children(cx))}
|
||||
</Show>
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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 `<Show/>` component, and call them there.
|
||||
|
||||
## A Final Note
|
||||
|
||||
Note that this works because `<Show/>` and `<Suspense/>` 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(cx: Scope) -> impl IntoView {
|
||||
let name = "Alice".to_string();
|
||||
view! { cx,
|
||||
<Outer>
|
||||
<Inner>
|
||||
<Inmost name=name.clone()/>
|
||||
</Inner>
|
||||
</Outer>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Outer(cx: Scope, children: ChildrenFn) -> impl IntoView {
|
||||
children(cx)
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Inner(cx: Scope, children: ChildrenFn) -> impl IntoView {
|
||||
children(cx)
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Inmost(cx: Scope, name: String) -> impl IntoView {
|
||||
view! { cx,
|
||||
<p>{name}</p>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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 `<Inner/>`’s children, which solves our ownership issue.
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
<Outer>
|
||||
<Inner clone:name>
|
||||
<Inmost name=name.clone()/>
|
||||
</Inner>
|
||||
</Outer>
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
@@ -12,6 +12,7 @@ leptos = { path = "../../leptos" }
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
gloo-timers = { version = "0.2.6", features = ["futures"] }
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
@@ -1,24 +1,46 @@
|
||||
use leptos::*;
|
||||
|
||||
/// A simple counter component.
|
||||
///
|
||||
/// You can use doc comments like this to document your component.
|
||||
fn update_counter_bg(mut value: i32, step: i32, sig: WriteSignal<i32>) {
|
||||
sig.set(value);
|
||||
value += step;
|
||||
if value < 1000 {
|
||||
leptos::set_timeout(
|
||||
move || {
|
||||
update_counter_bg(value, step, sig);
|
||||
},
|
||||
std::time::Duration::from_millis(10),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[component]
|
||||
pub fn SimpleCounter(
|
||||
cx: Scope,
|
||||
/// The starting value for the counter
|
||||
initial_value: i32,
|
||||
/// The change that should be applied each time the button is clicked.
|
||||
step: i32
|
||||
step: i32,
|
||||
) -> impl IntoView {
|
||||
let (value, set_value) = create_signal(cx, initial_value);
|
||||
|
||||
// update the value signal periodically
|
||||
update_counter_bg(initial_value, step, set_value);
|
||||
|
||||
view! { cx,
|
||||
<div>
|
||||
<button on:click=move |_| set_value(0)>"Clear"</button>
|
||||
<button on:click=move |_| set_value.update(|value| *value -= step)>"-1"</button>
|
||||
<span>"Value: " {value} "!"</span>
|
||||
<button on:click=move |_| set_value.update(|value| *value += step)>"+1"</button>
|
||||
<div>
|
||||
<button on:click=move |_| set_value(0)>"Clear"</button>
|
||||
<button on:click=move |_| set_value.update(|value| *value -= step)>"-1"</button>
|
||||
<span>"Value: " {value} "!"</span>
|
||||
<button on:click=move |_| set_value.update(|value| *value += step)>"+1"</button>
|
||||
</div>
|
||||
<Show when={move || value() % 2 == 0} fallback=|_| ()>
|
||||
<For each={|| vec![1, 2, 3]} key=|key| *key view={move |cx, k| {
|
||||
view! {
|
||||
cx,
|
||||
<article>{k}</article>
|
||||
}
|
||||
}}/>
|
||||
</Show>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,7 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
|
||||
// and by using the ErrorBoundary fallback to catch Err(_)
|
||||
// so we'll just implement our happy path and let the framework handle the rest
|
||||
let cats_view = move || {
|
||||
leptos::log!("rendering cats_view");
|
||||
cats.read(cx).map(|data| {
|
||||
data.map(|data| {
|
||||
data.iter()
|
||||
@@ -94,13 +95,13 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<ErrorBoundary fallback>
|
||||
//<ErrorBoundary fallback>
|
||||
<Transition fallback=move || {
|
||||
view! { cx, <div>"Loading (Suspense Fallback)..."</div> }
|
||||
}>
|
||||
{cats_view}
|
||||
</Transition>
|
||||
</ErrorBoundary>
|
||||
//</ErrorBoundary>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ pub fn Stories(cx: Scope) -> impl IntoView {
|
||||
}}
|
||||
</span>
|
||||
<span>"page " {page}</span>
|
||||
<Transition
|
||||
<Suspense
|
||||
fallback=move || view! { cx, <p>"Loading..."</p> }
|
||||
>
|
||||
<span class="page-link"
|
||||
@@ -78,13 +78,13 @@ pub fn Stories(cx: Scope) -> impl IntoView {
|
||||
"more >"
|
||||
</a>
|
||||
</span>
|
||||
</Transition>
|
||||
</Suspense>
|
||||
</div>
|
||||
<main class="news-list">
|
||||
<div>
|
||||
<Transition
|
||||
<Suspense
|
||||
fallback=move || view! { cx, <p>"Loading..."</p> }
|
||||
set_pending=set_pending.into()
|
||||
//set_pending=set_pending.into()
|
||||
>
|
||||
{move || match stories.read(cx) {
|
||||
None => None,
|
||||
@@ -105,7 +105,7 @@ pub fn Stories(cx: Scope) -> impl IntoView {
|
||||
}.into_any())
|
||||
}
|
||||
}}
|
||||
</Transition>
|
||||
</Suspense>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -45,20 +45,18 @@ where
|
||||
provide_context(cx, errors);
|
||||
|
||||
// Run children so that they render and execute resources
|
||||
let children = children(cx).into_view(cx);
|
||||
let errors_empty = create_memo(cx, move |_| errors.with(Errors::is_empty));
|
||||
let children = children(cx);
|
||||
|
||||
move || {
|
||||
if errors_empty.get() {
|
||||
children.clone().into_view(cx)
|
||||
} else {
|
||||
view! { cx,
|
||||
match errors.with(Errors::is_empty) {
|
||||
true => children.clone().into_view(cx),
|
||||
false => view! { cx,
|
||||
<>
|
||||
{fallback(cx, errors)}
|
||||
<leptos-error-boundary style="display: none">{children.clone()}</leptos-error-boundary>
|
||||
</>
|
||||
}
|
||||
.into_view(cx)
|
||||
.into_view(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,8 +82,17 @@ where
|
||||
move || {
|
||||
cfg_if! {
|
||||
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
|
||||
let mut child: Option<crate::View> = None;
|
||||
if context.ready() {
|
||||
Fragment::lazy(Box::new(|| vec![orig_child(cx).into_view(cx)])).into_view(cx)
|
||||
Fragment::lazy(Box::new(|| vec![{
|
||||
if let Some(child) = &child {
|
||||
child.clone()
|
||||
} else {
|
||||
let first_run_child = orig_child(cx).into_view(cx);
|
||||
child = Some(first_run_child.clone());
|
||||
first_run_child
|
||||
}
|
||||
}])).into_view(cx)
|
||||
} else {
|
||||
Fragment::lazy(Box::new(|| vec![fallback().into_view(cx)])).into_view(cx)
|
||||
}
|
||||
|
||||
@@ -17,11 +17,11 @@ cfg_if! {
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct DynChildRepr {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
document_fragment: web_sys::DocumentFragment,
|
||||
pub(crate) document_fragment: web_sys::DocumentFragment,
|
||||
#[cfg(debug_assertions)]
|
||||
opening: Comment,
|
||||
pub(crate) child: Rc<RefCell<Box<Option<View>>>>,
|
||||
closing: Comment,
|
||||
pub(crate) closing: Comment,
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
pub(crate) id: HydrationKey,
|
||||
}
|
||||
@@ -278,7 +278,33 @@ where
|
||||
let start = child.get_opening_node();
|
||||
let end = &closing;
|
||||
|
||||
unmount_child(&start, end);
|
||||
match child {
|
||||
View::CoreComponent(
|
||||
crate::CoreComponent::DynChild(
|
||||
child,
|
||||
),
|
||||
) => {
|
||||
let start =
|
||||
child.get_opening_node();
|
||||
let end = child.closing.node;
|
||||
prepare_to_move(
|
||||
&child.document_fragment,
|
||||
&start,
|
||||
&end,
|
||||
);
|
||||
}
|
||||
View::Component(child) => {
|
||||
let start =
|
||||
child.get_opening_node();
|
||||
let end = child.closing.node;
|
||||
prepare_to_move(
|
||||
&child.document_fragment,
|
||||
&start,
|
||||
&end,
|
||||
);
|
||||
}
|
||||
_ => unmount_child(&start, end),
|
||||
}
|
||||
}
|
||||
|
||||
// Mount the new child
|
||||
|
||||
@@ -817,8 +817,7 @@ pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
/// and [`DeserializeOwned`](https://docs.rs/serde/latest/serde/de/trait.DeserializeOwned.html).**
|
||||
/// They are serialized as an `application/x-www-form-urlencoded`
|
||||
/// form data using [`serde_urlencoded`](https://docs.rs/serde_urlencoded/latest/serde_urlencoded/) or as `application/cbor`
|
||||
/// using [`cbor`](https://docs.rs/cbor/latest/cbor/). **Note**: You should explicitly include `serde` with the
|
||||
/// `derive` feature enabled in your `Cargo.toml`. You can do this by running `cargo add serde --features=derive`.
|
||||
/// using [`cbor`](https://docs.rs/cbor/latest/cbor/).
|
||||
/// - **The `Scope` comes from the server.** Optionally, the first argument of a server function
|
||||
/// can be a Leptos `Scope`. This scope can be used to inject dependencies like the HTTP request
|
||||
/// or response or other server-only dependencies, but it does *not* have access to reactive state that exists in the client.
|
||||
|
||||
@@ -73,8 +73,7 @@
|
||||
//! need to deserialize the result to return it to the client.
|
||||
//! - **Arguments must be implement [serde::Serialize].** They are serialized as an `application/x-www-form-urlencoded`
|
||||
//! form data using [`serde_urlencoded`](https://docs.rs/serde_urlencoded/latest/serde_urlencoded/) or as `application/cbor`
|
||||
//! using [`cbor`](https://docs.rs/cbor/latest/cbor/). **Note**: You should explicitly include `serde` with the
|
||||
//! `derive` feature enabled in your `Cargo.toml`. You can do this by running `cargo add serde --features=derive`.
|
||||
//! using [`cbor`](https://docs.rs/cbor/latest/cbor/).
|
||||
//! - **The [Scope](leptos_reactive::Scope) comes from the server.** Optionally, the first argument of a server function
|
||||
//! can be a Leptos [Scope](leptos_reactive::Scope). This scope can be used to inject dependencies like the HTTP request
|
||||
//! or response or other server-only dependencies, but it does *not* have access to reactive state that exists in the client.
|
||||
|
||||
Reference in New Issue
Block a user