website: make tutorial testable (#3879)

This commit is contained in:
Tim Kurdov 2025-07-19 09:49:07 +00:00 committed by GitHub
parent 7ff45d3198
commit 4f3b85e31d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 427 additions and 222 deletions

2
Cargo.lock generated
View File

@ -3904,7 +3904,9 @@ dependencies = [
"derive_more",
"glob",
"gloo",
"gloo-net 0.6.0",
"js-sys",
"serde",
"tokio",
"wasm-bindgen",
"wasm-bindgen-futures",

View File

@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2021"
build = "build.rs"
publish = false
rust-version = "1.62"
rust-version = "1.81"
[dependencies]
yew-agent = { path = "../../packages/yew-agent/" }
@ -12,7 +12,9 @@ yew-agent = { path = "../../packages/yew-agent/" }
[dev-dependencies]
derive_more = { version = "2.0", features = ["from"] }
gloo = "0.11"
gloo-net = "0.6"
js-sys = "0.3"
serde = { version = "1.0", features = ["derive"] }
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
weblog = "0.3.0"

View File

@ -1,46 +1,141 @@
use std::collections::HashMap;
use std::fmt::{self, Write};
use std::error::Error;
use std::fmt::Write;
use std::fs::File;
use std::io::{self, BufRead, BufReader, ErrorKind, Read, Seek, SeekFrom};
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::{env, fs};
use glob::glob;
type Result<T = ()> = core::result::Result<T, Box<dyn Error + 'static>>;
macro_rules! e {
($($fmt:tt),* $(,)?) => {
return Err(format!($($fmt),*).into())
};
}
macro_rules! assert {
($condition:expr, $($fmt:tt),* $(,)?) => {
if !$condition { e!($($fmt),*) }
};
}
#[derive(Debug, Default)]
struct Level {
nested: HashMap<String, Level>,
files: Vec<PathBuf>,
}
fn main() {
let home = env::var("CARGO_MANIFEST_DIR").unwrap();
let pattern = format!("{home}/../../website/docs/**/*.md*");
let base = format!("{home}/../../website");
let base = Path::new(&base).canonicalize().unwrap();
let dir_pattern = format!("{home}/../../website/docs/**");
for dir in glob(&dir_pattern).unwrap() {
println!("cargo:rerun-if-changed={}", dir.unwrap().display());
fn should_combine_code_blocks(path: &Path) -> io::Result<bool> {
const FLAG: &[u8] = b"<!-- COMBINE CODE BLOCKS -->";
let mut file = File::open(path)?;
match file.seek(SeekFrom::End(-32)) {
Ok(_) => (),
Err(e) if e.kind() == ErrorKind::InvalidInput => return Ok(false),
Err(e) => return Err(e),
}
let mut buf = [0u8; 32];
file.read_exact(&mut buf)?;
Ok(buf.trim_ascii_end().ends_with(FLAG))
}
let mut level = Level::default();
fn apply_diff(src: &mut String, preamble: &str, added: &str, removed: &str) -> Result {
assert!(
!preamble.is_empty() || !removed.is_empty(),
"Failure on applying a diff: \nNo preamble or text to remove provided, unable to find \
location to insert:\n{added}\nIn the following text:\n{src}",
);
for entry in glob(&pattern).unwrap() {
let path = entry.unwrap();
let path = Path::new(&path).canonicalize().unwrap();
println!("cargo:rerun-if-changed={}", path.display());
let rel = path.strip_prefix(&base).unwrap();
let mut matches = src.match_indices(if preamble.is_empty() {
removed
} else {
preamble
});
let Some((preamble_start, _)) = matches.next() else {
e!(
"Failure on applying a diff: \ncouldn't find the following text:\n{preamble}\n\nIn \
the following text:\n{src}"
)
};
let mut parts = vec![];
assert!(
matches.next().is_none(),
"Failure on applying a diff: \nAmbiguous preamble:\n{preamble}\nIn the following \
text:\n{src}\nWhile trying to remove the following text:\n{removed}\nAnd add the \
following:\n{added}\n"
);
for part in rel {
parts.push(part.to_str().unwrap());
let preamble_end = preamble_start + preamble.len();
assert!(
src.get(preamble_end..preamble_end + removed.len()) == Some(removed),
"Failure on applying a diff: \nText to remove not found:\n{removed}\n\nIn the following \
text:\n{src}",
);
src.replace_range(preamble_end..preamble_end + removed.len(), added);
Ok(())
}
fn combined_code_blocks(path: &Path) -> Result<String> {
let file = BufReader::new(File::open(path)?);
let mut res = String::new();
let mut err = Ok(());
let mut lines = file
.lines()
.filter_map(|i| i.map_err(|e| err = Err(e)).ok());
while let Some(line) = lines.next() {
if !line.starts_with("```rust") {
continue;
}
level.insert(path.clone(), &parts[..]);
let mut preamble = String::new();
let mut added = String::new();
let mut removed = String::new();
let mut diff_applied = false;
for line in &mut lines {
if line.starts_with("```") {
if !added.is_empty() || !removed.is_empty() {
apply_diff(&mut res, &preamble, &added, &removed)?;
} else if !diff_applied {
// if no diff markers were found, just add the contents
res += &preamble;
}
break;
} else if let Some(line) = line.strip_prefix('+') {
if line.starts_with(char::is_whitespace) {
added += " ";
}
added += line;
added += "\n";
} else if let Some(line) = line.strip_prefix('-') {
if line.starts_with(char::is_whitespace) {
removed += " ";
}
removed += line;
removed += "\n";
} else if line.trim_ascii() == "// ..." {
// disregard the preamble
preamble.clear();
} else {
if !added.is_empty() || !removed.is_empty() {
diff_applied = true;
apply_diff(&mut res, &preamble, &added, &removed)?;
preamble += &added;
added.clear();
removed.clear();
}
preamble += &line;
preamble += "\n";
}
}
}
let out = format!("{}/website_tests.rs", env::var("OUT_DIR").unwrap());
fs::write(out, level.to_contents()).unwrap();
Ok(res)
}
impl Level {
@ -53,14 +148,14 @@ impl Level {
}
}
fn to_contents(&self) -> String {
fn to_contents(&self) -> Result<String> {
let mut dst = String::new();
self.write_inner(&mut dst, 0).unwrap();
dst
self.write_inner(&mut dst, 0)?;
Ok(dst)
}
fn write_into(&self, dst: &mut String, name: &str, level: usize) -> fmt::Result {
fn write_into(&self, dst: &mut String, name: &str, level: usize) -> Result {
self.write_space(dst, level);
let name = name.replace(['-', '.'], "_");
writeln!(dst, "pub mod {name} {{")?;
@ -73,24 +168,33 @@ impl Level {
Ok(())
}
fn write_inner(&self, dst: &mut String, level: usize) -> fmt::Result {
fn write_inner(&self, dst: &mut String, level: usize) -> Result {
for (name, nested) in &self.nested {
nested.write_into(dst, name, level)?;
}
self.write_space(dst, level);
for file in &self.files {
let stem = Path::new(file)
let stem = file
.file_stem()
.unwrap()
.ok_or_else(|| format!("no filename in path {file:?}"))?
.to_str()
.unwrap()
.ok_or_else(|| format!("non-UTF8 path: {file:?}"))?
.replace('-', "_");
self.write_space(dst, level);
writeln!(dst, "#[doc = include_str!(r\"{}\")]", file.display())?;
if should_combine_code_blocks(file)? {
let res = combined_code_blocks(file)?;
self.write_space(dst, level);
writeln!(dst, "/// ```rust, no_run")?;
for line in res.lines() {
self.write_space(dst, level);
writeln!(dst, "/// {line}")?;
}
self.write_space(dst, level);
writeln!(dst, "/// ```")?;
} else {
self.write_space(dst, level);
writeln!(dst, "#[doc = include_str!(r\"{}\")]", file.display())?;
}
self.write_space(dst, level);
writeln!(dst, "pub fn {stem}_md() {{}}")?;
}
@ -104,3 +208,48 @@ impl Level {
}
}
}
fn inner_main() -> Result {
let home = env::var("CARGO_MANIFEST_DIR")?;
let pattern = format!("{home}/../../website/docs/**/*.md*");
let base = format!("{home}/../../website");
let base = Path::new(&base).canonicalize()?;
let dir_pattern = format!("{home}/../../website/docs/**");
for dir in glob(&dir_pattern)? {
println!("cargo:rerun-if-changed={}", dir?.display());
}
let mut level = Level::default();
for entry in glob(&pattern)? {
let path = entry?.canonicalize()?;
println!("cargo:rerun-if-changed={}", path.display());
let rel = path.strip_prefix(&base)?;
let mut parts = vec![];
for part in rel {
parts.push(
part.to_str()
.ok_or_else(|| format!("Non-UTF8 path: {rel:?}"))?,
);
}
level.insert(path.clone(), &parts[..]);
}
let out = format!("{}/website_tests.rs", env::var("OUT_DIR")?);
fs::write(out, level.to_contents()?)?;
Ok(())
}
fn main() -> ExitCode {
match inner_main() {
Ok(_) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("{e}");
ExitCode::FAILURE
}
}
}

View File

@ -1,4 +1 @@
#![allow(clippy::needless_doctest_main)]
pub mod tutorial;
include!(concat!(env!("OUT_DIR"), "/website_tests.rs"));

View File

@ -1,7 +0,0 @@
#[derive(Clone, PartialEq, Eq)]
pub struct Video {
pub id: usize,
pub title: String,
pub speaker: String,
pub url: String,
}

View File

@ -24,6 +24,55 @@ Note this only builds for English locale unlike a production build.
> Documentation is written in `mdx`, a superset of markdown empowered with jsx.
> JetBrains and VSCode both provide MDX plugins.
## Testing
```console
cargo make website-test
```
[`website-test`](../tools/website-test) is a tool to test all code blocks in the docs as Rust doctests.
It gathers the Rust code blocks automatically, but by default they're all tested separate. In case of a
walkthrough, it makes more sense to combine the changes described in the blocks & test the code as one.
For this end `website-test` scans all doc files for a special flag:
```html
<!-- COMBINE CODE BLOCKS -->
```
If a file ends with this specific comment (and an optional newline after it), all code blocks will be
sown together, with respect to the diff markers in them. For example:
```md
\`\`\`rust
fn main() {
println!("Hello, World");
}
\`\`\`
\`\`\`rust
fn main() {
- println!("Hello, World");
+ println!("Goodbye, World");
}
\`\`\`
<!-- COMBINE CODE BLOCKS -->
```
Will be tested as:
```rust
fn main() {
println!("Goodbye, World");
}
```
:::warning
The current implementation only uses the code before the diff or the code to remove as context,
so make sure there's enough of it. The test assembler will tell you if there isn't.
:::
While assembling the code blocks, the test assembler will put special meaning into a code
line `// ...`. This line tells the test assembler to disregard any previous context for applying a diff
## Production Build
```console

View File

@ -318,14 +318,12 @@ Props are evaluated in the order they're specified, as shown by the following ex
#[derive(yew::Properties, PartialEq)]
struct Props { first: usize, second: usize, last: usize }
fn main() {
let mut g = 1..=3;
let props = yew::props!(Props { first: g.next().unwrap(), second: g.next().unwrap(), last: g.next().unwrap() });
let mut g = 1..=3;
let props = yew::props!(Props { first: g.next().unwrap(), second: g.next().unwrap(), last: g.next().unwrap() });
assert_eq!(props.first, 1);
assert_eq!(props.second, 2);
assert_eq!(props.last, 3);
}
assert_eq!(props.first, 1);
assert_eq!(props.second, 2);
assert_eq!(props.last, 3);
```
## Anti Patterns

View File

@ -164,7 +164,7 @@ html! { <div attribute={value} /> };
Properties are specified with `~` before the element name:
```rust
```rust , ignore
use yew::prelude::*;
html! { <my-element ~property="abc" /> };

View File

@ -185,22 +185,28 @@ We want to build a layout that looks something like this in raw HTML:
Now, let's convert this HTML into `html!`. Type (or copy/paste) the following snippet into the body of `app` function
such that the value of `html!` is returned by the function
```rust ,ignore
html! {
<>
<h1>{ "RustConf Explorer" }</h1>
<div>
<h3>{"Videos to watch"}</h3>
<p>{ "John Doe: Building and breaking things" }</p>
<p>{ "Jane Smith: The development process" }</p>
<p>{ "Matt Miller: The Web 7.0" }</p>
<p>{ "Tom Jerry: Mouseless development" }</p>
</div>
<div>
<h3>{ "John Doe: Building and breaking things" }</h3>
<img src="https://placehold.co/640x360.png?text=Video+Player+Placeholder" alt="video thumbnail" />
</div>
</>
```rust {3-21}
#[function_component(App)]
fn app() -> Html {
- html! {
- <h1>{ "Hello World" }</h1>
- }
+ html! {
+ <>
+ <h1>{ "RustConf Explorer" }</h1>
+ <div>
+ <h3>{ "Videos to watch" }</h3>
+ <p>{ "John Doe: Building and breaking things" }</p>
+ <p>{ "Jane Smith: The development process" }</p>
+ <p>{ "Matt Miller: The Web 7.0" }</p>
+ <p>{ "Tom Jerry: Mouseless development" }</p>
+ </div>
+ <div>
+ <h3>{ "John Doe: Building and breaking things" }</h3>
+ <img src="https://placehold.co/640x360.png?text=Video+Player+Placeholder" alt="video thumbnail" />
+ </div>
+ </>
+ }
}
```
@ -215,6 +221,7 @@ Now, instead of hardcoding the list of videos in the HTML, let's define them as
We create a simple `struct` (in `main.rs` or any file of our choice) that will hold our data.
```rust
#[derive(Clone, PartialEq)]
struct Video {
id: usize,
title: String,
@ -225,44 +232,49 @@ struct Video {
Next, we will create instances of this struct in our `app` function and use those instead of hardcoding the data:
```rust
use website_test::tutorial::Video; // replace with your own path
let videos = vec![
Video {
id: 1,
title: "Building and breaking things".to_string(),
speaker: "John Doe".to_string(),
url: "https://youtu.be/PsaFVLr8t4E".to_string(),
},
Video {
id: 2,
title: "The development process".to_string(),
speaker: "Jane Smith".to_string(),
url: "https://youtu.be/PsaFVLr8t4E".to_string(),
},
Video {
id: 3,
title: "The Web 7.0".to_string(),
speaker: "Matt Miller".to_string(),
url: "https://youtu.be/PsaFVLr8t4E".to_string(),
},
Video {
id: 4,
title: "Mouseless development".to_string(),
speaker: "Tom Jerry".to_string(),
url: "https://youtu.be/PsaFVLr8t4E".to_string(),
},
];
```rust {3-29}
#[function_component(App)]
fn app() -> Html {
+ let videos = vec![
+ Video {
+ id: 1,
+ title: "Building and breaking things".to_string(),
+ speaker: "John Doe".to_string(),
+ url: "https://youtu.be/PsaFVLr8t4E".to_string(),
+ },
+ Video {
+ id: 2,
+ title: "The development process".to_string(),
+ speaker: "Jane Smith".to_string(),
+ url: "https://youtu.be/PsaFVLr8t4E".to_string(),
+ },
+ Video {
+ id: 3,
+ title: "The Web 7.0".to_string(),
+ speaker: "Matt Miller".to_string(),
+ url: "https://youtu.be/PsaFVLr8t4E".to_string(),
+ },
+ Video {
+ id: 4,
+ title: "Mouseless development".to_string(),
+ speaker: "Tom Jerry".to_string(),
+ url: "https://youtu.be/PsaFVLr8t4E".to_string(),
+ },
+ ];
+
```
To display them, we need to convert the `Vec` into `Html`. We can do that by creating an iterator,
mapping it to `html!` and collecting it as `Html`:
```rust ,ignore
let videos = videos.iter().map(|video| html! {
<p key={video.id}>{format!("{}: {}", video.speaker, video.title)}</p>
}).collect::<Html>();
```rust {4-7}
},
];
+ let videos = videos.iter().map(|video| html! {
+ <p key={video.id}>{format!("{}: {}", video.speaker, video.title)}</p>
+ }).collect::<Html>();
+
```
:::tip
@ -271,21 +283,21 @@ Keys on list items help Yew keep track of which items have changed in the list,
And finally, we need to replace the hardcoded list of videos with the `Html` we created from the data:
```rust ,ignore {6-10}
html! {
<>
<h1>{ "RustConf Explorer" }</h1>
<div>
<h3>{ "Videos to watch" }</h3>
- <p>{ "John Doe: Building and breaking things" }</p>
- <p>{ "Jane Smith: The development process" }</p>
- <p>{ "Matt Miller: The Web 7.0" }</p>
- <p>{ "Tom Jerry: Mouseless development" }</p>
+ { videos }
</div>
// ...
</>
}
```rust {6-10}
html! {
<>
<h1>{ "RustConf Explorer" }</h1>
<div>
<h3>{ "Videos to watch" }</h3>
- <p>{ "John Doe: Building and breaking things" }</p>
- <p>{ "Jane Smith: The development process" }</p>
- <p>{ "Matt Miller: The Web 7.0" }</p>
- <p>{ "Tom Jerry: Mouseless development" }</p>
+ { videos }
</div>
// ...
</>
}
```
## Components
@ -305,16 +317,7 @@ In this tutorial, we will be using function components.
Now, let's split up our `App` component into smaller components. We begin by extracting the videos list into
its own component.
```rust ,compile_fail
use yew::prelude::*;
struct Video {
id: usize,
title: String,
speaker: String,
url: String,
}
```rust
#[derive(Properties, PartialEq)]
struct VideosListProps {
videos: Vec<Video>,
@ -336,33 +339,21 @@ In this case, `VideosListProps` is a struct that defines the props.
The struct used for props must implement `Properties` by deriving it.
:::
For the above code to compile, we need to modify the `Video` struct like this:
```rust {1}
#[derive(Clone, PartialEq)]
struct Video {
id: usize,
title: String,
speaker: String,
url: String,
}
```
Now, we can update our `App` component to make use of `VideosList` component.
```rust ,ignore {4-7,13-14}
```rust {4-7,13-14}
#[function_component(App)]
fn app() -> Html {
// ...
- let videos = videos.iter().map(|video| html! {
- <p key={video.id}>{format!("{}: {}", video.speaker, video.title)}</p>
- }).collect::<Html>();
- let videos = videos.iter().map(|video| html! {
- <p key={video.id}>{format!("{}: {}", video.speaker, video.title)}</p>
- }).collect::<Html>();
-
html! {
<>
<h1>{ "RustConf Explorer" }</h1>
<div>
<h3>{"Videos to watch"}</h3>
<h3>{ "Videos to watch" }</h3>
- { videos }
+ <VideosList videos={videos} />
</div>
@ -382,34 +373,35 @@ The final goal here is to display the selected video. To do that, `VideosList` c
parent when a video is selected, which is done via a `Callback`. This concept is called "passing handlers".
We modify its props to take an `on_click` callback:
```rust ,ignore {4}
```rust {4}
#[derive(Properties, PartialEq)]
struct VideosListProps {
videos: Vec<Video>,
+ on_click: Callback<Video>
+ on_click: Callback<Video>,
}
```
Then we modify the `VideosList` component to "emit" the selected video to the callback.
```rust ,ignore {2-4,6-12,15-16}
```rust {2-18}
#[function_component(VideosList)]
-fn videos_list(VideosListProps { videos }: &VideosListProps) -> Html {
+fn videos_list(VideosListProps { videos, on_click }: &VideosListProps) -> Html {
+ let on_click = on_click.clone();
videos.iter().map(|video| {
+ let on_video_select = {
+ let on_click = on_click.clone();
+ let video = video.clone();
+ Callback::from(move |_| {
+ on_click.emit(video.clone())
+ })
+ };
html! {
- <p key={video.id}>{format!("{}: {}", video.speaker, video.title)}</p>
- videos.iter().map(|video| html! {
- <p key={video.id}>{format!("{}: {}", video.speaker, video.title)}</p>
+ let on_click = on_click.clone();
+ videos.iter().map(|video| {
+ let on_video_select = {
+ let on_click = on_click.clone();
+ let video = video.clone();
+ Callback::from(move |_| {
+ on_click.emit(video.clone())
+ })
+ };
+
+ html! {
+ <p key={video.id} onclick={on_video_select}>{format!("{}: {}", video.speaker, video.title)}</p>
}
+ }
}).collect()
}
```
@ -418,9 +410,6 @@ Next, we need to modify the usage of `VideosList` to pass that callback. But bef
a new component, `VideoDetails`, that is displayed when a video is clicked.
```rust
use website_test::tutorial::Video;
use yew::prelude::*;
#[derive(Properties, PartialEq)]
struct VideosDetailsProps {
video: Video,
@ -439,36 +428,36 @@ fn video_details(VideosDetailsProps { video }: &VideosDetailsProps) -> Html {
Now, modify the `App` component to display `VideoDetails` component whenever a video is selected.
```rust ,ignore {4,6-11,13-15,22-23,25-29}
#[function_component(App)]
fn app() -> Html {
// ...
+ let selected_video = use_state(|| None);
+ let on_video_select = {
+ let selected_video = selected_video.clone();
+ Callback::from(move |video: Video| {
+ selected_video.set(Some(video))
+ })
+ };
+ let details = selected_video.as_ref().map(|video| html! {
+ <VideoDetails video={video.clone()} />
+ });
```rust {3-15,22-23,25-29}
},
];
+
+ let selected_video = use_state(|| None);
+
+ let on_video_select = {
+ let selected_video = selected_video.clone();
+ Callback::from(move |video: Video| {
+ selected_video.set(Some(video))
+ })
+ };
+
+ let details = selected_video.as_ref().map(|video| html! {
+ <VideoDetails video={video.clone()} />
+ });
html! {
<>
<h1>{ "RustConf Explorer" }</h1>
<div>
<h3>{"Videos to watch"}</h3>
<h3>{ "Videos to watch" }</h3>
- <VideosList videos={videos} />
+ <VideosList videos={videos} on_click={on_video_select.clone()} />
</div>
+ { for details }
- <div>
- <h3>{ "John Doe: Building and breaking things" }</h3>
- <img src="https://placehold.co/640x360.png?text=Video+Player+Placeholder" alt="video thumbnail" />
- </div>
+ { for details }
- <div>
- <h3>{ "John Doe: Building and breaking things" }</h3>
- <img src="https://placehold.co/640x360.png?text=Video+Player+Placeholder" alt="video thumbnail" />
- </div>
</>
}
}
@ -517,11 +506,12 @@ Otherwise you won't be able to run your application.
Update the `Video` struct to derive the `Deserialize` trait:
```rust ,ignore {1, 3-4}
+ use serde::Deserialize;
- #[derive(Clone, PartialEq)]
+ #[derive(Clone, PartialEq, Deserialize)]
```rust {2,4-5}
use yew::prelude::*;
+use serde::Deserialize;
// ...
-#[derive(Clone, PartialEq)]
+#[derive(Clone, PartialEq, Deserialize)]
struct Video {
id: usize,
title: String,
@ -532,32 +522,57 @@ struct Video {
Now as the last step, we need to update our `App` component to make the fetch request instead of using hardcoded data
```rust ,ignore {1,5-25,34-35}
+ use gloo_net::http::Request;
```rust {2,6-50,59-60}
use yew::prelude::*;
+use gloo_net::http::Request;
#[function_component(App)]
fn app() -> Html {
- let videos = vec![
- // ...
- ]
+ let videos = use_state(|| vec![]);
+ {
+ let videos = videos.clone();
+ use_effect_with((), move |_| {
+ let videos = videos.clone();
+ wasm_bindgen_futures::spawn_local(async move {
+ let fetched_videos: Vec<Video> = Request::get("https://yew.rs/tutorial/data.json")
+ .send()
+ .await
+ .unwrap()
+ .json()
+ .await
+ .unwrap();
+ videos.set(fetched_videos);
+ });
+ || ()
+ });
+ }
- let videos = vec![
- Video {
- id: 1,
- title: "Building and breaking things".to_string(),
- speaker: "John Doe".to_string(),
- url: "https://youtu.be/PsaFVLr8t4E".to_string(),
- },
- Video {
- id: 2,
- title: "The development process".to_string(),
- speaker: "Jane Smith".to_string(),
- url: "https://youtu.be/PsaFVLr8t4E".to_string(),
- },
- Video {
- id: 3,
- title: "The Web 7.0".to_string(),
- speaker: "Matt Miller".to_string(),
- url: "https://youtu.be/PsaFVLr8t4E".to_string(),
- },
- Video {
- id: 4,
- title: "Mouseless development".to_string(),
- speaker: "Tom Jerry".to_string(),
- url: "https://youtu.be/PsaFVLr8t4E".to_string(),
- },
- ];
-
+ let videos = use_state(|| vec![]);
+ {
+ let videos = videos.clone();
+ use_effect_with((), move |_| {
+ let videos = videos.clone();
+ wasm_bindgen_futures::spawn_local(async move {
+ let fetched_videos: Vec<Video> = Request::get("https://yew.rs/tutorial/data.json")
+ .send()
+ .await
+ .unwrap()
+ .json()
+ .await
+ .unwrap();
+ videos.set(fetched_videos);
+ });
+ || ()
+ });
+ }
// ...
@ -565,9 +580,9 @@ fn app() -> Html {
<>
<h1>{ "RustConf Explorer" }</h1>
<div>
<h3>{"Videos to watch"}</h3>
- <VideosList videos={videos} on_click={on_video_select.clone()} />
+ <VideosList videos={(*videos).clone()} on_click={on_video_select.clone()} />
<h3>{ "Videos to watch" }</h3>
- <VideosList videos={videos} on_click={on_video_select.clone()} />
+ <VideosList videos={(*videos).clone()} on_click={on_video_select.clone()} />
</div>
{ for details }
</>
@ -585,11 +600,9 @@ To fix that, we need a proxy server. Luckily trunk provides that.
Update the following line:
```rust ,ignore {2-3}
// ...
- let fetched_videos: Vec<Video> = Request::get("https://yew.rs/tutorial/data.json")
+ let fetched_videos: Vec<Video> = Request::get("/tutorial/data.json")
// ...
```rust {2-3}
- let fetched_videos: Vec<Video> = Request::get("https://yew.rs/tutorial/data.json")
+ let fetched_videos: Vec<Video> = Request::get("/tutorial/data.json")
```
Now, rerun the server with the following command:
@ -624,3 +637,5 @@ See [external libraries](/community/external-libs) for more details.
Read our [official documentation](../getting-started/introduction.mdx). It explains a lot of concepts in much more detail.
To learn more about the Yew API, see our [API docs](https://docs.rs/yew).
<!-- COMBINE CODE BLOCKS -->