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>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="" view=|cx| view! {
|
||||
cx,
|
||||
<ExampleErrors/>
|
||||
}/>
|
||||
<Route path="" view=|cx| view! { cx, <ExampleErrors/> }/>
|
||||
</Routes>
|
||||
</main>
|
||||
</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
|
||||
// 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.
|
||||
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors=errors/>}>
|
||||
<ErrorBoundary fallback=|cx, errors| view!{ cx, <ErrorTemplate errors=errors/>}>
|
||||
<ReturnsError/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
|
|
@ -177,12 +177,7 @@ pub fn TodoApp(cx: Scope) -> impl IntoView {
|
|||
<hr/>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="" view=|cx| view! {
|
||||
cx,
|
||||
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors=errors/>}>
|
||||
<Todos/>
|
||||
</ErrorBoundary>
|
||||
}/> //Route
|
||||
<Route path="" view=|cx| view! { cx, <Todos/> }/> //Route
|
||||
<Route path="signup" view=move |cx| view! {
|
||||
cx,
|
||||
<Signup action=signup/>
|
||||
|
@ -226,69 +221,71 @@ pub fn Todos(cx: Scope) -> impl IntoView {
|
|||
<input type="submit" value="Add"/>
|
||||
</MultiActionForm>
|
||||
<Transition fallback=move || view! {cx, <p>"Loading..."</p> }>
|
||||
{move || {
|
||||
let existing_todos = {
|
||||
move || {
|
||||
todos.read(cx)
|
||||
.map(move |todos| match todos {
|
||||
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)
|
||||
<ErrorBoundary fallback=|cx, errors| view!{ cx, <ErrorTemplate errors=errors/>}>
|
||||
{move || {
|
||||
let existing_todos = {
|
||||
move || {
|
||||
todos.read(cx)
|
||||
.map(move |todos| match todos {
|
||||
Err(e) => {
|
||||
view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_view(cx)
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
};
|
||||
|
||||
let pending_todos = move || {
|
||||
submissions
|
||||
.get()
|
||||
.into_iter()
|
||||
.filter(|submission| submission.pending().get())
|
||||
.map(|submission| {
|
||||
view! {
|
||||
cx,
|
||||
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
})
|
||||
.collect_view(cx)
|
||||
};
|
||||
};
|
||||
|
||||
view! {
|
||||
cx,
|
||||
<ul>
|
||||
{existing_todos}
|
||||
{pending_todos}
|
||||
</ul>
|
||||
let pending_todos = move || {
|
||||
submissions
|
||||
.get()
|
||||
.into_iter()
|
||||
.filter(|submission| submission.pending().get())
|
||||
.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>
|
||||
</div>
|
||||
}
|
||||
|
|
|
@ -87,22 +87,27 @@ fn Post(cx: Scope) -> impl IntoView {
|
|||
}
|
||||
});
|
||||
|
||||
let post_view = move || {
|
||||
post.with(cx, |post| {
|
||||
post.clone().map(|post| {
|
||||
view! { cx,
|
||||
// render content
|
||||
<h1>{&post.title}</h1>
|
||||
<p>{&post.content}</p>
|
||||
// this view needs to take the `Scope` from the `<Suspense/>`, not
|
||||
// from the parent component, so we take that as an argument and
|
||||
// pass it in under the `<Suspense/>` so that it is correct
|
||||
let post_view = move |cx| {
|
||||
move || {
|
||||
post.with(cx, |post| {
|
||||
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,
|
||||
// this metadata should be included in the actual HTML <head>
|
||||
// when it's first served
|
||||
<Title text=post.title/>
|
||||
<Meta name="description" content=post.content/>
|
||||
}
|
||||
// since we're using async rendering for this page,
|
||||
// this metadata should be included in the actual HTML <head>
|
||||
// when it's first served
|
||||
<Title text=post.title/>
|
||||
<Meta name="description" content=post.content/>
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
|
@ -121,7 +126,7 @@ fn Post(cx: Scope) -> impl IntoView {
|
|||
</div>
|
||||
}
|
||||
}>
|
||||
{post_view}
|
||||
{post_view(cx)}
|
||||
</ErrorBoundary>
|
||||
</Suspense>
|
||||
}
|
||||
|
|
|
@ -115,9 +115,7 @@ pub fn TodoApp(cx: Scope) -> impl IntoView {
|
|||
</header>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="" view=|cx| view! { cx,
|
||||
<Todos/>
|
||||
}/>
|
||||
<Route path="" view=|cx| view! { cx, <Todos/> }/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
|
|
|
@ -117,9 +117,7 @@ pub fn TodoApp(cx: Scope) -> impl IntoView {
|
|||
<Routes>
|
||||
<Route path="" view=|cx| view! {
|
||||
cx,
|
||||
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors=errors/>}>
|
||||
<Todos/>
|
||||
</ErrorBoundary>
|
||||
}/> //Route
|
||||
</Routes>
|
||||
</main>
|
||||
|
@ -151,63 +149,65 @@ pub fn Todos(cx: Scope) -> impl IntoView {
|
|||
<input type="submit" value="Add"/>
|
||||
</MultiActionForm>
|
||||
<Transition fallback=move || view! {cx, <p>"Loading..."</p> }>
|
||||
{move || {
|
||||
let existing_todos = {
|
||||
move || {
|
||||
todos.read(cx)
|
||||
.map(move |todos| match todos {
|
||||
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)
|
||||
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors/>}>
|
||||
{move || {
|
||||
let existing_todos = {
|
||||
move || {
|
||||
todos.read(cx)
|
||||
.map(move |todos| match todos {
|
||||
Err(e) => {
|
||||
view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_view(cx)
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
};
|
||||
|
||||
let pending_todos = move || {
|
||||
submissions
|
||||
.get()
|
||||
.into_iter()
|
||||
.filter(|submission| submission.pending().get())
|
||||
.map(|submission| {
|
||||
view! {
|
||||
cx,
|
||||
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
})
|
||||
.collect_view(cx)
|
||||
};
|
||||
};
|
||||
|
||||
view! {
|
||||
cx,
|
||||
<ul>
|
||||
{existing_todos}
|
||||
{pending_todos}
|
||||
</ul>
|
||||
let pending_todos = move || {
|
||||
submissions
|
||||
.get()
|
||||
.into_iter()
|
||||
.filter(|submission| submission.pending().get())
|
||||
.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>
|
||||
</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]
|
||||
pub fn ErrorBoundary<F, IV>(
|
||||
cx: Scope,
|
||||
|
@ -46,7 +64,25 @@ where
|
|||
provide_context(cx, errors);
|
||||
|
||||
// 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));
|
||||
|
||||
move || {
|
||||
|
|
|
@ -221,6 +221,12 @@ impl ComponentRepr {
|
|||
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.
|
||||
|
|
Loading…
Reference in New Issue