Tutorial
Introduction
In this hands-on tutorial, we will take a look at how we can use Yew to build web applications.Yew is a modernRust framework for building front-end web apps usingWebAssembly.Yew encourages a reusable, maintainable, and well-structured architecture by leveraging Rust's powerful type system.A large ecosystem of community-created libraries, known in Rust ascrates,provide components for commonly-used patterns such as state management.Cargo, the package manager for Rust, allows us to take advantage of thenumerous crates available oncrates.io, such as Yew.
What we are going to build
Rustconf is an intergalactic gathering of the Rust community that happens annually.Rustconf 2020 had a plethora of talks that provided a good amount of information.In this hands-on tutorial, we will be building a web application to help fellow Rustaceansget an overview of the talks and watch them all from one page.
Setting up
Prerequisites
This tutorial assumes you are already familiar with Rust. If you are new to Rust,the freeRust Book offers a great starting point forbeginners and continues to be an excellent resource even for experienced Rust developers.
Ensure the latest version of Rust is installed by runningrustup update
or byinstalling rust if you have not already done so.
After installing Rust, you can use Cargo to installtrunk
by running:
cargo install trunk
We will also need to add the WASM build target by running:
rustup target add wasm32-unknown-unknown
Setting up the project
First, create a new cargo project:
cargo new yew-app
cd yew-app
To verify the Rust environment is set up properly, run the initial project using the cargo build tool.After the output about the build process, you should see the expected "Hello, world!" message.
cargo run
Our first static page
To convert this simple command line application to a basic Yew web application, a few changes are needed.Update the files as follows:
[package]
name="yew-app"
version="0.1.0"
edition="2021"
[dependencies]
yew={git="https://github.com/yewstack/yew/",features=["csr"]}
You only need the featurecsr
if you are building an application.It will enable theRenderer
and all client-side rendering-related code.
If you are making a library, do not enable this feature as it will pull inclient-side rendering logic into the server-side rendering bundle.
If you need the Renderer for testing or examples, you should enable itin thedev-dependencies
instead.
useyew::prelude::*;
#[function_component(App)]
fnapp()->Html{
html!{
<h1>{"Hello World"}</h1>
}
}
fnmain(){
yew::Renderer::<App>::new().render();
}
Now, let's create anindex.html
at the root of the project.
<!doctypehtml>
<htmllang="en">
<head></head>
<body></body>
</html>
Start the development server
Run the following command to build and serve the application locally.
trunk serve --open
Remove option '--open' to not open your default browsertrunk serve
.
Trunk will open your application in your default browser, watch the project directory and helpfully rebuild yourapplication if you modify any source files.This will fail if the socket is being used by another application.By default server will listening at address '127.0.0.1' and port '8080' =>http://localhost:8080.To change it, create the following file and edit as needed:
[serve]
# The address to serve on LAN.
address="127.0.0.1"
# The address to serve on WAN.
# address = "0.0.0.0"
# The port to serve on.
port=8000
If you are curious, you can runtrunk help
andtrunk help <subcommand>
for more details on what is happening.
Congratulations
You have now successfully set up your Yew development environment and built your first Yew web application.
Building HTML
Yew makes use of Rust's procedural macros and provides us with a syntax similar to JSX (an extension to JavaScriptwhich allows you to write HTML-like code inside JavaScript) to create the markup.
Converting classic HTML
Since we already have a pretty good idea of what our website will look like, we can simply translate our mental draftinto a representation compatible withhtml!
. If you are comfortable writing simple HTML, you should have no problemwriting marking insidehtml!
. It is important to note that the macro does differ from HTML in a few ways:
- Expressions must be wrapped in curly braces (
{ }
) - There must only be one root node. If you want to have multiple elements without wrapping them in a container,an empty tag/fragment (
<> ... </>
) is used - Elements must be closed properly.
We want to build a layout that looks something like this in raw 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>
Now, let's convert this HTML intohtml!
. Type (or copy/paste) the following snippet into the body ofapp
functionsuch that the value ofhtml!
is returned by the function
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>
</>
}
Refresh the browser page, and you should see the following output displayed:
Using Rust language constructs in the markup
A big advantage of writing markup in Rust is that we get all the coolness of Rust in our markup.Now, instead of hardcoding the list of videos in the HTML, let's define them as aVec
ofVideo
structs.We create a simplestruct
(inmain.rs
or any file of our choice) that will hold our data.
structVideo{
id:usize,
title:String,
speaker:String,
url:String,
}
Next, we will create instances of this struct in ourapp
function and use those instead of hardcoding the data:
usewebsite_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(),
},
];
To display them, we need to convert theVec
intoHtml
. We can do that by creating an iterator,mapping it tohtml!
and collecting it asHtml
:
let videos= videos.iter().map(|video|html!{
<p key={video.id}>{format!("{}: {}", video.speaker, video.title)}</p>
}).collect::<Html>();
Keys on list items help Yew keep track of which items have changed in the list, resulting in faster re-renders.It is always recommended to use keys in lists.
And finally, we need to replace the hardcoded list of videos with theHtml
we created from the data:
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
Components are the building blocks of Yew applications. By combining components, which can be made of other components,we build our application. By structuring our components for re-usability and keeping them generic, we will be able to usethem in multiple parts of our application without having to duplicate code or logic.
Theapp
function we have been using so far is a component, calledApp
. It is a "function component".There are two different types of components in Yew.
- Struct Components
- Function Components
In this tutorial, we will be using function components.
Now, let's split up ourApp
component into smaller components. We begin by extracting the videos list intoits own component.
useyew::prelude::*;
structVideo{
id:usize,
title:String,
speaker:String,
url:String,
}
#[derive(Properties, PartialEq)]
structVideosListProps{
videos:Vec<Video>,
}
#[function_component(VideosList)]
fnvideos_list(VideosListProps{ videos}:&VideosListProps)->Html{
videos.iter().map(|video|html!{
<p key={video.id}>{format!("{}: {}", video.speaker, video.title)}</p>
}).collect()
}
Notice the parameters of ourVideosList
function component. A function component takes only one argument whichdefines its "props" (short for "properties"). Props are used to pass data down from a parent component to a child component.In this case,VideosListProps
is a struct that defines the props.
The struct used for props must implementProperties
by deriving it.
For the above code to compile, we need to modify theVideo
struct like this:
#[derive(Clone, PartialEq)]
structVideo{
id:usize,
title:String,
speaker:String,
url:String,
}
Now, we can update ourApp
component to make use ofVideosList
component.
#[function_component(App)]
fnapp()->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>
-{ videos}
+<VideosList videos={videos}/>
</div>
// ...
</>
}
}
By looking at the browser window, we can verify that the lists are rendered as they should be.We have moved the rendering logic of lists to its component. This shortens theApp
component’s source code,making it easier for us to read and understand.
Making it interactive
The final goal here is to display the selected video. To do that,VideosList
component needs to "notify" itsparent when a video is selected, which is done via aCallback
. This concept is called "passing handlers".We modify its props to take anon_click
callback:
#[derive(Properties, PartialEq)]
structVideosListProps{
videos:Vec<Video>,
+ on_click:Callback<Video>
}
Then we modify theVideosList
component to "emit" the selected video to the callback.
#[function_component(VideosList)]
-fnvideos_list(VideosListProps{ videos}:&VideosListProps)->Html{
+fnvideos_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>
+<p key={video.id} onclick={on_video_select}>{format!("{}: {}", video.speaker, video.title)}</p>
}
}).collect()
}
Next, we need to modify the usage ofVideosList
to pass that callback. But before doing that, we should createa new component,VideoDetails
, that is displayed when a video is clicked.
usewebsite_test::tutorial::Video;
useyew::prelude::*;
#[derive(Properties, PartialEq)]
structVideosDetailsProps{
video:Video,
}
#[function_component(VideoDetails)]
fnvideo_details(VideosDetailsProps{ video}:&VideosDetailsProps)->Html{
html!{
<div>
<h3>{ video.title.clone()}</h3>
<img src="https://placehold.co/640x360.png?text=Video+Player+Placeholder" alt="video thumbnail"/>
</div>
}
}
Now, modify theApp
component to displayVideoDetails
component whenever a video is selected.
#[function_component(App)]
fnapp()->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()}/>
+});
html!{
<>
<h1>{"RustConf Explorer"}</h1>
<div>
<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>
</>
}
}
Do not worry about theuse_state
right now, we will come back to that later.Note the trick we pulled with{ for details }
.Option<_>
implementsIterator
so we can use it to display the onlyelement returned by theIterator
with a special{ for ... }
syntaxsupported by thehtml!
macro.
Handling state
Remember theuse_state
used earlier? That is a special function, called a "hook". Hooks are used to "hook" intothe lifecycle of a function component and perform actions. You can learn more about this hook, and othershere.
Struct components act differently. Seethe documentation to learn about those.
Fetching data (using external REST API)
In a real-world application, data will usually come from an API instead of being hardcoded. Let's fetch ourvideos list from an external source. For this we will need to add the following crates:
gloo-net
For making the fetch call.serde
with derive featuresFor de-serializing the JSON responsewasm-bindgen-futures
For executing Rust Future as a Promise
Let's update the dependencies inCargo.toml
file:
[dependencies]
gloo-net="0.2"
serde={version="1.0",features=["derive"]}
wasm-bindgen-futures="0.4"
When choosing dependencies make sure they arewasm32
compatible!Otherwise you won't be able to run your application.
Update theVideo
struct to derive theDeserialize
trait:
+useserde::Deserialize;
-#[derive(Clone, PartialEq)]
+#[derive(Clone, PartialEq, Deserialize)]
structVideo{
id:usize,
title:String,
speaker:String,
url:String,
}
Now as the last step, we need to update ourApp
component to make the fetch request instead of using hardcoded data
+usegloo_net::http::Request;
#[function_component(App)]
fnapp()->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(asyncmove{
+let fetched_videos:Vec<Video>=Request::get("https://yew.rs/tutorial/data.json")
+.send()
+.await
+.unwrap()
+.json()
+.await
+.unwrap();
+ videos.set(fetched_videos);
+});
+||()
+});
+}
// ...
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()}/>
</div>
{for details}
</>
}
}
We are usingunwrap
s here because this is a demo application. In a real-world app, you would likely want to haveproper error handling.
Now, look at the browser to see everything working as expected... which would have been the case if it were not for CORS.To fix that, we need a proxy server. Luckily trunk provides that.
Update the following line:
// ...
-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:
trunk serve --proxy-backend=https://yew.rs/tutorial
Refresh the tab and everything should work as expected.
Wrapping up
Congratulations! You’ve created a web application that fetches data from an external API and displays a list of videos.
What's next
This application is very far from perfect or useful. After going through this tutorial,you can use it as a jumping-off point to explore more advanced topics.
Styles
Our apps look very ugly. There is no CSS or any kind of style.Unfortunately, Yew does not offer a built-in way to style components. SeeTrunk's assetsto learn how to add style sheets.
More libraries
Our app made use of only a few external dependencies. There are lots of crates out there that can be used.Seeexternal libraries for more details.
Learning more about Yew
Read ourofficial documentation. It explains a lot of concepts in much more detail.To learn more about the Yew API, see ourAPI docs.