Adding Items

Now we are going to add items from our frontend!

To achieve this, we will add a form that will post new items to our backend. After sending the new item, the list will be fetched again, so we have the latest list.

Creating a helper function

Let's first create a client function, that sends the post request to our backend. It's similar to the get_items function we have already written. Go to the main.rs of your frontend and create a post_item function.

#![allow(unused)]
fn main() {
async fn post_item(item: PostShopItem) -> Result<ShoppingListItem, reqwest::Error> {
    let response = reqwest::Client::new()
        .post("http://localhost:3001/items")
        .json(&item)
        .send()
        .await?
        .json::<ShoppingListItem>()
        .await?;

    Ok(response)
}
}

The post_item function will be used in our component we will write next.

Adding a form

We need a form with two fields. Namely the item name, as well as the person who wants that item, an author. Let's begin simple. We need a form that has two input fields - and a button to commit those fields. Let's add those.

#![allow(unused)]
fn main() {
#[component]
fn ItemInput() -> Element {
    let mut item = use_signal(|| "".to_string());
    let mut author = use_signal(|| "".to_string());

    // We implement this closure later
    let onsubmit = move |evt: FormEvent| {};

    rsx! {
        div {
            class: "w-300 m-4 mt-16 rounded",
            form { class: "grid grid-cols-3 gap-2",
                onsubmit: onsubmit,
                div {
                    input {
                        value: "{item}",
                        class: "input input-bordered input-primary w-full",
                        placeholder: "next item..",
                        r#type: "text",
                        id: "item_name",
                        name: "item_name",
                        oninput: move |e| item.set(e.data.value().clone())
                    }
                }
                div {
                    input {
                        value: "{author}",
                        class: "input input-bordered input-primary w-full",
                        placeholder: "wanted by..",
                        r#type: "text",
                        id: "author",
                        name: "author",
                        oninput: move |e| author.set(e.data.value().clone())
                    }
                }
                button {
                    class: "btn btn-primary w-full",
                    r#type: "submit",
                    "Commit"
                }
            }
        }
    }
}
}

The content inside the rsx might seem long. But in the end you have three elements. Namely two input fields, as well as a button to commit the two input fields. Currently, we do not push anything to our backend yet. This going to happen in the onsubmit action, which we will implement later. If you ask yourself what the use_signals are doing: They give this component state.

When the component is rendered first, we initialize item and author to empty Strings. Every time we input something into the input fields (oninput actions), the respective symbols are set, when we enter a character. The plan: If we press the button, then we use the current stored strings, build an PostShopItem out them and push this to our backend.

But let's add this component to our existing app first, so you can already examine the UI.

#![allow(unused)]
fn main() {
pub fn App() -> Element {
    let rust_basel = "Rust Basel";
    rsx! {
        h1{
            "Welcome to {rust_basel}"
        }
        button{
            class: "btn",
            "My stylish button"
        }
        ShoppingList{}
        ItemInput{}
    }
}
}

If you run our cargo make --no-workspace dev, then you can already see the finished form.

OnSubmit Action - posting items

Currently, we do nothing on submit - except changing the state of the two hooks. Let's add a closure, that posts items to our backend. For this we will use spawn. This allows us to run async function inside our components, which are fired once.

#![allow(unused)]
fn main() {
let onsubmit = move |_| {
    spawn({
        async move {
            let item_name = item.read().to_string();
            let author = author.read().to_string();
            let _ = post_item(PostShopItem {
                title: item_name,
                posted_by: author,
            })
            .await;
        }
    });
};
}

As we stored the item as well as the author value inside hooks (our components state), we can just read those values and copy them as strings. For now we just ignore the result of our http call for simplicity.

If you now run everything you are able to create new items. The downside: You do not recognize that you already post items, as the list is not getting updated. But if you open your browser's developer settings (usually f12), you will see in your network tab, that you post items.

But let us fix the list in the next step.

Use resource - dependencies

You can configure the use_resource hook. It is run when the components renders the first time. If you want to re-run the hook, then you have to add a dependency. If this dependency changes, the use_resource is run again. Let's do exactly this.

Let's first introduce something, that signals a change.

#![allow(unused)]
fn main() {
struct ListChanged;
}

In our case a zero-sized object is sufficient.

The idea:

  • When we post an item - and the post request has been successful - then we write to that signal.
  • Our list display subscribes this signal. Because we write to it, anything that depends on it (i.e. our use_resource hook), will be executed again.

In order that both components ItemInput and ShoppingList both have access to that signal, we have to hoist (lifting the state up) the state.

So let's add a use_signal hook on top level and add the signal to both components:

New components signature:

#![allow(unused)]
fn main() {
fn ShoppingList(change_signal: Signal<ListChanged>) -> Element {...}
}
#![allow(unused)]
fn main() {
fn ItemInput(change_signal: Signal<ListChanged>) -> Element {...}
}

and add it to your App function:

#![allow(unused)]
fn main() {
pub fn App() -> Element {
    let change_signal = use_signal(|| ListChanged);
    let rust_basel = "Rust Basel";
    rsx! {
        h1{
            "Welcome to {rust_basel}"
        }
        button{
            class: "btn",
            "My stylish button"
        }
        ShoppingList{change_signal}
        ItemInput{change_signal}
    }
}
}

Let's read the signal on the list fetching side - and write on the input side in the next steps.

Writing the signal

In the ItemInput component, go into your onsubmit callback. Now we can use the Result coming from our post_item http request. Change it and check the result, if it is ok. If it is ok, we write the signal (without writing anything to it).

#![allow(unused)]
fn main() {
  let item_name = item.read().to_string();
  let author = author.read().to_string();
  let response = post_item(PostShopItem {
      title: item_name,
      posted_by: author,
  })
  .await;

  if response.is_ok() {
      change_signal.write();
  }
}

But that is sufficient for re-rendering other components subscribed to that signal (automatically when you have a read). Or to execute some logic on subscribed components withing hooks.

Reading the signal

Now let's subscribe to that signal by reading it inside our ShoppingList component's use_resource hook.

#![allow(unused)]
fn main() {
  let items_request = use_resource(move || async move {
      change_signal.read();
      get_items().await
  });
}

That is all you need to do in order let your components communicate with each other.

If you did it correctly, the list is fetched and updates just after you add a new item (if not - have a look at the solution)

Great! :)

Let's do the same to delete items in our next chapter.