Compare commits

..

5 Commits

Author SHA1 Message Date
Greg Johnston
90f4090845 cargo fmt 2023-05-05 12:25:57 -04:00
Greg Johnston
6b2133f46f fix suspense hydration 2023-05-05 12:25:53 -04:00
Greg Johnston
0482aa30b9 stash some work on examples 2023-04-28 17:04:36 -04:00
Greg Johnston
b68e39f9c9 clone Suspense rather than rendering it repeatedly 2023-04-28 16:48:24 -04:00
Greg Johnston
3cb9c04c08 move DynChild back into fragment when unmounting it, rather than only unmounting nodes 2023-04-28 16:48:13 -04:00
11 changed files with 88 additions and 211 deletions

View File

@@ -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)

View File

@@ -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 were 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 wont 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. Heres 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 cant 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 dont 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, its 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
```
Its captured through multiple levels of children that need to run more than once, and theres 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.

View File

@@ -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"

View File

@@ -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>
}
}
}

View File

@@ -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>
}
}

View File

@@ -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>

View File

@@ -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),
}
}
}

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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.

View File

@@ -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.