docs: document `<ErrorBoundary/>`/`<Suspense/>` relationship (#1210)
This commit is contained in:
parent
bbc7799b7c
commit
2cb8171105
|
@ -34,10 +34,7 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="" view=|cx| view! {
|
<Route path="" view=|cx| view! { cx, <ExampleErrors/> }/>
|
||||||
cx,
|
|
||||||
<ExampleErrors/>
|
|
||||||
}/>
|
|
||||||
</Routes>
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
</Router>
|
</Router>
|
||||||
|
@ -66,7 +63,7 @@ pub fn ExampleErrors(cx: Scope) -> impl IntoView {
|
||||||
// note that the error boundaries could be placed above in the Router or lower down
|
// note that the error boundaries could be placed above in the Router or lower down
|
||||||
// in a particular route. The generated errors on the entire page contribute to the
|
// in a particular route. The generated errors on the entire page contribute to the
|
||||||
// final status code sent by the server when producing ssr pages.
|
// final status code sent by the server when producing ssr pages.
|
||||||
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors=errors/>}>
|
<ErrorBoundary fallback=|cx, errors| view!{ cx, <ErrorTemplate errors=errors/>}>
|
||||||
<ReturnsError/>
|
<ReturnsError/>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -177,12 +177,7 @@ pub fn TodoApp(cx: Scope) -> impl IntoView {
|
||||||
<hr/>
|
<hr/>
|
||||||
<main>
|
<main>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="" view=|cx| view! {
|
<Route path="" view=|cx| view! { cx, <Todos/> }/> //Route
|
||||||
cx,
|
|
||||||
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors=errors/>}>
|
|
||||||
<Todos/>
|
|
||||||
</ErrorBoundary>
|
|
||||||
}/> //Route
|
|
||||||
<Route path="signup" view=move |cx| view! {
|
<Route path="signup" view=move |cx| view! {
|
||||||
cx,
|
cx,
|
||||||
<Signup action=signup/>
|
<Signup action=signup/>
|
||||||
|
@ -226,69 +221,71 @@ pub fn Todos(cx: Scope) -> impl IntoView {
|
||||||
<input type="submit" value="Add"/>
|
<input type="submit" value="Add"/>
|
||||||
</MultiActionForm>
|
</MultiActionForm>
|
||||||
<Transition fallback=move || view! {cx, <p>"Loading..."</p> }>
|
<Transition fallback=move || view! {cx, <p>"Loading..."</p> }>
|
||||||
{move || {
|
<ErrorBoundary fallback=|cx, errors| view!{ cx, <ErrorTemplate errors=errors/>}>
|
||||||
let existing_todos = {
|
{move || {
|
||||||
move || {
|
let existing_todos = {
|
||||||
todos.read(cx)
|
move || {
|
||||||
.map(move |todos| match todos {
|
todos.read(cx)
|
||||||
Err(e) => {
|
.map(move |todos| match todos {
|
||||||
view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_view(cx)
|
Err(e) => {
|
||||||
}
|
view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_view(cx)
|
||||||
Ok(todos) => {
|
|
||||||
if todos.is_empty() {
|
|
||||||
view! { cx, <p>"No tasks were found."</p> }.into_view(cx)
|
|
||||||
} else {
|
|
||||||
todos
|
|
||||||
.into_iter()
|
|
||||||
.map(move |todo| {
|
|
||||||
view! {
|
|
||||||
cx,
|
|
||||||
<li>
|
|
||||||
{todo.title}
|
|
||||||
": Created at "
|
|
||||||
{todo.created_at}
|
|
||||||
" by "
|
|
||||||
{
|
|
||||||
todo.user.unwrap_or_default().username
|
|
||||||
}
|
|
||||||
<ActionForm action=delete_todo>
|
|
||||||
<input type="hidden" name="id" value={todo.id}/>
|
|
||||||
<input type="submit" value="X"/>
|
|
||||||
</ActionForm>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect_view(cx)
|
|
||||||
}
|
}
|
||||||
}
|
Ok(todos) => {
|
||||||
})
|
if todos.is_empty() {
|
||||||
.unwrap_or_default()
|
view! { cx, <p>"No tasks were found."</p> }.into_view(cx)
|
||||||
}
|
} else {
|
||||||
};
|
todos
|
||||||
|
.into_iter()
|
||||||
let pending_todos = move || {
|
.map(move |todo| {
|
||||||
submissions
|
view! {
|
||||||
.get()
|
cx,
|
||||||
.into_iter()
|
<li>
|
||||||
.filter(|submission| submission.pending().get())
|
{todo.title}
|
||||||
.map(|submission| {
|
": Created at "
|
||||||
view! {
|
{todo.created_at}
|
||||||
cx,
|
" by "
|
||||||
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
|
{
|
||||||
|
todo.user.unwrap_or_default().username
|
||||||
|
}
|
||||||
|
<ActionForm action=delete_todo>
|
||||||
|
<input type="hidden" name="id" value={todo.id}/>
|
||||||
|
<input type="submit" value="X"/>
|
||||||
|
</ActionForm>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect_view(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
})
|
};
|
||||||
.collect_view(cx)
|
|
||||||
};
|
|
||||||
|
|
||||||
view! {
|
let pending_todos = move || {
|
||||||
cx,
|
submissions
|
||||||
<ul>
|
.get()
|
||||||
{existing_todos}
|
.into_iter()
|
||||||
{pending_todos}
|
.filter(|submission| submission.pending().get())
|
||||||
</ul>
|
.map(|submission| {
|
||||||
|
view! {
|
||||||
|
cx,
|
||||||
|
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect_view(cx)
|
||||||
|
};
|
||||||
|
|
||||||
|
view! {
|
||||||
|
cx,
|
||||||
|
<ul>
|
||||||
|
{existing_todos}
|
||||||
|
{pending_todos}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
</ErrorBoundary>
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,22 +87,27 @@ fn Post(cx: Scope) -> impl IntoView {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let post_view = move || {
|
// this view needs to take the `Scope` from the `<Suspense/>`, not
|
||||||
post.with(cx, |post| {
|
// from the parent component, so we take that as an argument and
|
||||||
post.clone().map(|post| {
|
// pass it in under the `<Suspense/>` so that it is correct
|
||||||
view! { cx,
|
let post_view = move |cx| {
|
||||||
// render content
|
move || {
|
||||||
<h1>{&post.title}</h1>
|
post.with(cx, |post| {
|
||||||
<p>{&post.content}</p>
|
post.clone().map(|post| {
|
||||||
|
view! { cx,
|
||||||
|
// render content
|
||||||
|
<h1>{&post.title}</h1>
|
||||||
|
<p>{&post.content}</p>
|
||||||
|
|
||||||
// since we're using async rendering for this page,
|
// since we're using async rendering for this page,
|
||||||
// this metadata should be included in the actual HTML <head>
|
// this metadata should be included in the actual HTML <head>
|
||||||
// when it's first served
|
// when it's first served
|
||||||
<Title text=post.title/>
|
<Title text=post.title/>
|
||||||
<Meta name="description" content=post.content/>
|
<Meta name="description" content=post.content/>
|
||||||
}
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
view! { cx,
|
view! { cx,
|
||||||
|
@ -121,7 +126,7 @@ fn Post(cx: Scope) -> impl IntoView {
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}>
|
}>
|
||||||
{post_view}
|
{post_view(cx)}
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
}
|
}
|
||||||
|
|
|
@ -115,9 +115,7 @@ pub fn TodoApp(cx: Scope) -> impl IntoView {
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="" view=|cx| view! { cx,
|
<Route path="" view=|cx| view! { cx, <Todos/> }/>
|
||||||
<Todos/>
|
|
||||||
}/>
|
|
||||||
</Routes>
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
</Router>
|
</Router>
|
||||||
|
|
|
@ -117,9 +117,7 @@ pub fn TodoApp(cx: Scope) -> impl IntoView {
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="" view=|cx| view! {
|
<Route path="" view=|cx| view! {
|
||||||
cx,
|
cx,
|
||||||
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors=errors/>}>
|
|
||||||
<Todos/>
|
<Todos/>
|
||||||
</ErrorBoundary>
|
|
||||||
}/> //Route
|
}/> //Route
|
||||||
</Routes>
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
|
@ -151,63 +149,65 @@ pub fn Todos(cx: Scope) -> impl IntoView {
|
||||||
<input type="submit" value="Add"/>
|
<input type="submit" value="Add"/>
|
||||||
</MultiActionForm>
|
</MultiActionForm>
|
||||||
<Transition fallback=move || view! {cx, <p>"Loading..."</p> }>
|
<Transition fallback=move || view! {cx, <p>"Loading..."</p> }>
|
||||||
{move || {
|
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors/>}>
|
||||||
let existing_todos = {
|
{move || {
|
||||||
move || {
|
let existing_todos = {
|
||||||
todos.read(cx)
|
move || {
|
||||||
.map(move |todos| match todos {
|
todos.read(cx)
|
||||||
Err(e) => {
|
.map(move |todos| match todos {
|
||||||
view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_view(cx)
|
Err(e) => {
|
||||||
}
|
view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_view(cx)
|
||||||
Ok(todos) => {
|
|
||||||
if todos.is_empty() {
|
|
||||||
view! { cx, <p>"No tasks were found."</p> }.into_view(cx)
|
|
||||||
} else {
|
|
||||||
todos
|
|
||||||
.into_iter()
|
|
||||||
.map(move |todo| {
|
|
||||||
view! {
|
|
||||||
cx,
|
|
||||||
<li>
|
|
||||||
{todo.title}
|
|
||||||
<ActionForm action=delete_todo>
|
|
||||||
<input type="hidden" name="id" value={todo.id}/>
|
|
||||||
<input type="submit" value="X"/>
|
|
||||||
</ActionForm>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect_view(cx)
|
|
||||||
}
|
}
|
||||||
}
|
Ok(todos) => {
|
||||||
})
|
if todos.is_empty() {
|
||||||
.unwrap_or_default()
|
view! { cx, <p>"No tasks were found."</p> }.into_view(cx)
|
||||||
}
|
} else {
|
||||||
};
|
todos
|
||||||
|
.into_iter()
|
||||||
let pending_todos = move || {
|
.map(move |todo| {
|
||||||
submissions
|
view! {
|
||||||
.get()
|
cx,
|
||||||
.into_iter()
|
<li>
|
||||||
.filter(|submission| submission.pending().get())
|
{todo.title}
|
||||||
.map(|submission| {
|
<ActionForm action=delete_todo>
|
||||||
view! {
|
<input type="hidden" name="id" value={todo.id}/>
|
||||||
cx,
|
<input type="submit" value="X"/>
|
||||||
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
|
</ActionForm>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect_view(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
})
|
};
|
||||||
.collect_view(cx)
|
|
||||||
};
|
|
||||||
|
|
||||||
view! {
|
let pending_todos = move || {
|
||||||
cx,
|
submissions
|
||||||
<ul>
|
.get()
|
||||||
{existing_todos}
|
.into_iter()
|
||||||
{pending_todos}
|
.filter(|submission| submission.pending().get())
|
||||||
</ul>
|
.map(|submission| {
|
||||||
|
view! {
|
||||||
|
cx,
|
||||||
|
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect_view(cx)
|
||||||
|
};
|
||||||
|
|
||||||
|
view! {
|
||||||
|
cx,
|
||||||
|
<ul>
|
||||||
|
{existing_todos}
|
||||||
|
{pending_todos}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
</ErrorBoundary>
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,24 @@ use leptos_reactive::{
|
||||||
/// }
|
/// }
|
||||||
/// # });
|
/// # });
|
||||||
/// ```
|
/// ```
|
||||||
|
///
|
||||||
|
/// ## Interaction with `<Suspense/>`
|
||||||
|
/// If you use this with a `<Suspense/>` or `<Transition/>` component, note that the
|
||||||
|
/// `<ErrorBoundary/>` should go inside the `<Suspense/>`, not the other way around,
|
||||||
|
/// if there’s a chance that the `<ErrorBoundary/>` will begin in the error state.
|
||||||
|
/// This is a limitation of the current design of the two components and the way they
|
||||||
|
/// hydrate. Placing the `<ErrorBoundary/>` outside the `<Suspense/>` means that
|
||||||
|
/// it is rendered on the server without any knowledge of the suspended view, so it
|
||||||
|
/// will always be rendered on the server as if there were no errors, but might need
|
||||||
|
/// to be hydrated with errors, depending on the actual result.
|
||||||
|
///
|
||||||
|
/// ```rust,ignore
|
||||||
|
/// view! { cx,
|
||||||
|
/// <Suspense fallback=move || view! {cx, <p>"Loading..."</p> }>
|
||||||
|
/// <ErrorBoundary fallback=|cx, errors| view!{ cx, <ErrorTemplate errors=errors/>}>
|
||||||
|
/// {move || {
|
||||||
|
/// /* etc. */
|
||||||
|
/// ```
|
||||||
#[component]
|
#[component]
|
||||||
pub fn ErrorBoundary<F, IV>(
|
pub fn ErrorBoundary<F, IV>(
|
||||||
cx: Scope,
|
cx: Scope,
|
||||||
|
@ -46,7 +64,25 @@ where
|
||||||
provide_context(cx, errors);
|
provide_context(cx, errors);
|
||||||
|
|
||||||
// Run children so that they render and execute resources
|
// Run children so that they render and execute resources
|
||||||
let children = children(cx).into_view(cx);
|
let children = children(cx);
|
||||||
|
|
||||||
|
#[cfg(all(debug_assertions, feature = "hydrate"))]
|
||||||
|
{
|
||||||
|
use leptos_dom::View;
|
||||||
|
if children.nodes.iter().any(|child| {
|
||||||
|
matches!(child, View::Suspense(_, _))
|
||||||
|
|| matches!(child, View::Component(repr) if repr.name() == "Transition")
|
||||||
|
}) {
|
||||||
|
crate::debug_warn!("You are using a <Suspense/> or \
|
||||||
|
<Transition/> as the direct child of an <ErrorBoundary/>. To ensure correct \
|
||||||
|
hydration, these should be reorganized so that the <ErrorBoundary/> is a child \
|
||||||
|
of the <Suspense/> or <Transition/> instead: \n\
|
||||||
|
\nview! {{ cx,\
|
||||||
|
\n <Suspense fallback=todo!()>\n <ErrorBoundary fallback=todo!()>\n {{move || {{ /* etc. */")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let children = children.into_view(cx);
|
||||||
let errors_empty = create_memo(cx, move |_| errors.with(Errors::is_empty));
|
let errors_empty = create_memo(cx, move |_| errors.with(Errors::is_empty));
|
||||||
|
|
||||||
move || {
|
move || {
|
||||||
|
|
|
@ -221,6 +221,12 @@ impl ComponentRepr {
|
||||||
view_marker: None,
|
view_marker: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(debug_assertions, feature = "ssr"))]
|
||||||
|
/// Returns the name of the component.
|
||||||
|
pub fn name(&self) -> &str {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A user-defined `leptos` component.
|
/// A user-defined `leptos` component.
|
||||||
|
|
Loading…
Reference in New Issue