Suspense Support (#2212)

* Make Html a Result.

* Fix tests.

* Implement Suspense.

* Schedule render when suspension is resumed.

* Shift children into a detached node.

* styled example.

* Update wording a little bit.

* Move hint to hint.

* Add some tests.

* Fix clippy.

* Add docs.

* Add to sidebar.

* Fix syntax highlight.

* Component -> BaseComponent.

* Html -> VNode, HtmlResult = RenderResult<Html>.

* Suspendible Function Component.

* Add a method to create suspension from futures.

* Revert extra changes.

* Fix tests.

* Update documentation.

* Switch to custom trait to make test reliable.

* Fix file permission.

* Fix docs.

* Remove log.

* Fix file permission.

* Fix component name error.

* Make Suspension a future.
This commit is contained in:
Kaede Hoshikawa 2022-01-05 22:16:34 +09:00 committed by GitHub
parent eb23b327d9
commit ac3af0a9bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
55 changed files with 1985 additions and 430 deletions

View File

@ -31,6 +31,7 @@ members = [
"examples/two_apps",
"examples/webgl",
"examples/web_worker_fib",
"examples/suspense",
# Tools
"tools/changelog",

View File

@ -2,7 +2,7 @@ use crate::hooks::use_bool_toggle::use_bool_toggle;
use crate::state::Entry as Item;
use web_sys::{HtmlInputElement, MouseEvent};
use yew::events::{Event, FocusEvent, KeyboardEvent};
use yew::{function_component, html, Callback, Classes, Properties, TargetCast};
use yew::prelude::*;
#[derive(PartialEq, Properties, Clone)]
pub struct EntryProps {

View File

@ -1,5 +1,5 @@
use crate::state::Filter as FilterEnum;
use yew::{function_component, html, Callback, Properties};
use yew::prelude::*;
#[derive(PartialEq, Properties)]
pub struct FilterProps {

View File

@ -1,6 +1,6 @@
use web_sys::HtmlInputElement;
use yew::events::KeyboardEvent;
use yew::{function_component, html, Callback, Properties, TargetCast};
use yew::prelude::*;
#[derive(PartialEq, Properties, Clone)]
pub struct HeaderInputProps {

View File

@ -1,4 +1,4 @@
use yew::{function_component, html};
use yew::prelude::*;
#[function_component(InfoFooter)]
pub fn info_footer() -> Html {

View File

@ -1,7 +1,7 @@
use gloo::storage::{LocalStorage, Storage};
use state::{Action, Filter, State};
use strum::IntoEnumIterator;
use yew::{classes, function_component, html, use_effect_with_deps, use_reducer, Callback};
use yew::prelude::*;
mod components;
mod hooks;

View File

@ -0,0 +1,19 @@
[package]
name = "suspense"
version = "0.1.0"
edition = "2018"
license = "MIT OR Apache-2.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
yew = { path = "../../packages/yew" }
gloo-timers = { version = "0.2.2", features = ["futures"] }
wasm-bindgen-futures = "0.4"
wasm-bindgen = "0.2"
[dependencies.web-sys]
version = "0.3"
features = [
"HtmlTextAreaElement",
]

View File

@ -0,0 +1,10 @@
# Suspense Example
[![Demo](https://img.shields.io/website?label=demo&url=https%3A%2F%2Fexamples.yew.rs%2Fsuspense)](https://examples.yew.rs/suspense)
This is an example that demonstrates `<Suspense />` support.
## Concepts
This example shows that how `<Suspense />` works in Yew and how you can
create hooks that utilises suspense.

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Yew Suspense Demo</title>
<link data-trunk rel="sass" href="index.scss" />
</head>
<body></body>
</html>

View File

@ -0,0 +1,71 @@
html, body {
font-family: sans-serif;
margin: 0;
padding: 0;
background-color: rgb(237, 244, 255);
}
.layout {
height: 100vh;
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.content {
height: 600px;
width: 600px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border-radius: 4px;
box-shadow: 0 0 5px 0 black;
background: white;
}
.content-area {
width: 350px;
height: 500px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
textarea {
width: 300px;
height: 300px;
font-size: 15px;
}
.action-area {
padding-top: 40px;
}
button {
color: white;
height: 50px;
width: 300px;
font-size: 20px;
background-color: rgb(88, 164, 255);
border-radius: 5px;
border: none;
}
.hint {
padding-top: 20px;
font-size: 12px;
text-align: center;
color: rgb(100, 100, 100);
}

View File

@ -0,0 +1,60 @@
use web_sys::HtmlTextAreaElement;
use yew::prelude::*;
mod use_sleep;
use use_sleep::use_sleep;
#[function_component(PleaseWait)]
fn please_wait() -> Html {
html! {<div class="content-area">{"Please wait 5 Seconds..."}</div>}
}
#[function_component(AppContent)]
fn app_content() -> HtmlResult {
let resleep = use_sleep()?;
let value = use_state(|| "I am writing a long story...".to_string());
let on_text_input = {
let value = value.clone();
Callback::from(move |e: InputEvent| {
let input: HtmlTextAreaElement = e.target_unchecked_into();
value.set(input.value());
})
};
let on_take_a_break = Callback::from(move |_| (resleep.clone())());
Ok(html! {
<div class="content-area">
<textarea value={value.to_string()} oninput={on_text_input}></textarea>
<div class="action-area">
<button onclick={on_take_a_break}>{"Take a break!"}</button>
<div class="hint">{"You can take a break at anytime"}<br />{"and your work will be preserved."}</div>
</div>
</div>
})
}
#[function_component(App)]
fn app() -> Html {
let fallback = html! {<PleaseWait />};
html! {
<div class="layout">
<div class="content">
<h1>{"Yew Suspense Demo"}</h1>
<Suspense fallback={fallback}>
<AppContent />
</Suspense>
</div>
</div>
}
}
fn main() {
yew::start_app::<App>();
}

View File

@ -0,0 +1,39 @@
use std::rc::Rc;
use std::time::Duration;
use gloo_timers::future::sleep;
use yew::prelude::*;
use yew::suspense::{Suspension, SuspensionResult};
#[derive(PartialEq)]
pub struct SleepState {
s: Suspension,
}
impl SleepState {
fn new() -> Self {
let s = Suspension::from_future(async {
sleep(Duration::from_secs(5)).await;
});
Self { s }
}
}
impl Reducible for SleepState {
type Action = ();
fn reduce(self: Rc<Self>, _action: Self::Action) -> Rc<Self> {
Self::new().into()
}
}
pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> {
let sleep_state = use_reducer(SleepState::new);
if sleep_state.s.resumed() {
Ok(Rc::new(move || sleep_state.dispatch(())))
} else {
Err(sleep_state.s.clone())
}
}

View File

@ -1,11 +1,11 @@
use proc_macro2::TokenStream;
use quote::{format_ident, quote, quote_spanned, ToTokens};
use proc_macro2::{Span, TokenStream};
use quote::{format_ident, quote, ToTokens};
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::token::Comma;
use syn::token::{Comma, Fn};
use syn::{Attribute, Block, FnArg, Generics, Ident, Item, ItemFn, ReturnType, Type, Visibility};
#[derive(Clone)]
pub struct FunctionComponent {
block: Box<Block>,
props_type: Box<Type>,
@ -15,127 +15,132 @@ pub struct FunctionComponent {
attrs: Vec<Attribute>,
name: Ident,
return_type: Box<Type>,
fn_token: Fn,
}
impl Parse for FunctionComponent {
fn parse(input: ParseStream) -> syn::Result<Self> {
let parsed: Item = input.parse()?;
match parsed {
Item::Fn(func) => {
let ItemFn {
attrs,
vis,
let func = match parsed {
Item::Fn(m) => m,
item => {
return Err(syn::Error::new_spanned(
item,
"`function_component` attribute can only be applied to functions",
))
}
};
let ItemFn {
attrs,
vis,
sig,
block,
} = func;
if sig.generics.lifetimes().next().is_some() {
return Err(syn::Error::new_spanned(
sig.generics,
"function components can't have generic lifetime parameters",
));
}
if sig.asyncness.is_some() {
return Err(syn::Error::new_spanned(
sig.asyncness,
"function components can't be async",
));
}
if sig.constness.is_some() {
return Err(syn::Error::new_spanned(
sig.constness,
"const functions can't be function components",
));
}
if sig.abi.is_some() {
return Err(syn::Error::new_spanned(
sig.abi,
"extern functions can't be function components",
));
}
let return_type = match sig.output {
ReturnType::Default => {
return Err(syn::Error::new_spanned(
sig,
block,
} = func;
"function components must return `yew::Html` or `yew::HtmlResult`",
))
}
ReturnType::Type(_, ty) => ty,
};
if sig.generics.lifetimes().next().is_some() {
return Err(syn::Error::new_spanned(
sig.generics,
"function components can't have generic lifetime parameters",
));
}
let mut inputs = sig.inputs.into_iter();
let arg = inputs
.next()
.unwrap_or_else(|| syn::parse_quote! { _: &() });
if sig.asyncness.is_some() {
return Err(syn::Error::new_spanned(
sig.asyncness,
"function components can't be async",
));
}
if sig.constness.is_some() {
return Err(syn::Error::new_spanned(
sig.constness,
"const functions can't be function components",
));
}
if sig.abi.is_some() {
return Err(syn::Error::new_spanned(
sig.abi,
"extern functions can't be function components",
));
}
let return_type = match sig.output {
ReturnType::Default => {
let ty = match &arg {
FnArg::Typed(arg) => match &*arg.ty {
Type::Reference(ty) => {
if ty.lifetime.is_some() {
return Err(syn::Error::new_spanned(
sig,
"function components must return `yew::Html`",
))
}
ReturnType::Type(_, ty) => ty,
};
let mut inputs = sig.inputs.into_iter();
let arg: FnArg = inputs
.next()
.unwrap_or_else(|| syn::parse_quote! { _: &() });
let ty = match &arg {
FnArg::Typed(arg) => match &*arg.ty {
Type::Reference(ty) => {
if ty.lifetime.is_some() {
return Err(syn::Error::new_spanned(
&ty.lifetime,
"reference must not have a lifetime",
));
}
if ty.mutability.is_some() {
return Err(syn::Error::new_spanned(
&ty.mutability,
"reference must not be mutable",
));
}
ty.elem.clone()
}
ty => {
let msg = format!(
"expected a reference to a `Properties` type (try: `&{}`)",
ty.to_token_stream()
);
return Err(syn::Error::new_spanned(ty, msg));
}
},
FnArg::Receiver(_) => {
return Err(syn::Error::new_spanned(
arg,
"function components can't accept a receiver",
&ty.lifetime,
"reference must not have a lifetime",
));
}
};
// Checking after param parsing may make it a little inefficient
// but that's a requirement for better error messages in case of receivers
// `>0` because first one is already consumed.
if inputs.len() > 0 {
let params: TokenStream = inputs.map(|it| it.to_token_stream()).collect();
return Err(syn::Error::new_spanned(
params,
"function components can accept at most one parameter for the props",
));
if ty.mutability.is_some() {
return Err(syn::Error::new_spanned(
&ty.mutability,
"reference must not be mutable",
));
}
ty.elem.clone()
}
ty => {
let msg = format!(
"expected a reference to a `Properties` type (try: `&{}`)",
ty.to_token_stream()
);
return Err(syn::Error::new_spanned(ty, msg));
}
},
Ok(Self {
props_type: ty,
block,
FnArg::Receiver(_) => {
return Err(syn::Error::new_spanned(
arg,
generics: sig.generics,
vis,
attrs,
name: sig.ident,
return_type,
})
"function components can't accept a receiver",
));
}
item => Err(syn::Error::new_spanned(
item,
"`function_component` attribute can only be applied to functions",
)),
};
// Checking after param parsing may make it a little inefficient
// but that's a requirement for better error messages in case of receivers
// `>0` because first one is already consumed.
if inputs.len() > 0 {
let params: TokenStream = inputs.map(|it| it.to_token_stream()).collect();
return Err(syn::Error::new_spanned(
params,
"function components can accept at most one parameter for the props",
));
}
Ok(Self {
props_type: ty,
block,
arg,
generics: sig.generics,
vis,
attrs,
name: sig.ident,
return_type,
fn_token: sig.fn_token,
})
}
}
@ -159,63 +164,104 @@ impl Parse for FunctionComponentName {
}
}
fn print_fn(func_comp: FunctionComponent, use_fn_name: bool) -> TokenStream {
let FunctionComponent {
fn_token,
name,
attrs,
block,
return_type,
generics,
arg,
..
} = func_comp;
let (_impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let name = if use_fn_name {
name
} else {
Ident::new("inner", Span::mixed_site())
};
quote! {
#(#attrs)*
#fn_token #name #ty_generics (#arg) -> #return_type
#where_clause
{
#block
}
}
}
pub fn function_component_impl(
name: FunctionComponentName,
component: FunctionComponent,
) -> syn::Result<TokenStream> {
let FunctionComponentName { component_name } = name;
let has_separate_name = component_name.is_some();
let func = print_fn(component.clone(), has_separate_name);
let FunctionComponent {
block,
props_type,
arg,
generics,
vis,
attrs,
name: function_name,
return_type,
..
} = component;
let component_name = component_name.unwrap_or_else(|| function_name.clone());
let function_name = format_ident!(
let provider_name = format_ident!(
"{}FunctionProvider",
function_name,
span = function_name.span()
component_name,
span = Span::mixed_site()
);
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
if function_name == component_name {
if has_separate_name && function_name == component_name {
return Err(syn::Error::new_spanned(
component_name,
"the component must not have the same name as the function",
));
}
let ret_type = quote_spanned!(return_type.span()=> ::yew::html::Html);
let phantom_generics = generics
.type_params()
.map(|ty_param| ty_param.ident.clone()) // create a new Punctuated sequence without any type bounds
.collect::<Punctuated<_, Comma>>();
let provider_props = Ident::new("props", Span::mixed_site());
let fn_generics = ty_generics.as_turbofish();
let fn_name = if has_separate_name {
function_name
} else {
Ident::new("inner", Span::mixed_site())
};
let quoted = quote! {
#[doc(hidden)]
#[allow(non_camel_case_types)]
#[allow(unused_parens)]
#vis struct #function_name #generics {
#vis struct #provider_name #ty_generics {
_marker: ::std::marker::PhantomData<(#phantom_generics)>,
}
impl #impl_generics ::yew::functional::FunctionProvider for #function_name #ty_generics #where_clause {
#[automatically_derived]
impl #impl_generics ::yew::functional::FunctionProvider for #provider_name #ty_generics #where_clause {
type TProps = #props_type;
fn run(#arg) -> #ret_type {
#block
fn run(#provider_props: &Self::TProps) -> ::yew::html::HtmlResult {
#func
::yew::html::IntoHtmlResult::into_html_result(#fn_name #fn_generics (#provider_props))
}
}
#(#attrs)*
#[allow(type_alias_bounds)]
#vis type #component_name #generics = ::yew::functional::FunctionComponent<#function_name #ty_generics>;
#vis type #component_name #generics = ::yew::functional::FunctionComponent<#provider_name #ty_generics>;
};
Ok(quoted)

View File

@ -92,7 +92,7 @@ impl ToTokens for HtmlComponent {
children,
} = self;
let props_ty = quote_spanned!(ty.span()=> <#ty as ::yew::html::Component>::Properties);
let props_ty = quote_spanned!(ty.span()=> <#ty as ::yew::html::BaseComponent>::Properties);
let children_renderer = if children.is_empty() {
None
} else {

View File

@ -16,10 +16,8 @@ error: expected identifier
26 | #[function_component(124)]
| ^^^
warning: type `component` should have an upper camel case name
error: the component must not have the same name as the function
--> tests/function_component_attr/bad-name-fail.rs:35:22
|
35 | #[function_component(component)]
| ^^^^^^^^^ help: convert the identifier to upper camel case (notice the capitalization): `Component`
|
= note: `#[warn(non_camel_case_types)]` on by default
| ^^^^^^^^^

View File

@ -1,13 +1,14 @@
error: function components must return `yew::Html`
--> $DIR/bad-return-type-fail.rs:9:1
error: function components must return `yew::Html` or `yew::HtmlResult`
--> tests/function_component_attr/bad-return-type-fail.rs:9:1
|
9 | fn comp_1(_props: &Props) {}
| ^^^^^^^^^^^^^^^^^^^^^^^^^
error[E0308]: mismatched types
--> $DIR/bad-return-type-fail.rs:13:5
error[E0277]: the trait bound `u32: IntoHtmlResult` is not satisfied
--> tests/function_component_attr/bad-return-type-fail.rs:11:1
|
12 | fn comp(_props: &Props) -> u32 {
| --- expected `VNode` because of return type
13 | 1
| ^ expected enum `VNode`, found integer
11 | #[function_component(Comp)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `IntoHtmlResult` is not implemented for `u32`
|
= note: required by `into_html_result`
= note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info)

View File

@ -58,20 +58,21 @@ fn comp1<T1, T2>(_props: &()) -> ::yew::Html {
}
}
#[::yew::function_component(ConstGenerics)]
fn const_generics<const N: ::std::primitive::i32>() -> ::yew::Html {
::yew::html! {
<div>
{ N }
</div>
}
}
// no longer possible?
// #[::yew::function_component(ConstGenerics)]
// fn const_generics<const N: ::std::primitive::i32>() -> ::yew::Html {
// ::yew::html! {
// <div>
// { N }
// </div>
// }
// }
fn compile_pass() {
::yew::html! { <Comp<Props> a=10 /> };
::yew::html! { <Comp1<::std::primitive::usize, ::std::primitive::usize> /> };
::yew::html! { <ConstGenerics<10> /> };
// ::yew::html! { <ConstGenerics<10> /> };
}
fn main() {}

View File

@ -16,27 +16,41 @@ error[E0599]: no method named `build` found for struct `PropsBuilder<PropsBuilde
22 | html! { <Comp<Props> /> };
| ^^^^ method not found in `PropsBuilder<PropsBuilderStep_missing_required_prop_a>`
error[E0277]: the trait bound `FunctionComponent<CompFunctionProvider<MissingTypeBounds>>: BaseComponent` is not satisfied
--> tests/function_component_attr/generic-props-fail.rs:27:14
|
27 | html! { <Comp<MissingTypeBounds> /> };
| ^^^^ the trait `BaseComponent` is not implemented for `FunctionComponent<CompFunctionProvider<MissingTypeBounds>>`
|
= help: the following implementations were found:
<FunctionComponent<T> as BaseComponent>
error[E0599]: the function or associated item `new` exists for struct `VChild<FunctionComponent<CompFunctionProvider<MissingTypeBounds>>>`, but its trait bounds were not satisfied
--> tests/function_component_attr/generic-props-fail.rs:27:14
|
27 | html! { <Comp<MissingTypeBounds> /> };
| ^^^^ function or associated item cannot be called on `VChild<FunctionComponent<CompFunctionProvider<MissingTypeBounds>>>` due to unsatisfied trait bounds
|
::: $WORKSPACE/packages/yew/src/functional/mod.rs
|
| pub struct FunctionComponent<T: FunctionProvider + 'static> {
| ----------------------------------------------------------- doesn't satisfy `_: BaseComponent`
|
= note: the following trait bounds were not satisfied:
`FunctionComponent<CompFunctionProvider<MissingTypeBounds>>: BaseComponent`
error[E0277]: the trait bound `MissingTypeBounds: yew::Properties` is not satisfied
--> tests/function_component_attr/generic-props-fail.rs:27:14
|
27 | html! { <Comp<MissingTypeBounds> /> };
| ^^^^ the trait `yew::Properties` is not implemented for `MissingTypeBounds`
|
= note: required because of the requirements on the impl of `FunctionProvider` for `compFunctionProvider<MissingTypeBounds>`
error[E0599]: the function or associated item `new` exists for struct `VChild<FunctionComponent<compFunctionProvider<MissingTypeBounds>>>`, but its trait bounds were not satisfied
--> tests/function_component_attr/generic-props-fail.rs:27:14
|
27 | html! { <Comp<MissingTypeBounds> /> };
| ^^^^ function or associated item cannot be called on `VChild<FunctionComponent<compFunctionProvider<MissingTypeBounds>>>` due to unsatisfied trait bounds
|
::: $WORKSPACE/packages/yew/src/functional/mod.rs
|
| pub struct FunctionComponent<T: FunctionProvider + 'static> {
| ----------------------------------------------------------- doesn't satisfy `_: yew::Component`
| ---------------- required by this bound in `FunctionComponent`
|
= note: the following trait bounds were not satisfied:
`FunctionComponent<compFunctionProvider<MissingTypeBounds>>: yew::Component`
= note: required because of the requirements on the impl of `FunctionProvider` for `CompFunctionProvider<MissingTypeBounds>`
error[E0107]: missing generics for type alias `Comp`
--> tests/function_component_attr/generic-props-fail.rs:30:14

View File

@ -3,15 +3,17 @@ error[E0277]: the trait bound `Unimplemented: yew::Component` is not satisfied
|
6 | html! { <Unimplemented /> };
| ^^^^^^^^^^^^^ the trait `yew::Component` is not implemented for `Unimplemented`
|
= note: required because of the requirements on the impl of `BaseComponent` for `Unimplemented`
error[E0599]: the function or associated item `new` exists for struct `VChild<Unimplemented>`, but its trait bounds were not satisfied
--> tests/html_macro/component-unimplemented-fail.rs:6:14
|
3 | struct Unimplemented;
| --------------------- doesn't satisfy `Unimplemented: yew::Component`
| --------------------- doesn't satisfy `Unimplemented: BaseComponent`
...
6 | html! { <Unimplemented /> };
| ^^^^^^^^^^^^^ function or associated item cannot be called on `VChild<Unimplemented>` due to unsatisfied trait bounds
|
= note: the following trait bounds were not satisfied:
`Unimplemented: yew::Component`
`Unimplemented: BaseComponent`

View File

@ -25,6 +25,7 @@ slab = "0.4"
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
yew-macro = { version = "^0.19.0", path = "../yew-macro" }
thiserror = "1.0"
scoped-tls-hkt = "0.1"
@ -62,6 +63,7 @@ features = [
[dev-dependencies]
easybench-wasm = "0.2"
wasm-bindgen-test = "0.3"
gloo = { version = "0.4", features = ["futures"] }
[features]
doc_test = []

View File

@ -3,21 +3,21 @@
use std::ops::Deref;
use crate::html::{Component, NodeRef, Scope, Scoped};
use crate::html::{BaseComponent, NodeRef, Scope, Scoped};
use gloo_utils::document;
use std::rc::Rc;
use web_sys::Element;
/// An instance of an application.
#[derive(Debug)]
pub struct AppHandle<COMP: Component> {
pub struct AppHandle<COMP: BaseComponent> {
/// `Scope` holder
pub(crate) scope: Scope<COMP>,
}
impl<COMP> AppHandle<COMP>
where
COMP: Component,
COMP: BaseComponent,
{
/// The main entry point of a Yew program which also allows passing properties. It works
/// similarly to the `program` function in Elm. You should provide an initial model, `update`
@ -56,7 +56,7 @@ where
impl<COMP> Deref for AppHandle<COMP>
where
COMP: Component,
COMP: BaseComponent,
{
type Target = Scope<COMP>;

View File

@ -77,7 +77,8 @@ pub fn use_ref<T: 'static>(initial_value: impl FnOnce() -> T) -> Rc<T> {
/// ```rust
/// # use wasm_bindgen::{prelude::Closure, JsCast};
/// # use yew::{
/// # function_component, html, use_effect_with_deps, use_node_ref
/// # function_component, html, use_effect_with_deps, use_node_ref,
/// # Html,
/// # };
/// # use web_sys::{Event, HtmlElement};
///

View File

@ -1,6 +1,6 @@
//! Function components are a simplified version of normal components.
//! They consist of a single function annotated with the attribute `#[function_component(_)]`
//! that receives props and determines what should be rendered by returning [`Html`].
//! that receives props and determines what should be rendered by returning [`Html`](crate::Html).
//!
//! ```rust
//! # use yew::prelude::*;
@ -13,8 +13,8 @@
//!
//! More details about function components and Hooks can be found on [Yew Docs](https://yew.rs/docs/next/concepts/function-components/introduction)
use crate::html::AnyScope;
use crate::{Component, Html, Properties};
use crate::html::{AnyScope, BaseComponent, HtmlResult};
use crate::Properties;
use scoped_tls_hkt::scoped_thread_local;
use std::cell::RefCell;
use std::fmt;
@ -70,10 +70,10 @@ pub trait FunctionProvider {
/// Properties for the Function Component.
type TProps: Properties + PartialEq;
/// Render the component. This function returns the [`Html`] to be rendered for the component.
/// Render the component. This function returns the [`Html`](crate::Html) to be rendered for the component.
///
/// Equivalent of [`Component::view`].
fn run(props: &Self::TProps) -> Html;
/// Equivalent of [`Component::view`](crate::html::Component::view).
fn run(props: &Self::TProps) -> HtmlResult;
}
/// Wrapper that allows a struct implementing [`FunctionProvider`] to be consumed as a component.
@ -100,7 +100,7 @@ where
}
}
impl<T: 'static> Component for FunctionComponent<T>
impl<T: 'static> BaseComponent for FunctionComponent<T>
where
T: FunctionProvider,
{
@ -137,7 +137,11 @@ where
msg()
}
fn view(&self, ctx: &Context<Self>) -> Html {
fn changed(&mut self, _ctx: &Context<Self>) -> bool {
true
}
fn view(&self, ctx: &Context<Self>) -> HtmlResult {
self.with_hook_state(|| T::run(&*ctx.props()))
}
@ -192,6 +196,7 @@ pub struct HookUpdater {
hook: Rc<RefCell<dyn std::any::Any>>,
process_message: ProcessMessage,
}
impl HookUpdater {
/// Callback which runs the hook.
pub fn callback<T: 'static, F>(&self, cb: F)

View File

@ -1,13 +1,16 @@
//! Component lifecycle module
use super::{Component, Scope};
use super::{AnyScope, BaseComponent, Scope};
use crate::html::RenderError;
use crate::scheduler::{self, Runnable, Shared};
use crate::suspense::{Suspense, Suspension};
use crate::virtual_dom::{VDiff, VNode};
use crate::Callback;
use crate::{Context, NodeRef};
use std::rc::Rc;
use web_sys::Element;
pub(crate) struct ComponentState<COMP: Component> {
pub(crate) struct ComponentState<COMP: BaseComponent> {
pub(crate) component: Box<COMP>,
pub(crate) root_node: VNode,
@ -17,12 +20,14 @@ pub(crate) struct ComponentState<COMP: Component> {
node_ref: NodeRef,
has_rendered: bool,
suspension: Option<Suspension>,
// Used for debug logging
#[cfg(debug_assertions)]
pub(crate) vcomp_id: u64,
}
impl<COMP: Component> ComponentState<COMP> {
impl<COMP: BaseComponent> ComponentState<COMP> {
pub(crate) fn new(
parent: Element,
next_sibling: NodeRef,
@ -47,6 +52,7 @@ impl<COMP: Component> ComponentState<COMP> {
parent,
next_sibling,
node_ref,
suspension: None,
has_rendered: false,
#[cfg(debug_assertions)]
@ -55,7 +61,7 @@ impl<COMP: Component> ComponentState<COMP> {
}
}
pub(crate) struct CreateRunner<COMP: Component> {
pub(crate) struct CreateRunner<COMP: BaseComponent> {
pub(crate) parent: Element,
pub(crate) next_sibling: NodeRef,
pub(crate) placeholder: VNode,
@ -64,7 +70,7 @@ pub(crate) struct CreateRunner<COMP: Component> {
pub(crate) scope: Scope<COMP>,
}
impl<COMP: Component> Runnable for CreateRunner<COMP> {
impl<COMP: BaseComponent> Runnable for CreateRunner<COMP> {
fn run(self: Box<Self>) {
let mut current_state = self.scope.state.borrow_mut();
if current_state.is_none() {
@ -83,21 +89,23 @@ impl<COMP: Component> Runnable for CreateRunner<COMP> {
}
}
pub(crate) enum UpdateEvent<COMP: Component> {
pub(crate) enum UpdateEvent<COMP: BaseComponent> {
/// Wraps messages for a component.
Message(COMP::Message),
/// Wraps batch of messages for a component.
MessageBatch(Vec<COMP::Message>),
/// Wraps properties, node ref, and next sibling for a component.
Properties(Rc<COMP::Properties>, NodeRef, NodeRef),
/// Shift Scope.
Shift(Element, NodeRef),
}
pub(crate) struct UpdateRunner<COMP: Component> {
pub(crate) struct UpdateRunner<COMP: BaseComponent> {
pub(crate) state: Shared<Option<ComponentState<COMP>>>,
pub(crate) event: UpdateEvent<COMP>,
}
impl<COMP: Component> Runnable for UpdateRunner<COMP> {
impl<COMP: BaseComponent> Runnable for UpdateRunner<COMP> {
fn run(self: Box<Self>) {
if let Some(mut state) = self.state.borrow_mut().as_mut() {
let schedule_render = match self.event {
@ -120,6 +128,16 @@ impl<COMP: Component> Runnable for UpdateRunner<COMP> {
false
}
}
UpdateEvent::Shift(parent, next_sibling) => {
state
.root_node
.shift(&state.parent, &parent, next_sibling.clone());
state.parent = parent;
state.next_sibling = next_sibling;
false
}
};
#[cfg(debug_assertions)]
@ -144,11 +162,11 @@ impl<COMP: Component> Runnable for UpdateRunner<COMP> {
}
}
pub(crate) struct DestroyRunner<COMP: Component> {
pub(crate) struct DestroyRunner<COMP: BaseComponent> {
pub(crate) state: Shared<Option<ComponentState<COMP>>>,
}
impl<COMP: Component> Runnable for DestroyRunner<COMP> {
impl<COMP: BaseComponent> Runnable for DestroyRunner<COMP> {
fn run(self: Box<Self>) {
if let Some(mut state) = self.state.borrow_mut().take() {
#[cfg(debug_assertions)]
@ -161,33 +179,99 @@ impl<COMP: Component> Runnable for DestroyRunner<COMP> {
}
}
pub(crate) struct RenderRunner<COMP: Component> {
pub(crate) struct RenderRunner<COMP: BaseComponent> {
pub(crate) state: Shared<Option<ComponentState<COMP>>>,
}
impl<COMP: Component> Runnable for RenderRunner<COMP> {
impl<COMP: BaseComponent> Runnable for RenderRunner<COMP> {
fn run(self: Box<Self>) {
if let Some(state) = self.state.borrow_mut().as_mut() {
#[cfg(debug_assertions)]
crate::virtual_dom::vcomp::log_event(state.vcomp_id, "render");
let mut new_root = state.component.view(&state.context);
std::mem::swap(&mut new_root, &mut state.root_node);
let ancestor = Some(new_root);
let new_root = &mut state.root_node;
let scope = state.context.scope.clone().into();
let next_sibling = state.next_sibling.clone();
let node = new_root.apply(&scope, &state.parent, next_sibling, ancestor);
state.node_ref.link(node);
match state.component.view(&state.context) {
Ok(m) => {
// Currently not suspended, we remove any previous suspension and update
// normally.
let mut root = m;
std::mem::swap(&mut root, &mut state.root_node);
if let Some(ref m) = state.suspension {
let comp_scope = AnyScope::from(state.context.scope.clone());
let suspense_scope = comp_scope.find_parent_scope::<Suspense>().unwrap();
let suspense = suspense_scope.get_component().unwrap();
suspense.resume(m.clone());
}
let ancestor = Some(root);
let new_root = &mut state.root_node;
let scope = state.context.scope.clone().into();
let next_sibling = state.next_sibling.clone();
let node = new_root.apply(&scope, &state.parent, next_sibling, ancestor);
state.node_ref.link(node);
}
Err(RenderError::Suspended(m)) => {
// Currently suspended, we re-use previous root node and send
// suspension to parent element.
let shared_state = self.state.clone();
if m.resumed() {
// schedule a render immediately if suspension is resumed.
scheduler::push_component_render(
shared_state.as_ptr() as usize,
RenderRunner {
state: shared_state.clone(),
},
RenderedRunner {
state: shared_state,
},
);
} else {
// We schedule a render after current suspension is resumed.
let comp_scope = AnyScope::from(state.context.scope.clone());
let suspense_scope = comp_scope.find_parent_scope::<Suspense>().unwrap();
let suspense = suspense_scope.get_component().unwrap();
m.listen(Callback::from(move |_| {
scheduler::push_component_render(
shared_state.as_ptr() as usize,
RenderRunner {
state: shared_state.clone(),
},
RenderedRunner {
state: shared_state.clone(),
},
);
}));
if let Some(ref last_m) = state.suspension {
if &m != last_m {
// We remove previous suspension from the suspense.
suspense.resume(last_m.clone());
}
}
state.suspension = Some(m.clone());
suspense.suspend(m);
}
}
};
}
}
}
pub(crate) struct RenderedRunner<COMP: Component> {
pub(crate) struct RenderedRunner<COMP: BaseComponent> {
pub(crate) state: Shared<Option<ComponentState<COMP>>>,
}
impl<COMP: Component> Runnable for RenderedRunner<COMP> {
impl<COMP: BaseComponent> Runnable for RenderedRunner<COMP> {
fn run(self: Box<Self>) {
if let Some(state) = self.state.borrow_mut().as_mut() {
#[cfg(debug_assertions)]

View File

@ -5,7 +5,7 @@ mod lifecycle;
mod properties;
mod scope;
use super::Html;
use super::{Html, HtmlResult, IntoHtmlResult};
pub use children::*;
pub use properties::*;
pub(crate) use scope::Scoped;
@ -15,12 +15,12 @@ use std::rc::Rc;
/// The [`Component`]'s context. This contains component's [`Scope`] and and props and
/// is passed to every lifecycle method.
#[derive(Debug)]
pub struct Context<COMP: Component> {
pub struct Context<COMP: BaseComponent> {
pub(crate) scope: Scope<COMP>,
pub(crate) props: Rc<COMP::Properties>,
}
impl<COMP: Component> Context<COMP> {
impl<COMP: BaseComponent> Context<COMP> {
/// The component scope
#[inline]
pub fn link(&self) -> &Scope<COMP> {
@ -34,6 +34,38 @@ impl<COMP: Component> Context<COMP> {
}
}
/// The common base of both function components and struct components.
///
/// If you are taken here by doc links, you might be looking for [`Component`] or
/// [`#[function_component]`](crate::functional::function_component).
///
/// We provide a blanket implementation of this trait for every member that implements [`Component`].
pub trait BaseComponent: Sized + 'static {
/// The Component's Message.
type Message: 'static;
/// The Component's properties.
type Properties: Properties;
/// Creates a component.
fn create(ctx: &Context<Self>) -> Self;
/// Updates component's internal state.
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool;
/// React to changes of component properties.
fn changed(&mut self, ctx: &Context<Self>) -> bool;
/// Returns a component layout to be rendered.
fn view(&self, ctx: &Context<Self>) -> HtmlResult;
/// Notified after a layout is rendered.
fn rendered(&mut self, ctx: &Context<Self>, first_render: bool);
/// Notified before a component is destroyed.
fn destroy(&mut self, ctx: &Context<Self>);
}
/// Components are the basic building blocks of the UI in a Yew app. Each Component
/// chooses how to display itself using received props and self-managed state.
/// Components can be dynamic and interactive by declaring messages that are
@ -95,3 +127,36 @@ pub trait Component: Sized + 'static {
#[allow(unused_variables)]
fn destroy(&mut self, ctx: &Context<Self>) {}
}
impl<T> BaseComponent for T
where
T: Sized + Component + 'static,
{
type Message = <T as Component>::Message;
type Properties = <T as Component>::Properties;
fn create(ctx: &Context<Self>) -> Self {
Component::create(ctx)
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
Component::update(self, ctx, msg)
}
fn changed(&mut self, ctx: &Context<Self>) -> bool {
Component::changed(self, ctx)
}
fn view(&self, ctx: &Context<Self>) -> HtmlResult {
Component::view(self, ctx).into_html_result()
}
fn rendered(&mut self, ctx: &Context<Self>, first_render: bool) {
Component::rendered(self, ctx, first_render)
}
fn destroy(&mut self, ctx: &Context<Self>) {
Component::destroy(self, ctx)
}
}

View File

@ -5,7 +5,7 @@ use super::{
ComponentState, CreateRunner, DestroyRunner, RenderRunner, RenderedRunner, UpdateEvent,
UpdateRunner,
},
Component,
BaseComponent,
};
use crate::callback::Callback;
use crate::context::{ContextHandle, ContextProvider};
@ -34,7 +34,7 @@ pub struct AnyScope {
pub(crate) vcomp_id: u64,
}
impl<COMP: Component> From<Scope<COMP>> for AnyScope {
impl<COMP: BaseComponent> From<Scope<COMP>> for AnyScope {
fn from(scope: Scope<COMP>) -> Self {
AnyScope {
type_id: TypeId::of::<COMP>(),
@ -71,7 +71,7 @@ impl AnyScope {
}
/// Attempts to downcast into a typed scope
pub fn downcast<COMP: Component>(self) -> Scope<COMP> {
pub fn downcast<COMP: BaseComponent>(self) -> Scope<COMP> {
let state = self
.state
.downcast::<RefCell<Option<ComponentState<COMP>>>>()
@ -93,7 +93,7 @@ impl AnyScope {
}
}
fn find_parent_scope<C: Component>(&self) -> Option<Scope<C>> {
pub(crate) fn find_parent_scope<C: BaseComponent>(&self) -> Option<Scope<C>> {
let expected_type_id = TypeId::of::<C>();
iter::successors(Some(self), |scope| scope.get_parent())
.filter(|scope| scope.get_type_id() == &expected_type_id)
@ -119,9 +119,10 @@ pub(crate) trait Scoped {
fn to_any(&self) -> AnyScope;
fn root_vnode(&self) -> Option<Ref<'_, VNode>>;
fn destroy(&mut self);
fn shift_node(&self, parent: Element, next_sibling: NodeRef);
}
impl<COMP: Component> Scoped for Scope<COMP> {
impl<COMP: BaseComponent> Scoped for Scope<COMP> {
fn to_any(&self) -> AnyScope {
self.clone().into()
}
@ -145,10 +146,17 @@ impl<COMP: Component> Scoped for Scope<COMP> {
// Not guaranteed to already have the scheduler started
scheduler::start();
}
fn shift_node(&self, parent: Element, next_sibling: NodeRef) {
scheduler::push_component_update(UpdateRunner {
state: self.state.clone(),
event: UpdateEvent::Shift(parent, next_sibling),
});
}
}
/// A context which allows sending messages to a component.
pub struct Scope<COMP: Component> {
pub struct Scope<COMP: BaseComponent> {
parent: Option<Rc<AnyScope>>,
pub(crate) state: Shared<Option<ComponentState<COMP>>>,
@ -157,13 +165,13 @@ pub struct Scope<COMP: Component> {
pub(crate) vcomp_id: u64,
}
impl<COMP: Component> fmt::Debug for Scope<COMP> {
impl<COMP: BaseComponent> fmt::Debug for Scope<COMP> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("Scope<_>")
}
}
impl<COMP: Component> Clone for Scope<COMP> {
impl<COMP: BaseComponent> Clone for Scope<COMP> {
fn clone(&self) -> Self {
Scope {
parent: self.parent.clone(),
@ -175,7 +183,7 @@ impl<COMP: Component> Clone for Scope<COMP> {
}
}
impl<COMP: Component> Scope<COMP> {
impl<COMP: BaseComponent> Scope<COMP> {
/// Returns the parent scope
pub fn get_parent(&self) -> Option<&AnyScope> {
self.parent.as_deref()
@ -268,7 +276,7 @@ impl<COMP: Component> Scope<COMP> {
/// Send a message to the component.
///
/// Please be aware that currently this method synchronously
/// schedules a call to the [Component](Component) interface.
/// schedules a call to the [Component](crate::html::Component) interface.
pub fn send_message<T>(&self, msg: T)
where
T: Into<COMP::Message>,
@ -283,7 +291,7 @@ impl<COMP: Component> Scope<COMP> {
/// function is called only once if needed.
///
/// Please be aware that currently this method synchronously
/// schedules calls to the [Component](Component) interface.
/// schedules calls to the [Component](crate::html::Component) interface.
pub fn send_message_batch(&self, messages: Vec<COMP::Message>) {
// There is no reason to schedule empty batches.
// This check is especially handy for the batch_callback method.
@ -340,7 +348,6 @@ impl<COMP: Component> Scope<COMP> {
};
closure.into()
}
/// This method creates a [`Callback`] which returns a Future which
/// returns a message to be sent back to the component's event
/// loop.
@ -409,7 +416,7 @@ impl<COMP: Component> Scope<COMP> {
/// Defines a message type that can be sent to a component.
/// Used for the return value of closure given to [Scope::batch_callback](struct.Scope.html#method.batch_callback).
pub trait SendAsMessage<COMP: Component> {
pub trait SendAsMessage<COMP: BaseComponent> {
/// Sends the message to the given component's scope.
/// See [Scope::batch_callback](struct.Scope.html#method.batch_callback).
fn send(self, scope: &Scope<COMP>);
@ -417,7 +424,7 @@ pub trait SendAsMessage<COMP: Component> {
impl<COMP> SendAsMessage<COMP> for Option<COMP::Message>
where
COMP: Component,
COMP: BaseComponent,
{
fn send(self, scope: &Scope<COMP>) {
if let Some(msg) = self {
@ -428,7 +435,7 @@ where
impl<COMP> SendAsMessage<COMP> for Vec<COMP::Message>
where
COMP: Component,
COMP: BaseComponent,
{
fn send(self, scope: &Scope<COMP>) {
scope.send_message_batch(self);

View File

@ -2,7 +2,7 @@ use super::{Component, NodeRef, Scope};
use crate::virtual_dom::AttrValue;
use std::{borrow::Cow, rc::Rc};
/// Marker trait for types that the [`html!`] macro may clone implicitly.
/// Marker trait for types that the [`html!`](macro@crate::html) macro may clone implicitly.
pub trait ImplicitClone: Clone {}
// this is only implemented because there's no way to avoid cloning this value

View File

@ -0,0 +1,14 @@
use thiserror::Error;
use crate::suspense::Suspension;
/// Render Error.
#[derive(Error, Debug, Clone, PartialEq)]
pub enum RenderError {
/// Component Rendering Suspended
#[error("component rendering is suspended.")]
Suspended(#[from] Suspension),
}
/// Render Result.
pub type RenderResult<T> = std::result::Result<T, RenderError>;

View File

@ -3,13 +3,16 @@
mod classes;
mod component;
mod conversion;
mod error;
mod listener;
pub use classes::*;
pub use component::*;
pub use conversion::*;
pub use error::*;
pub use listener::*;
use crate::sealed::Sealed;
use crate::virtual_dom::{VNode, VPortal};
use std::cell::RefCell;
use std::rc::Rc;
@ -19,6 +22,31 @@ use web_sys::{Element, Node};
/// A type which expected as a result of `view` function implementation.
pub type Html = VNode;
/// An enhanced type of `Html` returned in suspendible function components.
pub type HtmlResult = RenderResult<Html>;
impl Sealed for HtmlResult {}
impl Sealed for Html {}
/// A trait to translate into a [`HtmlResult`].
pub trait IntoHtmlResult: Sealed {
/// Performs the conversion.
fn into_html_result(self) -> HtmlResult;
}
impl IntoHtmlResult for HtmlResult {
#[inline(always)]
fn into_html_result(self) -> HtmlResult {
self
}
}
impl IntoHtmlResult for Html {
#[inline(always)]
fn into_html_result(self) -> HtmlResult {
Ok(self)
}
}
/// Wrapped Node reference for later use in Component lifecycle methods.
///
/// # Example

View File

@ -160,7 +160,6 @@ pub use yew_macro::html;
/// impl Into<Html> for ListItem {
/// fn into(self) -> Html { html! { <self /> } }
/// }
///
/// // You can use `List` with nested `ListItem` components.
/// // Using any other kind of element would result in a compile error.
/// # fn test() -> Html {
@ -262,6 +261,8 @@ pub mod context;
pub mod functional;
pub mod html;
pub mod scheduler;
mod sealed;
pub mod suspense;
#[cfg(test)]
pub mod tests;
pub mod utils;
@ -283,6 +284,8 @@ pub mod events {
pub use crate::app_handle::AppHandle;
use web_sys::Element;
use crate::html::BaseComponent;
thread_local! {
static PANIC_HOOK_IS_SET: Cell<bool> = Cell::new(false);
}
@ -305,7 +308,7 @@ fn set_default_panic_hook() {
/// If you would like to pass props, use the `start_app_with_props_in_element` method.
pub fn start_app_in_element<COMP>(element: Element) -> AppHandle<COMP>
where
COMP: Component,
COMP: BaseComponent,
COMP::Properties: Default,
{
start_app_with_props_in_element(element, COMP::Properties::default())
@ -315,7 +318,7 @@ where
/// Alias to start_app_in_element(Body)
pub fn start_app<COMP>() -> AppHandle<COMP>
where
COMP: Component,
COMP: BaseComponent,
COMP::Properties: Default,
{
start_app_with_props(COMP::Properties::default())
@ -328,7 +331,7 @@ where
/// CSS classes of the body element.
pub fn start_app_as_body<COMP>() -> AppHandle<COMP>
where
COMP: Component,
COMP: BaseComponent,
COMP::Properties: Default,
{
start_app_with_props_as_body(COMP::Properties::default())
@ -341,7 +344,7 @@ pub fn start_app_with_props_in_element<COMP>(
props: COMP::Properties,
) -> AppHandle<COMP>
where
COMP: Component,
COMP: BaseComponent,
{
set_default_panic_hook();
AppHandle::<COMP>::mount_with_props(element, Rc::new(props))
@ -351,7 +354,7 @@ where
/// This function does the same as `start_app(...)` but allows to start an Yew application with properties.
pub fn start_app_with_props<COMP>(props: COMP::Properties) -> AppHandle<COMP>
where
COMP: Component,
COMP: BaseComponent,
{
start_app_with_props_in_element(
gloo_utils::document()
@ -369,7 +372,7 @@ where
/// CSS classes of the body element.
pub fn start_app_with_props_as_body<COMP>(props: COMP::Properties) -> AppHandle<COMP>
where
COMP: Component,
COMP: BaseComponent,
{
set_default_panic_hook();
AppHandle::<COMP>::mount_as_body_with_props(Rc::new(props))
@ -389,10 +392,11 @@ pub mod prelude {
pub use crate::context::ContextProvider;
pub use crate::events::*;
pub use crate::html::{
create_portal, Children, ChildrenWithProps, Classes, Component, Context, Html, NodeRef,
Properties,
create_portal, BaseComponent, Children, ChildrenWithProps, Classes, Component, Context,
Html, HtmlResult, NodeRef, Properties,
};
pub use crate::macros::{classes, html, html_nested};
pub use crate::suspense::Suspense;
pub use crate::functional::*;
}

View File

@ -0,0 +1,2 @@
/// Base traits for sealed traits.
pub trait Sealed {}

View File

@ -0,0 +1,99 @@
use crate::html::{Children, Component, Context, Html, Properties, Scope};
use crate::virtual_dom::{Key, VList, VNode, VSuspense};
use gloo_utils::document;
use web_sys::Element;
use super::Suspension;
#[derive(Properties, PartialEq, Debug, Clone)]
pub struct SuspenseProps {
#[prop_or_default]
pub children: Children,
#[prop_or_default]
pub fallback: Html,
#[prop_or_default]
pub key: Option<Key>,
}
#[derive(Debug)]
pub enum SuspenseMsg {
Suspend(Suspension),
Resume(Suspension),
}
/// Suspend rendering and show a fallback UI until the underlying task completes.
#[derive(Debug)]
pub struct Suspense {
link: Scope<Self>,
suspensions: Vec<Suspension>,
detached_parent: Element,
}
impl Component for Suspense {
type Properties = SuspenseProps;
type Message = SuspenseMsg;
fn create(ctx: &Context<Self>) -> Self {
Self {
link: ctx.link().clone(),
suspensions: Vec::new(),
detached_parent: document().create_element("div").unwrap(),
}
}
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Self::Message::Suspend(m) => {
if m.resumed() {
return false;
}
m.listen(self.link.callback(Self::Message::Resume));
self.suspensions.push(m);
true
}
Self::Message::Resume(ref m) => {
let suspensions_len = self.suspensions.len();
self.suspensions.retain(|n| m != n);
suspensions_len != self.suspensions.len()
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
let SuspenseProps {
children,
fallback: fallback_vnode,
key,
} = (*ctx.props()).clone();
let children_vnode =
VNode::from(VList::with_children(children.into_iter().collect(), None));
let vsuspense = VSuspense::new(
children_vnode,
fallback_vnode,
self.detached_parent.clone(),
!self.suspensions.is_empty(),
key,
);
VNode::from(vsuspense)
}
}
impl Suspense {
pub(crate) fn suspend(&self, s: Suspension) {
self.link.send_message(SuspenseMsg::Suspend(s));
}
pub(crate) fn resume(&self, s: Suspension) {
self.link.send_message(SuspenseMsg::Resume(s));
}
}

View File

@ -0,0 +1,7 @@
//! This module provides suspense support.
mod component;
mod suspension;
pub use component::Suspense;
pub use suspension::{Suspension, SuspensionHandle, SuspensionResult};

View File

@ -0,0 +1,140 @@
use std::cell::RefCell;
use std::future::Future;
use std::pin::Pin;
use std::rc::Rc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::task::{Context, Poll};
use thiserror::Error;
use wasm_bindgen_futures::spawn_local;
use crate::Callback;
thread_local! {
static SUSPENSION_ID: RefCell<usize> = RefCell::default();
}
/// A Suspension.
///
/// This type can be sent back as an `Err(_)` to suspend a component until the underlying task
/// completes.
#[derive(Error, Debug, Clone)]
#[error("suspend component rendering")]
pub struct Suspension {
id: usize,
listeners: Rc<RefCell<Vec<Callback<Self>>>>,
resumed: Rc<AtomicBool>,
}
impl PartialEq for Suspension {
fn eq(&self, rhs: &Self) -> bool {
self.id == rhs.id
}
}
impl Suspension {
/// Creates a Suspension.
pub fn new() -> (Self, SuspensionHandle) {
let id = SUSPENSION_ID.with(|m| {
let mut m = m.borrow_mut();
*m += 1;
*m
});
let self_ = Suspension {
id,
listeners: Rc::default(),
resumed: Rc::default(),
};
(self_.clone(), SuspensionHandle { inner: self_ })
}
/// Creates a Suspension that resumes when the [`Future`] resolves.
pub fn from_future(f: impl Future<Output = ()> + 'static) -> Self {
let (self_, handle) = Self::new();
spawn_local(async move {
f.await;
handle.resume();
});
self_
}
/// Returns `true` if the current suspension is already resumed.
pub fn resumed(&self) -> bool {
self.resumed.load(Ordering::Relaxed)
}
/// Listens to a suspension and get notified when it resumes.
pub(crate) fn listen(&self, cb: Callback<Self>) {
if self.resumed() {
cb.emit(self.clone());
return;
}
let mut listeners = self.listeners.borrow_mut();
listeners.push(cb);
}
fn resume_by_ref(&self) {
// The component can resume rendering by returning a non-suspended result after a state is
// updated, so we always need to check here.
if !self.resumed() {
self.resumed.store(true, Ordering::Relaxed);
let listeners = self.listeners.borrow();
for listener in listeners.iter() {
listener.emit(self.clone());
}
}
}
}
impl Future for Suspension {
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
if self.resumed() {
return Poll::Ready(());
}
let waker = cx.waker().clone();
self.listen(Callback::from(move |_| {
waker.clone().wake();
}));
Poll::Pending
}
}
/// A Suspension Result.
pub type SuspensionResult<T> = std::result::Result<T, Suspension>;
/// A Suspension Handle.
///
/// This type is used to control the corresponding [`Suspension`].
///
/// When the current struct is dropped or `resume` is called, it will resume rendering of current
/// component.
#[derive(Debug, PartialEq)]
pub struct SuspensionHandle {
inner: Suspension,
}
impl SuspensionHandle {
/// Resumes component rendering.
pub fn resume(self) {
self.inner.resume_by_ref();
}
}
impl Drop for SuspensionHandle {
fn drop(&mut self) {
self.inner.resume_by_ref();
}
}

View File

@ -13,6 +13,8 @@ pub mod vnode;
#[doc(hidden)]
pub mod vportal;
#[doc(hidden)]
pub mod vsuspense;
#[doc(hidden)]
pub mod vtag;
#[doc(hidden)]
pub mod vtext;
@ -36,6 +38,8 @@ pub use self::vnode::VNode;
#[doc(inline)]
pub use self::vportal::VPortal;
#[doc(inline)]
pub use self::vsuspense::VSuspense;
#[doc(inline)]
pub use self::vtag::VTag;
#[doc(inline)]
pub use self::vtext::VText;
@ -224,7 +228,7 @@ pub enum Attributes {
/// Attribute keys. Includes both always set and optional attribute keys.
keys: &'static [&'static str],
/// Attribute values. Matches [keys]. Optional attributes are designated by setting [None].
/// Attribute values. Matches [keys](Attributes::Dynamic::keys). Optional attributes are designated by setting [None].
values: Box<[Option<AttrValue>]>,
},
@ -495,6 +499,12 @@ pub(crate) trait VDiff {
/// Remove self from parent.
fn detach(&mut self, parent: &Element);
/// Move elements from one parent to another parent.
/// This is currently only used by `VSuspense` to preserve component state without detaching
/// (which destroys component state).
/// Prefer `detach` then apply if possible.
fn shift(&self, previous_parent: &Element, next_parent: &Element, next_sibling: NodeRef);
/// Scoped diff apply to other tree.
///
/// Virtual rendering for the node. It uses parent node and existing

View File

@ -1,7 +1,7 @@
//! This module contains the implementation of a virtual component (`VComp`).
use super::{Key, VDiff, VNode};
use crate::html::{AnyScope, Component, NodeRef, Scope, Scoped};
use crate::html::{AnyScope, BaseComponent, NodeRef, Scope, Scoped};
use std::any::TypeId;
use std::borrow::Borrow;
use std::fmt;
@ -73,7 +73,7 @@ impl Clone for VComp {
}
/// A virtual child component.
pub struct VChild<COMP: Component> {
pub struct VChild<COMP: BaseComponent> {
/// The component properties
pub props: Rc<COMP::Properties>,
/// Reference to the mounted node
@ -81,7 +81,7 @@ pub struct VChild<COMP: Component> {
key: Option<Key>,
}
impl<COMP: Component> Clone for VChild<COMP> {
impl<COMP: BaseComponent> Clone for VChild<COMP> {
fn clone(&self) -> Self {
VChild {
props: Rc::clone(&self.props),
@ -91,7 +91,7 @@ impl<COMP: Component> Clone for VChild<COMP> {
}
}
impl<COMP: Component> PartialEq for VChild<COMP>
impl<COMP: BaseComponent> PartialEq for VChild<COMP>
where
COMP::Properties: PartialEq,
{
@ -102,7 +102,7 @@ where
impl<COMP> VChild<COMP>
where
COMP: Component,
COMP: BaseComponent,
{
/// Creates a child component that can be accessed and modified by its parent.
pub fn new(props: COMP::Properties, node_ref: NodeRef, key: Option<Key>) -> Self {
@ -116,7 +116,7 @@ where
impl<COMP> From<VChild<COMP>> for VComp
where
COMP: Component,
COMP: BaseComponent,
{
fn from(vchild: VChild<COMP>) -> Self {
VComp::new::<COMP>(vchild.props, vchild.node_ref, vchild.key)
@ -127,7 +127,7 @@ impl VComp {
/// Creates a new `VComp` instance.
pub fn new<COMP>(props: Rc<COMP::Properties>, node_ref: NodeRef, key: Option<Key>) -> Self
where
COMP: Component,
COMP: BaseComponent,
{
VComp {
type_id: TypeId::of::<COMP>(),
@ -183,17 +183,17 @@ trait Mountable {
fn reuse(self: Box<Self>, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef);
}
struct PropsWrapper<COMP: Component> {
struct PropsWrapper<COMP: BaseComponent> {
props: Rc<COMP::Properties>,
}
impl<COMP: Component> PropsWrapper<COMP> {
impl<COMP: BaseComponent> PropsWrapper<COMP> {
pub fn new(props: Rc<COMP::Properties>) -> Self {
Self { props }
}
}
impl<COMP: Component> Mountable for PropsWrapper<COMP> {
impl<COMP: BaseComponent> Mountable for PropsWrapper<COMP> {
fn copy(&self) -> Box<dyn Mountable> {
let wrapper: PropsWrapper<COMP> = PropsWrapper {
props: Rc::clone(&self.props),
@ -225,6 +225,11 @@ impl VDiff for VComp {
self.take_scope().destroy();
}
fn shift(&self, _previous_parent: &Element, next_parent: &Element, next_sibling: NodeRef) {
let scope = self.scope.as_ref().unwrap();
scope.shift_node(next_parent.clone(), next_sibling);
}
fn apply(
&mut self,
parent_scope: &AnyScope,
@ -272,7 +277,7 @@ impl fmt::Debug for VComp {
}
}
impl<COMP: Component> fmt::Debug for VChild<COMP> {
impl<COMP: BaseComponent> fmt::Debug for VChild<COMP> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("VChild<_>")
}

View File

@ -291,6 +291,16 @@ impl VDiff for VList {
}
}
fn shift(&self, previous_parent: &Element, next_parent: &Element, next_sibling: NodeRef) {
let mut last_node_ref = next_sibling;
for node in self.children.iter().rev() {
node.shift(previous_parent, next_parent, last_node_ref);
last_node_ref = NodeRef::default();
last_node_ref.set(node.first_node());
}
}
fn apply(
&mut self,
parent_scope: &AnyScope,

View File

@ -1,7 +1,7 @@
//! This module contains the implementation of abstract virtual node.
use super::{Key, VChild, VComp, VDiff, VList, VPortal, VTag, VText};
use crate::html::{AnyScope, Component, NodeRef};
use super::{Key, VChild, VComp, VDiff, VList, VPortal, VSuspense, VTag, VText};
use crate::html::{AnyScope, BaseComponent, NodeRef};
use gloo::console;
use std::cmp::PartialEq;
use std::fmt;
@ -25,6 +25,8 @@ pub enum VNode {
VPortal(VPortal),
/// A holder for any `Node` (necessary for replacing node).
VRef(Node),
/// A suspendible document fragment.
VSuspense(VSuspense),
}
impl VNode {
@ -36,6 +38,7 @@ impl VNode {
VNode::VTag(vtag) => vtag.key.clone(),
VNode::VText(_) => None,
VNode::VPortal(vportal) => vportal.node.key(),
VNode::VSuspense(vsuspense) => vsuspense.key.clone(),
}
}
@ -47,6 +50,7 @@ impl VNode {
VNode::VRef(_) | VNode::VText(_) => false,
VNode::VTag(vtag) => vtag.key.is_some(),
VNode::VPortal(vportal) => vportal.node.has_key(),
VNode::VSuspense(vsuspense) => vsuspense.key.is_some(),
}
}
@ -63,6 +67,7 @@ impl VNode {
VNode::VList(vlist) => vlist.get(0).and_then(VNode::first_node),
VNode::VRef(node) => Some(node.clone()),
VNode::VPortal(vportal) => vportal.next_sibling(),
VNode::VSuspense(vsuspense) => vsuspense.first_node(),
}
}
@ -94,6 +99,9 @@ impl VNode {
.unchecked_first_node(),
VNode::VRef(node) => node.clone(),
VNode::VPortal(_) => panic!("portals have no first node, they are empty inside"),
VNode::VSuspense(vsuspense) => {
vsuspense.first_node().expect("VSuspense is not mounted")
}
}
}
@ -130,6 +138,28 @@ impl VDiff for VNode {
}
}
VNode::VPortal(ref mut vportal) => vportal.detach(parent),
VNode::VSuspense(ref mut vsuspense) => vsuspense.detach(parent),
}
}
fn shift(&self, previous_parent: &Element, next_parent: &Element, next_sibling: NodeRef) {
match *self {
VNode::VTag(ref vtag) => vtag.shift(previous_parent, next_parent, next_sibling),
VNode::VText(ref vtext) => vtext.shift(previous_parent, next_parent, next_sibling),
VNode::VComp(ref vcomp) => vcomp.shift(previous_parent, next_parent, next_sibling),
VNode::VList(ref vlist) => vlist.shift(previous_parent, next_parent, next_sibling),
VNode::VRef(ref node) => {
previous_parent.remove_child(node).unwrap();
next_parent
.insert_before(node, next_sibling.get().as_ref())
.unwrap();
}
VNode::VPortal(ref vportal) => {
vportal.shift(previous_parent, next_parent, next_sibling)
}
VNode::VSuspense(ref vsuspense) => {
vsuspense.shift(previous_parent, next_parent, next_sibling)
}
}
}
@ -166,6 +196,9 @@ impl VDiff for VNode {
VNode::VPortal(ref mut vportal) => {
vportal.apply(parent_scope, parent, next_sibling, ancestor)
}
VNode::VSuspense(ref mut vsuspense) => {
vsuspense.apply(parent_scope, parent, next_sibling, ancestor)
}
}
}
}
@ -204,9 +237,16 @@ impl From<VComp> for VNode {
}
}
impl From<VSuspense> for VNode {
#[inline]
fn from(vsuspense: VSuspense) -> Self {
VNode::VSuspense(vsuspense)
}
}
impl<COMP> From<VChild<COMP>> for VNode
where
COMP: Component,
COMP: BaseComponent,
{
fn from(vchild: VChild<COMP>) -> Self {
VNode::VComp(VComp::from(vchild))
@ -237,6 +277,7 @@ impl fmt::Debug for VNode {
VNode::VList(ref vlist) => vlist.fmt(f),
VNode::VRef(ref vref) => write!(f, "VRef ( \"{}\" )", crate::utils::print_node(vref)),
VNode::VPortal(ref vportal) => vportal.fmt(f),
VNode::VSuspense(ref vsuspense) => vsuspense.fmt(f),
}
}
}

View File

@ -22,6 +22,10 @@ impl VDiff for VPortal {
self.sibling_ref.set(None);
}
fn shift(&self, _previous_parent: &Element, _next_parent: &Element, _next_sibling: NodeRef) {
// portals have nothing in it's original place of DOM, we also do nothing.
}
fn apply(
&mut self,
parent_scope: &AnyScope,

View File

@ -0,0 +1,147 @@
use super::{Key, VDiff, VNode};
use crate::html::{AnyScope, NodeRef};
use web_sys::{Element, Node};
/// This struct represents a suspendable DOM fragment.
#[derive(Clone, Debug, PartialEq)]
pub struct VSuspense {
/// Child nodes.
children: Box<VNode>,
/// Fallback nodes when suspended.
fallback: Box<VNode>,
/// The element to attach to when children is not attached to DOM
detached_parent: Element,
/// Whether the current status is suspended.
suspended: bool,
/// The Key.
pub(crate) key: Option<Key>,
}
impl VSuspense {
pub(crate) fn new(
children: VNode,
fallback: VNode,
detached_parent: Element,
suspended: bool,
key: Option<Key>,
) -> Self {
Self {
children: children.into(),
fallback: fallback.into(),
detached_parent,
suspended,
key,
}
}
pub(crate) fn first_node(&self) -> Option<Node> {
if self.suspended {
self.fallback.first_node()
} else {
self.children.first_node()
}
}
}
impl VDiff for VSuspense {
fn detach(&mut self, parent: &Element) {
if self.suspended {
self.fallback.detach(parent);
self.children.detach(&self.detached_parent);
} else {
self.children.detach(parent);
}
}
fn shift(&self, previous_parent: &Element, next_parent: &Element, next_sibling: NodeRef) {
if self.suspended {
self.fallback
.shift(previous_parent, next_parent, next_sibling);
} else {
self.children
.shift(previous_parent, next_parent, next_sibling);
}
}
fn apply(
&mut self,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
ancestor: Option<VNode>,
) -> NodeRef {
let (already_suspended, children_ancestor, fallback_ancestor) = match ancestor {
Some(VNode::VSuspense(mut m)) => {
// We only preserve the child state if they are the same suspense.
if m.key != self.key || self.detached_parent != m.detached_parent {
m.detach(parent);
(false, None, None)
} else {
(m.suspended, Some(*m.children), Some(*m.fallback))
}
}
Some(mut m) => {
m.detach(parent);
(false, None, None)
}
None => (false, None, None),
};
// When it's suspended, we render children into an element that is detached from the dom
// tree while rendering fallback UI into the original place where children resides in.
match (self.suspended, already_suspended) {
(true, true) => {
self.children.apply(
parent_scope,
&self.detached_parent,
NodeRef::default(),
children_ancestor,
);
self.fallback
.apply(parent_scope, parent, next_sibling, fallback_ancestor)
}
(false, false) => {
self.children
.apply(parent_scope, parent, next_sibling, children_ancestor)
}
(true, false) => {
children_ancestor.as_ref().unwrap().shift(
parent,
&self.detached_parent,
NodeRef::default(),
);
self.children.apply(
parent_scope,
&self.detached_parent,
NodeRef::default(),
children_ancestor,
);
// first render of fallback, ancestor needs to be None.
self.fallback
.apply(parent_scope, parent, next_sibling, None)
}
(false, true) => {
fallback_ancestor.unwrap().detach(parent);
children_ancestor.as_ref().unwrap().shift(
&self.detached_parent,
parent,
next_sibling.clone(),
);
self.children
.apply(parent_scope, parent, next_sibling, children_ancestor)
}
}
}
}

View File

@ -493,6 +493,18 @@ impl VDiff for VTag {
}
}
fn shift(&self, previous_parent: &Element, next_parent: &Element, next_sibling: NodeRef) {
let node = self
.reference
.as_ref()
.expect("tried to shift not rendered VTag from DOM");
previous_parent.remove_child(node).unwrap();
next_parent
.insert_before(node, next_sibling.get().as_ref())
.unwrap();
}
/// Renders virtual tag over DOM [Element], but it also compares this with an ancestor [VTag]
/// to compute what to patch in the actual DOM nodes.
fn apply(

View File

@ -54,6 +54,18 @@ impl VDiff for VText {
}
}
fn shift(&self, previous_parent: &Element, next_parent: &Element, next_sibling: NodeRef) {
let node = self
.reference
.as_ref()
.expect("tried to shift not rendered VTag from DOM");
previous_parent.remove_child(node).unwrap();
next_parent
.insert_before(node, next_sibling.get().as_ref())
.unwrap();
}
/// Renders virtual node over existing `TextNode`, but only if value of text has changed.
fn apply(
&mut self,

View File

@ -3,7 +3,7 @@ mod common;
use common::obtain_result;
use wasm_bindgen_test::*;
use yew::functional::{FunctionComponent, FunctionProvider};
use yew::{html, Html, Properties};
use yew::{html, HtmlResult, Properties};
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
@ -17,13 +17,13 @@ fn props_are_passed() {
impl FunctionProvider for PropsPassedFunction {
type TProps = PropsPassedFunctionProps;
fn run(props: &Self::TProps) -> Html {
fn run(props: &Self::TProps) -> HtmlResult {
assert_eq!(&props.value, "props");
return html! {
return Ok(html! {
<div id="result">
{"done"}
</div>
};
});
}
}
type PropsComponent = FunctionComponent<PropsPassedFunction>;

View File

@ -0,0 +1,401 @@
mod common;
use common::obtain_result;
use wasm_bindgen_test::*;
use yew::prelude::*;
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
use std::rc::Rc;
use gloo::timers::future::TimeoutFuture;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::spawn_local;
use web_sys::{HtmlElement, HtmlTextAreaElement};
use yew::suspense::{Suspension, SuspensionResult};
#[wasm_bindgen_test]
async fn suspense_works() {
#[derive(PartialEq)]
pub struct SleepState {
s: Suspension,
}
impl SleepState {
fn new() -> Self {
let (s, handle) = Suspension::new();
spawn_local(async move {
TimeoutFuture::new(50).await;
handle.resume();
});
Self { s }
}
}
impl Reducible for SleepState {
type Action = ();
fn reduce(self: Rc<Self>, _action: Self::Action) -> Rc<Self> {
Self::new().into()
}
}
pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> {
let sleep_state = use_reducer(SleepState::new);
if sleep_state.s.resumed() {
Ok(Rc::new(move || sleep_state.dispatch(())))
} else {
Err(sleep_state.s.clone())
}
}
#[function_component(Content)]
fn content() -> HtmlResult {
let resleep = use_sleep()?;
let value = use_state(|| 0);
let on_increment = {
let value = value.clone();
Callback::from(move |_: MouseEvent| {
value.set(*value + 1);
})
};
let on_take_a_break = Callback::from(move |_: MouseEvent| (resleep.clone())());
Ok(html! {
<div class="content-area">
<div class="actual-result">{*value}</div>
<button class="increase" onclick={on_increment}>{"increase"}</button>
<div class="action-area">
<button class="take-a-break" onclick={on_take_a_break}>{"Take a break!"}</button>
</div>
</div>
})
}
#[function_component(App)]
fn app() -> Html {
let fallback = html! {<div>{"wait..."}</div>};
html! {
<div id="result">
<Suspense {fallback}>
<Content />
</Suspense>
</div>
}
}
yew::start_app_in_element::<App>(gloo_utils::document().get_element_by_id("output").unwrap());
TimeoutFuture::new(10).await;
let result = obtain_result();
assert_eq!(result.as_str(), "<div>wait...</div>");
TimeoutFuture::new(50).await;
let result = obtain_result();
assert_eq!(
result.as_str(),
r#"<div class="content-area"><div class="actual-result">0</div><button class="increase">increase</button><div class="action-area"><button class="take-a-break">Take a break!</button></div></div>"#
);
TimeoutFuture::new(10).await;
gloo_utils::document()
.query_selector(".increase")
.unwrap()
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap()
.click();
gloo_utils::document()
.query_selector(".increase")
.unwrap()
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap()
.click();
let result = obtain_result();
assert_eq!(
result.as_str(),
r#"<div class="content-area"><div class="actual-result">2</div><button class="increase">increase</button><div class="action-area"><button class="take-a-break">Take a break!</button></div></div>"#
);
gloo_utils::document()
.query_selector(".take-a-break")
.unwrap()
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap()
.click();
TimeoutFuture::new(10).await;
let result = obtain_result();
assert_eq!(result.as_str(), "<div>wait...</div>");
TimeoutFuture::new(50).await;
let result = obtain_result();
assert_eq!(
result.as_str(),
r#"<div class="content-area"><div class="actual-result">2</div><button class="increase">increase</button><div class="action-area"><button class="take-a-break">Take a break!</button></div></div>"#
);
}
#[wasm_bindgen_test]
async fn suspense_not_suspended_at_start() {
#[derive(PartialEq)]
pub struct SleepState {
s: Option<Suspension>,
}
impl SleepState {
fn new() -> Self {
Self { s: None }
}
}
impl Reducible for SleepState {
type Action = ();
fn reduce(self: Rc<Self>, _action: Self::Action) -> Rc<Self> {
let (s, handle) = Suspension::new();
spawn_local(async move {
TimeoutFuture::new(50).await;
handle.resume();
});
Self { s: Some(s) }.into()
}
}
pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> {
let sleep_state = use_reducer(SleepState::new);
let s = match sleep_state.s.clone() {
Some(m) => m,
None => return Ok(Rc::new(move || sleep_state.dispatch(()))),
};
if s.resumed() {
Ok(Rc::new(move || sleep_state.dispatch(())))
} else {
Err(s)
}
}
#[function_component(Content)]
fn content() -> HtmlResult {
let resleep = use_sleep()?;
let value = use_state(|| "I am writing a long story...".to_string());
let on_text_input = {
let value = value.clone();
Callback::from(move |e: InputEvent| {
let input: HtmlTextAreaElement = e.target_unchecked_into();
value.set(input.value());
})
};
let on_take_a_break = Callback::from(move |_| (resleep.clone())());
Ok(html! {
<div class="content-area">
<textarea value={value.to_string()} oninput={on_text_input}></textarea>
<div class="action-area">
<button class="take-a-break" onclick={on_take_a_break}>{"Take a break!"}</button>
</div>
</div>
})
}
#[function_component(App)]
fn app() -> Html {
let fallback = html! {<div>{"wait..."}</div>};
html! {
<div id="result">
<Suspense {fallback}>
<Content />
</Suspense>
</div>
}
}
yew::start_app_in_element::<App>(gloo_utils::document().get_element_by_id("output").unwrap());
TimeoutFuture::new(10).await;
let result = obtain_result();
assert_eq!(
result.as_str(),
r#"<div class="content-area"><textarea></textarea><div class="action-area"><button class="take-a-break">Take a break!</button></div></div>"#
);
gloo_utils::document()
.query_selector(".take-a-break")
.unwrap()
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap()
.click();
TimeoutFuture::new(10).await;
let result = obtain_result();
assert_eq!(result.as_str(), "<div>wait...</div>");
TimeoutFuture::new(50).await;
let result = obtain_result();
assert_eq!(
result.as_str(),
r#"<div class="content-area"><textarea></textarea><div class="action-area"><button class="take-a-break">Take a break!</button></div></div>"#
);
}
#[wasm_bindgen_test]
async fn suspense_nested_suspense_works() {
#[derive(PartialEq)]
pub struct SleepState {
s: Suspension,
}
impl SleepState {
fn new() -> Self {
let (s, handle) = Suspension::new();
spawn_local(async move {
TimeoutFuture::new(50).await;
handle.resume();
});
Self { s }
}
}
impl Reducible for SleepState {
type Action = ();
fn reduce(self: Rc<Self>, _action: Self::Action) -> Rc<Self> {
Self::new().into()
}
}
pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> {
let sleep_state = use_reducer(SleepState::new);
if sleep_state.s.resumed() {
Ok(Rc::new(move || sleep_state.dispatch(())))
} else {
Err(sleep_state.s.clone())
}
}
#[function_component(InnerContent)]
fn inner_content() -> HtmlResult {
let resleep = use_sleep()?;
let on_take_a_break = Callback::from(move |_: MouseEvent| (resleep.clone())());
Ok(html! {
<div class="content-area">
<div class="action-area">
<button class="take-a-break2" onclick={on_take_a_break}>{"Take a break!"}</button>
</div>
</div>
})
}
#[function_component(Content)]
fn content() -> HtmlResult {
let resleep = use_sleep()?;
let fallback = html! {<div>{"wait...(inner)"}</div>};
let on_take_a_break = Callback::from(move |_: MouseEvent| (resleep.clone())());
Ok(html! {
<div class="content-area">
<div class="action-area">
<button class="take-a-break" onclick={on_take_a_break}>{"Take a break!"}</button>
</div>
<Suspense {fallback}>
<InnerContent />
</Suspense>
</div>
})
}
#[function_component(App)]
fn app() -> Html {
let fallback = html! {<div>{"wait...(outer)"}</div>};
html! {
<div id="result">
<Suspense {fallback}>
<Content />
</Suspense>
</div>
}
}
yew::start_app_in_element::<App>(gloo_utils::document().get_element_by_id("output").unwrap());
TimeoutFuture::new(10).await;
let result = obtain_result();
assert_eq!(result.as_str(), "<div>wait...(outer)</div>");
TimeoutFuture::new(50).await;
let result = obtain_result();
assert_eq!(
result.as_str(),
r#"<div class="content-area"><div class="action-area"><button class="take-a-break">Take a break!</button></div><div>wait...(inner)</div></div>"#
);
TimeoutFuture::new(50).await;
let result = obtain_result();
assert_eq!(
result.as_str(),
r#"<div class="content-area"><div class="action-area"><button class="take-a-break">Take a break!</button></div><div class="content-area"><div class="action-area"><button class="take-a-break2">Take a break!</button></div></div></div>"#
);
gloo_utils::document()
.query_selector(".take-a-break2")
.unwrap()
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap()
.click();
TimeoutFuture::new(10).await;
let result = obtain_result();
assert_eq!(
result.as_str(),
r#"<div class="content-area"><div class="action-area"><button class="take-a-break">Take a break!</button></div><div>wait...(inner)</div></div>"#
);
TimeoutFuture::new(50).await;
let result = obtain_result();
assert_eq!(
result.as_str(),
r#"<div class="content-area"><div class="action-area"><button class="take-a-break">Take a break!</button></div><div class="content-area"><div class="action-area"><button class="take-a-break2">Take a break!</button></div></div></div>"#
);
}

View File

@ -6,7 +6,7 @@ use wasm_bindgen_test::*;
use yew::functional::{
use_context, use_effect, use_mut_ref, use_state, FunctionComponent, FunctionProvider,
};
use yew::{html, Children, ContextProvider, Html, Properties};
use yew::{html, Children, ContextProvider, HtmlResult, Properties};
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
@ -23,24 +23,24 @@ fn use_context_scoping_works() {
impl FunctionProvider for ExpectNoContextFunction {
type TProps = ();
fn run(_props: &Self::TProps) -> Html {
fn run(_props: &Self::TProps) -> HtmlResult {
if use_context::<ExampleContext>().is_some() {
console_log!(
"Context should be None here, but was {:?}!",
use_context::<ExampleContext>().unwrap()
);
};
return html! {
Ok(html! {
<div></div>
};
})
}
}
impl FunctionProvider for UseContextFunctionOuter {
type TProps = ();
fn run(_props: &Self::TProps) -> Html {
fn run(_props: &Self::TProps) -> HtmlResult {
type ExampleContextProvider = ContextProvider<ExampleContext>;
return html! {
Ok(html! {
<div>
<ExampleContextProvider context={ExampleContext("wrong1".into())}>
<div>{"ignored"}</div>
@ -58,17 +58,17 @@ fn use_context_scoping_works() {
</ExampleContextProvider>
<ExpectNoContextComponent />
</div>
};
})
}
}
impl FunctionProvider for UseContextFunctionInner {
type TProps = ();
fn run(_props: &Self::TProps) -> Html {
fn run(_props: &Self::TProps) -> HtmlResult {
let context = use_context::<ExampleContext>();
return html! {
Ok(html! {
<div id="result">{ &context.unwrap().0 }</div>
};
})
}
}
@ -90,11 +90,11 @@ fn use_context_works_with_multiple_types() {
impl FunctionProvider for Test1Function {
type TProps = ();
fn run(_props: &Self::TProps) -> Html {
fn run(_props: &Self::TProps) -> HtmlResult {
assert_eq!(use_context::<ContextA>(), Some(ContextA(2)));
assert_eq!(use_context::<ContextB>(), Some(ContextB(1)));
return html! {};
Ok(html! {})
}
}
type Test1 = FunctionComponent<Test1Function>;
@ -103,11 +103,11 @@ fn use_context_works_with_multiple_types() {
impl FunctionProvider for Test2Function {
type TProps = ();
fn run(_props: &Self::TProps) -> Html {
fn run(_props: &Self::TProps) -> HtmlResult {
assert_eq!(use_context::<ContextA>(), Some(ContextA(0)));
assert_eq!(use_context::<ContextB>(), Some(ContextB(1)));
return html! {};
Ok(html! {})
}
}
type Test2 = FunctionComponent<Test2Function>;
@ -116,11 +116,11 @@ fn use_context_works_with_multiple_types() {
impl FunctionProvider for Test3Function {
type TProps = ();
fn run(_props: &Self::TProps) -> Html {
fn run(_props: &Self::TProps) -> HtmlResult {
assert_eq!(use_context::<ContextA>(), Some(ContextA(0)));
assert_eq!(use_context::<ContextB>(), None);
return html! {};
Ok(html! {})
}
}
type Test3 = FunctionComponent<Test3Function>;
@ -129,11 +129,11 @@ fn use_context_works_with_multiple_types() {
impl FunctionProvider for Test4Function {
type TProps = ();
fn run(_props: &Self::TProps) -> Html {
fn run(_props: &Self::TProps) -> HtmlResult {
assert_eq!(use_context::<ContextA>(), None);
assert_eq!(use_context::<ContextB>(), None);
return html! {};
Ok(html! {})
}
}
type Test4 = FunctionComponent<Test4Function>;
@ -142,11 +142,11 @@ fn use_context_works_with_multiple_types() {
impl FunctionProvider for TestFunction {
type TProps = ();
fn run(_props: &Self::TProps) -> Html {
fn run(_props: &Self::TProps) -> HtmlResult {
type ContextAProvider = ContextProvider<ContextA>;
type ContextBProvider = ContextProvider<ContextB>;
return html! {
Ok(html! {
<div>
<ContextAProvider context={ContextA(0)}>
<ContextBProvider context={ContextB(1)}>
@ -159,7 +159,7 @@ fn use_context_works_with_multiple_types() {
</ContextAProvider>
<Test4 />
</div>
};
})
}
}
type TestComponent = FunctionComponent<TestFunction>;
@ -184,17 +184,17 @@ fn use_context_update_works() {
impl FunctionProvider for RenderCounterFunction {
type TProps = RenderCounterProps;
fn run(props: &Self::TProps) -> Html {
fn run(props: &Self::TProps) -> HtmlResult {
let counter = use_mut_ref(|| 0);
*counter.borrow_mut() += 1;
return html! {
Ok(html! {
<>
<div id={props.id.clone()}>
{ format!("total: {}", counter.borrow()) }
</div>
{ props.children.clone() }
</>
};
})
}
}
type RenderCounter = FunctionComponent<RenderCounterFunction>;
@ -209,20 +209,20 @@ fn use_context_update_works() {
impl FunctionProvider for ContextOutletFunction {
type TProps = ContextOutletProps;
fn run(props: &Self::TProps) -> Html {
fn run(props: &Self::TProps) -> HtmlResult {
let counter = use_mut_ref(|| 0);
*counter.borrow_mut() += 1;
let ctx = use_context::<Rc<MyContext>>().expect("context not passed down");
return html! {
Ok(html! {
<>
<div>{ format!("magic: {}\n", props.magic) }</div>
<div id={props.id.clone()}>
{ format!("current: {}, total: {}", ctx.0, counter.borrow()) }
</div>
</>
};
})
}
}
type ContextOutlet = FunctionComponent<ContextOutletFunction>;
@ -231,7 +231,7 @@ fn use_context_update_works() {
impl FunctionProvider for TestFunction {
type TProps = ();
fn run(_props: &Self::TProps) -> Html {
fn run(_props: &Self::TProps) -> HtmlResult {
type MyContextProvider = ContextProvider<Rc<MyContext>>;
let ctx = use_state(|| MyContext("hello".into()));
@ -263,14 +263,14 @@ fn use_context_update_works() {
|| {}
});
}
return html! {
Ok(html! {
<MyContextProvider context={Rc::new((*ctx).clone())}>
<RenderCounter id="test-0">
<ContextOutlet id="test-1"/>
<ContextOutlet id="test-2" {magic}/>
</RenderCounter>
</MyContextProvider>
};
})
}
}
type TestComponent = FunctionComponent<TestFunction>;

View File

@ -7,7 +7,7 @@ use wasm_bindgen_test::*;
use yew::functional::{
use_effect_with_deps, use_mut_ref, use_state, FunctionComponent, FunctionProvider,
};
use yew::{html, Html, Properties};
use yew::{html, HtmlResult, Properties};
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
@ -39,7 +39,7 @@ fn use_effect_destroys_on_component_drop() {
impl FunctionProvider for UseEffectFunction {
type TProps = FunctionProps;
fn run(props: &Self::TProps) -> Html {
fn run(props: &Self::TProps) -> HtmlResult {
let effect_called = props.effect_called.clone();
let destroy_called = props.destroy_called.clone();
use_effect_with_deps(
@ -50,23 +50,23 @@ fn use_effect_destroys_on_component_drop() {
},
(),
);
return html! {};
Ok(html! {})
}
}
impl FunctionProvider for UseEffectWrapper {
type TProps = WrapperProps;
fn run(props: &Self::TProps) -> Html {
fn run(props: &Self::TProps) -> HtmlResult {
let show = use_state(|| true);
if *show {
let effect_called: Rc<dyn Fn()> = { Rc::new(move || show.set(false)) };
html! {
Ok(html! {
<UseEffectComponent destroy_called={props.destroy_called.clone()} {effect_called} />
}
})
} else {
html! {
Ok(html! {
<div>{ "EMPTY" }</div>
}
})
}
}
}
@ -87,7 +87,7 @@ fn use_effect_works_many_times() {
impl FunctionProvider for UseEffectFunction {
type TProps = ();
fn run(_: &Self::TProps) -> Html {
fn run(_: &Self::TProps) -> HtmlResult {
let counter = use_state(|| 0);
let counter_clone = counter.clone();
@ -101,13 +101,13 @@ fn use_effect_works_many_times() {
*counter,
);
return html! {
Ok(html! {
<div>
{ "The test result is" }
<div id="result">{ *counter }</div>
{ "\n" }
</div>
};
})
}
}
@ -125,7 +125,7 @@ fn use_effect_works_once() {
impl FunctionProvider for UseEffectFunction {
type TProps = ();
fn run(_: &Self::TProps) -> Html {
fn run(_: &Self::TProps) -> HtmlResult {
let counter = use_state(|| 0);
let counter_clone = counter.clone();
@ -137,13 +137,13 @@ fn use_effect_works_once() {
(),
);
return html! {
Ok(html! {
<div>
{ "The test result is" }
<div id="result">{ *counter }</div>
{ "\n" }
</div>
};
})
}
}
type UseEffectComponent = FunctionComponent<UseEffectFunction>;
@ -160,7 +160,7 @@ fn use_effect_refires_on_dependency_change() {
impl FunctionProvider for UseEffectFunction {
type TProps = ();
fn run(_: &Self::TProps) -> Html {
fn run(_: &Self::TProps) -> HtmlResult {
let number_ref = use_mut_ref(|| 0);
let number_ref_c = number_ref.clone();
let number_ref2 = use_mut_ref(|| 0);
@ -185,13 +185,13 @@ fn use_effect_refires_on_dependency_change() {
},
arg,
);
return html! {
Ok(html! {
<div>
{"The test result is"}
<div id="result">{*number_ref.borrow_mut().deref_mut()}{*number_ref2.borrow_mut().deref_mut()}</div>
{"\n"}
</div>
};
})
}
}
type UseEffectComponent = FunctionComponent<UseEffectFunction>;

View File

@ -34,7 +34,7 @@ fn use_reducer_works() {
struct UseReducerFunction {}
impl FunctionProvider for UseReducerFunction {
type TProps = ();
fn run(_: &Self::TProps) -> Html {
fn run(_: &Self::TProps) -> HtmlResult {
let counter = use_reducer(|| CounterState { counter: 10 });
let counter_clone = counter.clone();
@ -45,13 +45,13 @@ fn use_reducer_works() {
},
(),
);
return html! {
Ok(html! {
<div>
{"The test result is"}
<div id="result">{counter.counter}</div>
{"\n"}
</div>
};
})
}
}
type UseReducerComponent = FunctionComponent<UseReducerFunction>;
@ -83,7 +83,7 @@ fn use_reducer_eq_works() {
struct UseReducerFunction {}
impl FunctionProvider for UseReducerFunction {
type TProps = ();
fn run(_: &Self::TProps) -> Html {
fn run(_: &Self::TProps) -> HtmlResult {
let content = use_reducer_eq(|| ContentState {
content: HashSet::default(),
});
@ -104,7 +104,7 @@ fn use_reducer_eq_works() {
let add_content_b = Callback::from(move |_| content.dispatch("B".to_string()));
return html! {
Ok(html! {
<>
<div>
{"This component has been rendered: "}<span id="result">{render_count}</span>{" Time(s)."}
@ -112,7 +112,7 @@ fn use_reducer_eq_works() {
<button onclick={add_content_a} id="add-a">{"Add A to Content"}</button>
<button onclick={add_content_b} id="add-b">{"Add B to Content"}</button>
</>
};
})
}
}
type UseReducerComponent = FunctionComponent<UseReducerFunction>;

View File

@ -4,7 +4,7 @@ use common::obtain_result;
use std::ops::DerefMut;
use wasm_bindgen_test::*;
use yew::functional::{use_mut_ref, use_state, FunctionComponent, FunctionProvider};
use yew::{html, Html};
use yew::{html, HtmlResult};
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
@ -14,20 +14,20 @@ fn use_ref_works() {
impl FunctionProvider for UseRefFunction {
type TProps = ();
fn run(_: &Self::TProps) -> Html {
fn run(_: &Self::TProps) -> HtmlResult {
let ref_example = use_mut_ref(|| 0);
*ref_example.borrow_mut().deref_mut() += 1;
let counter = use_state(|| 0);
if *counter < 5 {
counter.set(*counter + 1)
}
return html! {
Ok(html! {
<div>
{"The test output is: "}
<div id="result">{*ref_example.borrow_mut().deref_mut() > 4}</div>
{"\n"}
</div>
};
})
}
}
type UseRefComponent = FunctionComponent<UseRefFunction>;

View File

@ -5,7 +5,7 @@ use wasm_bindgen_test::*;
use yew::functional::{
use_effect_with_deps, use_state, use_state_eq, FunctionComponent, FunctionProvider,
};
use yew::{html, Html};
use yew::{html, HtmlResult};
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
@ -15,18 +15,18 @@ fn use_state_works() {
impl FunctionProvider for UseStateFunction {
type TProps = ();
fn run(_: &Self::TProps) -> Html {
fn run(_: &Self::TProps) -> HtmlResult {
let counter = use_state(|| 0);
if *counter < 5 {
counter.set(*counter + 1)
}
return html! {
return Ok(html! {
<div>
{"Test Output: "}
<div id="result">{*counter}</div>
{"\n"}
</div>
};
});
}
}
type UseComponent = FunctionComponent<UseStateFunction>;
@ -43,7 +43,7 @@ fn multiple_use_state_setters() {
impl FunctionProvider for UseStateFunction {
type TProps = ();
fn run(_: &Self::TProps) -> Html {
fn run(_: &Self::TProps) -> HtmlResult {
let counter = use_state(|| 0);
let counter_clone = counter.clone();
use_effect_with_deps(
@ -64,14 +64,14 @@ fn multiple_use_state_setters() {
}
};
another_scope();
return html! {
Ok(html! {
<div>
{ "Test Output: " }
// expected output
<div id="result">{ *counter }</div>
{ "\n" }
</div>
};
})
}
}
type UseComponent = FunctionComponent<UseStateFunction>;
@ -92,18 +92,18 @@ fn use_state_eq_works() {
impl FunctionProvider for UseStateFunction {
type TProps = ();
fn run(_: &Self::TProps) -> Html {
fn run(_: &Self::TProps) -> HtmlResult {
RENDER_COUNT.fetch_add(1, Ordering::Relaxed);
let counter = use_state_eq(|| 0);
counter.set(1);
return html! {
Ok(html! {
<div>
{"Test Output: "}
<div id="result">{*counter}</div>
{"\n"}
</div>
};
})
}
}
type UseComponent = FunctionComponent<UseStateFunction>;

View File

@ -16,7 +16,7 @@ Typical uses of portals can include modal dialogs and hovercards, as well as mor
Note that `yew::create_portal` is a rather low-level building block, on which other components should be built that provide the interface for your specific use case. As an example, here is a simple modal dialogue that renders its `children` into an element outside `yew`'s control, identified by the `id="modal_host"`.
```rust
use yew::{html, create_portal, function_component, Children, Properties};
use yew::{html, create_portal, function_component, Children, Properties, Html};
#[derive(Properties, PartialEq)]
pub struct ModalProps {

View File

@ -54,7 +54,7 @@ html! {
<TabItem value="With props" label="With props">
```rust
use yew::{function_component, html, Properties};
use yew::{function_component, html, Properties, Html};
#[derive(Properties, PartialEq)]
pub struct RenderedAtProps {
@ -76,7 +76,7 @@ pub fn RenderedAt(props: &RenderedAtProps) -> Html {
<TabItem value="Without props" label="Without props">
```rust
use yew::{function_component, html, use_state, Callback};
use yew::{function_component, html, use_state, Callback, Html};
#[function_component]
fn App() -> Html {
@ -108,7 +108,7 @@ The `#[function_component(_)]` attribute also works with generic functions for c
```rust title=my_generic_component.rs
use std::fmt::Display;
use yew::{function_component, html, Properties};
use yew::{function_component, html, Properties, Html};
#[derive(Properties, PartialEq)]
pub struct Props<T>

View File

@ -16,7 +16,7 @@ implement the `Component` trait.
The easiest way to create a function component is to add the [`#[function_component]`](./../function-components/attribute.mdx) attribute to a function.
```rust
use yew::{function_component, html};
use yew::{function_component, html, Html};
#[function_component]
fn HelloWorld() -> Html {

View File

@ -24,7 +24,7 @@ re-render when the state changes.
### Example
```rust
use yew::{Callback, function_component, html, use_state};
use yew::{Callback, function_component, html, use_state, Html};
#[function_component(UseState)]
fn state() -> Html {
@ -75,7 +75,7 @@ If you need a mutable reference, consider using [`use_mut_ref`](#use_mut_ref).
If you need the component to be re-rendered on state change, consider using [`use_state`](#use_state).
```rust
use yew::{function_component, html, use_ref, use_state, Callback};
use yew::{function_component, html, use_ref, use_state, Callback, Html};
#[function_component(UseRef)]
fn ref_hook() -> Html {
@ -104,6 +104,7 @@ use yew::{
events::Event,
function_component, html, use_mut_ref, use_state,
Callback, TargetCast,
Html,
};
#[function_component(UseMutRef)]
@ -152,7 +153,7 @@ DOM.
use web_sys::HtmlInputElement;
use yew::{
function_component, functional::*, html,
NodeRef
NodeRef, Html
};
#[function_component(UseRef)]
@ -296,7 +297,7 @@ The destructor can be used to clean up the effects introduced and it can take ow
### Example
```rust
use yew::{Callback, function_component, html, use_effect, use_state};
use yew::{Callback, function_component, html, use_effect, use_state, Html};
#[function_component(UseEffect)]
fn effect() -> Html {
@ -348,7 +349,7 @@ use_effect_with_deps(
### Example
```rust
use yew::{ContextProvider, function_component, html, use_context, use_state};
use yew::{ContextProvider, function_component, html, use_context, use_state, Html};
/// App theme

View File

@ -0,0 +1,180 @@
---
title: "Suspense"
description: "Suspense for data fetching"
---
Suspense is a way to suspend component rendering whilst waiting a task
to complete and a fallback (placeholder) UI is shown in the meanwhile.
It can be used to fetch data from server, wait for tasks to be completed
by an agent, or perform other background asynchronous task.
Before suspense, data fetching usually happens after (Fetch-on-render) or before
component rendering (Fetch-then-render).
### Render-as-You-Fetch
Suspense enables a new approach that allows components to initiate data request
during the rendering process. When a component initiates a data request,
the rendering process will become suspended and a fallback UI will be
shown until the request is completed.
The recommended way to use suspense is with hooks.
```rust ,ignore
use yew::prelude::*;
#[function_component(Content)]
fn content() -> HtmlResult {
let user = use_user()?;
Ok(html! {<div>{"Hello, "}{&user.name}</div>})
}
#[function_component(App)]
fn app() -> Html {
let fallback = html! {<div>{"Loading..."}</div>};
html! {
<Suspense {fallback}>
<Content />
</Suspense>
}
}
```
In the above example, the `use_user` hook will suspend the component
rendering while user information is loading and a `Loading...` placeholder will
be shown until `user` is loaded.
To define a hook that suspends a component rendering, it needs to return
a `SuspensionResult<T>`. When the component needs to be suspended, the
hook should return a `Err(Suspension)` and users should unwrap it with
`?` in which it will be converted into `Html`.
```rust ,ignore
use yew::prelude::*;
use yew::suspense::{Suspension, SuspensionResult};
struct User {
name: String,
}
fn use_user() -> SuspensionResult<User> {
match load_user() {
// If a user is loaded, then we return it as Ok(user).
Some(m) => Ok(m),
None => {
// When user is still loading, then we create a `Suspension`
// and call `SuspensionHandle::resume` when data loading
// completes, the component will be re-rendered
// automatically.
let (s, handle) = Suspension::new();
on_load_user_complete(move || {handle.resume();});
Err(s)
},
}
}
```
# Complete Example
```rust
use yew::prelude::*;
use yew::suspense::{Suspension, SuspensionResult};
#[derive(Debug)]
struct User {
name: String,
}
fn load_user() -> Option<User> {
todo!() // implementation omitted.
}
fn on_load_user_complete<F: Fn()>(_fn: F) {
todo!() // implementation omitted.
}
fn use_user() -> SuspensionResult<User> {
match load_user() {
// If a user is loaded, then we return it as Ok(user).
Some(m) => Ok(m),
None => {
// When user is still loading, then we create a `Suspension`
// and call `SuspensionHandle::resume` when data loading
// completes, the component will be re-rendered
// automatically.
let (s, handle) = Suspension::new();
on_load_user_complete(move || {handle.resume();});
Err(s)
},
}
}
#[function_component(Content)]
fn content() -> HtmlResult {
let user = use_user()?;
Ok(html! {<div>{"Hello, "}{&user.name}</div>})
}
#[function_component(App)]
fn app() -> Html {
let fallback = html! {<div>{"Loading..."}</div>};
html! {
<Suspense {fallback}>
<Content />
</Suspense>
}
}
```
### Use Suspense in Struct Components
It's not possible to suspend a struct component directly. However, you
can use a function component as a Higher-Order-Component to
achieve suspense-based data fetching.
```rust ,ignore
use yew::prelude::*;
#[function_component(WithUser)]
fn with_user<T>() -> HtmlResult
where T: BaseComponent
{
let user = use_user()?;
Ok(html! {<T {user} />})
}
#[derive(Debug, PartialEq, Properties)]
pub struct UserContentProps {
pub user: User,
}
pub struct BaseUserContent;
impl Component for BaseUserContent {
type Properties = UserContentProps;
type Message = ();
fn create(ctx: &Context<Self>) -> Self {
Self
}
fn view(&self, ctx: &Context<Self>) -> Html {
let name = ctx.props().user.name;
html! {<div>{"Hello, "}{name}{"!"}</div>}
}
}
pub type UserContent = WithUser<BaseUserContent>;
```
## Relevant examples
- [Suspense](https://github.com/yewstack/yew/tree/master/examples/suspense)

View File

@ -13,122 +13,123 @@ module.exports = {
// By default, Docusaurus generates a sidebar from the docs folder structure
// conceptsSidebar: [{type: 'autogenerated', dirName: '.'}],
// But you can create a sidebar manually
sidebar: [
// But you can create a sidebar manually
sidebar: [
{
type: "category",
label: "Getting Started",
items: [
{
type: 'category',
label: 'Getting Started',
items: [
{
type: 'category',
label: 'Project Setup',
items: [
'getting-started/project-setup/introduction',
'getting-started/project-setup/using-trunk',
'getting-started/project-setup/using-wasm-pack',
]
},
"getting-started/build-a-sample-app",
"getting-started/examples",
"getting-started/starter-templates",
],
type: "category",
label: "Project Setup",
items: [
"getting-started/project-setup/introduction",
"getting-started/project-setup/using-trunk",
"getting-started/project-setup/using-wasm-pack",
],
},
"getting-started/build-a-sample-app",
"getting-started/examples",
"getting-started/starter-templates",
],
},
{
type: "category",
label: "Concepts",
items: [
{
type: "category",
label: "wasm-bindgen",
items: [
"concepts/wasm-bindgen/introduction",
"concepts/wasm-bindgen/web-sys",
],
},
{
type: "category",
label: "Concepts",
items: [
{
type: "category",
label: "wasm-bindgen",
items: [
"concepts/wasm-bindgen/introduction",
"concepts/wasm-bindgen/web-sys",
]
},
{
type: "category",
label: "Components",
items: [
"concepts/components/introduction",
"concepts/components/callbacks",
"concepts/components/scope",
"concepts/components/properties",
"concepts/components/children",
"concepts/components/refs"
],
},
{
type: "category",
label: "HTML",
items: [
"concepts/html/introduction",
"concepts/html/components",
"concepts/html/elements",
"concepts/html/events",
"concepts/html/classes",
"concepts/html/fragments",
"concepts/html/lists",
"concepts/html/literals-and-expressions"
]
},
{
type: "category",
label: "Function Components",
items: [
"concepts/function-components/introduction",
"concepts/function-components/attribute",
"concepts/function-components/pre-defined-hooks",
"concepts/function-components/custom-hooks",
]
},
"concepts/agents",
"concepts/contexts",
"concepts/router",
]
type: "category",
label: "Components",
items: [
"concepts/components/introduction",
"concepts/components/callbacks",
"concepts/components/scope",
"concepts/components/properties",
"concepts/components/children",
"concepts/components/refs",
],
},
{
type: 'category',
label: 'Advanced topics',
items: [
"advanced-topics/how-it-works",
"advanced-topics/optimizations",
"advanced-topics/portals",
]
type: "category",
label: "HTML",
items: [
"concepts/html/introduction",
"concepts/html/components",
"concepts/html/elements",
"concepts/html/events",
"concepts/html/classes",
"concepts/html/fragments",
"concepts/html/lists",
"concepts/html/literals-and-expressions",
],
},
{
type: 'category',
label: 'More',
items: [
"more/debugging",
"more/development-tips",
"more/external-libs",
"more/css",
"more/testing",
"more/roadmap",
"more/wasm-build-tools"
]
type: "category",
label: "Function Components",
items: [
"concepts/function-components/introduction",
"concepts/function-components/attribute",
"concepts/function-components/pre-defined-hooks",
"concepts/function-components/custom-hooks",
],
},
"concepts/agents",
"concepts/contexts",
"concepts/router",
"concepts/suspense",
],
},
{
type: "category",
label: "Advanced topics",
items: [
"advanced-topics/how-it-works",
"advanced-topics/optimizations",
"advanced-topics/portals",
],
},
{
type: "category",
label: "More",
items: [
"more/debugging",
"more/development-tips",
"more/external-libs",
"more/css",
"more/testing",
"more/roadmap",
"more/wasm-build-tools",
],
},
{
type: "category",
label: "Migration guides",
items: [
{
type: "category",
label: "yew",
items: ["migration-guides/yew/from-0_18_0-to-0_19_0"],
},
{
type: "category",
label: "Migration guides",
items: [
{
type: "category",
label: "yew",
items: ["migration-guides/yew/from-0_18_0-to-0_19_0"],
},
{
type: "category",
label: "yew-agent",
items: ["migration-guides/yew-agent/from-0_0_0-to-0_1_0"],
},
{
type: "category",
label: "yew-router",
items: ["migration-guides/yew-router/from-0_15_0-to-0_16_0"],
},
],
type: "category",
label: "yew-agent",
items: ["migration-guides/yew-agent/from-0_0_0-to-0_1_0"],
},
"tutorial"
],
{
type: "category",
label: "yew-router",
items: ["migration-guides/yew-router/from-0_15_0-to-0_16_0"],
},
],
},
"tutorial",
],
};