App Effects
Alright folks, we've got our state management all set up. Now, the magic happens! We need to synchronize the values of that state when different parts of our app interact with our users.
Imagine our first call to the API to fetch our freshly minted films, or the moment when we open the Film Modal in edit mode. We need to pre-populate the form with the values of the film we're about to edit.
No sweat, we've got the use_effect
hook to handle this. This useful hook allows us to execute a function when a value changes, or when the component is mounted or unmounted. Pretty cool, huh?
Now, let's break down the key parts of the use_effect
hook:
- It should be nestled inside a closure function.
- If we're planning to use a
use_state
hook inside it, we need toclone()
it or pass the ownership usingto_owned()
to the closure function. - The parameters inside the
use_effect()
function include the Scope of our app (cx
), thedependencies
that will trigger the effect again, and afuture
that will spring into action when the effect is triggered.
Here's a quick look at how it works:
#![allow(unused)] fn main() { { let some_state = some_state.clone(); use_effect(cx, change_dependency, |_| async move { // Do something with some_state or something else }) } }
Film Modal
We will begin by adapting our FilmModal
component. This will be modified to pre-populate the form with the values of the film that is currently being edited. To accomplish this, we will use the use_effect
hook.
front/src/components/film_modal.rs
...
pub fn FilmModal<'a>(cx: Scope<'a, FilmModalProps>) -> Element<'a> {
let is_modal_visible = use_shared_state::<FilmModalVisibility>(cx).unwrap();
let draft_film = use_state::<Film>(cx, || Film {
title: "".to_string(),
poster: "".to_string(),
director: "".to_string(),
year: 1900,
id: Uuid::new_v4(),
created_at: None,
updated_at: None,
});
+ {
+ let draft_film = draft_film.clone();
+ use_effect(cx, &cx.props.film, |film| async move {
+ match film {
+ Some(film) => draft_film.set(film),
+ None => draft_film.set(Film {
+ title: "".to_string(),
+ poster: "".to_string(),
+ director: "".to_string(),
+ year: 1900,
+ id: Uuid::new_v4(),
+ created_at: None,
+ updated_at: None,
+ }),
+ }
+ });
+ }
...
}
In essence, we are initiating an effect when the film
property changes. If the film
property is Some(film)
, we set the draft_film
state to the value of the film
property. If the film
property is None
, we set the draft_film
state to a new Film
initial object.
App Component
Next, we will adapt our App
component to fetch the films from the API when the app is mounted or when we need to force the API to update the list of films. We'll accomplish this by modifying force_get_films
. As this state has no type or initial value, it is solely used to trigger the effect.
We will also add HTTP request configurations to enable these functions. We will use the reqwest
crate for this purpose, which can be added to our Cargo.toml
file or installed with the following command:
cargo add reqwest
To streamline future requests, we will create a films_endpoint()
function to return the URL of our API endpoint.
First install some missing dependencies by updating our Cargo.toml
.
front/Cargo.toml
+reqwest = { version = "0.11.18", features = ["json"] }
+web-sys = "0.3.64"
+serde = { version = "1.0.164", features = ["derive"] }
After that, here are the necessary modifications for the App
component:
front/src/main.rs
...
+const API_ENDPOINT: &str = "api/v1";
+fn films_endpoint() -> String {
+ let window = web_sys::window().expect("no global `window` exists");
+ let location = window.location();
+ let host = location.host().expect("should have a host");
+ let protocol = location.protocol().expect("should have a protocol");
+ let endpoint = format!("{}//{}/{}", protocol, host, API_ENDPOINT);
+ format!("{}/films", endpoint)
+}
+async fn get_films() -> Vec<Film> {
+ log::info!("Fetching films from {}", films_endpoint());
+ reqwest::get(&films_endpoint())
+ .await
+ .unwrap()
+ .json::<Vec<Film>>()
+ .await
+ .unwrap()
+}
fn App(cx: Scope) -> Element {
...
let force_get_films = use_state(cx, || ());
+ {
+ let films = films.clone();
+ use_effect(cx, force_get_films, |_| async move {
+ let existing_films = get_films().await;
+ if existing_films.is_empty() {
+ films.set(None);
+ } else {
+ films.set(Some(existing_films));
+ }
+ });
+ }
}
What we have done here is trigger an effect whenever there is a need to fetch films from our API. We then evaluate whether there are any films available. If there are, we set the films
state to these existing films. If not, we set the films
state to None
. This allows us to enhance our App
component with additional functionality.