The Component Pattern

The component pattern allows you to cleanly structure your application, including your state, update and view logic. This pattern can also be found in this showcase from the founder of iced.

Just like your top level iced application, a component implements the Model-View-Update architecture and implements the following:

  • The component state, usually named after it's function (e.g. LoginForm)
  • It's own Message
  • An Action enum

In effect, the component is a self contained iced program.

The state will have a normal view function that returns a iced::Element<Message>.

The update function will differ a bit. Instead of a Task like our main application, it will return an action enum.

impl MyComponent {
    // instead of this
    pub fn update(&mut self, message: Message) -> Task<Message> {}
    // we implement this
    pub fn update(&mut self, message: Message) -> Action {}
}

Common variants the Action enum can be None, Task(iced::Task<Message>) and Submit { username: String, password: String }. You can think of this action like a message that is sent from the view to the parent application that should handle it.

The State

Your state is usually called after the it's function, e.g. NewJoke or LazyImage. It contains the data you are currently working with.


#[derive(Debug, Clone)]
pub enum Message {
    ChangeContent(String),
    RandomJoke,
    Submit,
    Cancel,
}

pub enum Action {
    // The user was happy with the joke and wants to submit it to the list
    Submit(String),
    // The user wants to cancel adding a new joke
    Cancel,
    // The components needs to run a task
    Run(iced::Task<Message>),
    // The component does not require any additional actions
    None,
}

pub struct NewJoke {
    joke: String,
}

impl NewJoke {
    pub fn new() -> Self {
        Self {
            joke: String::new(),
        }
    }
}

impl NewJoke {
    pub fn view(&self) -> iced::Element<Message> {
        iced::widget::column![
            iced::widget::text_input("Content", &self.joke)
                // on_input expects a closure, which would usually look like this:
                // |new_value| Message::ChangeContent(new_value)
                // Thankfully, you can just use the enum variants name directly
                .on_input(Message::ChangeContent),
            iced::widget::button("Random Joke").on_press(Message::RandomJoke),
            iced::widget::row![
                iced::widget::button("Cancel").on_press(Message::Cancel),
                iced::widget::button("Submit").on_press(Message::Submit)
            ]
            .spacing(10),
        ]
        .padding(10)
        .spacing(10)
        .into()
    }
}

impl NewJoke {
    #[must_use]
    pub fn update(&mut self, message: Message) -> Action {
        match message {
            Message::Submit => Action::Submit(self.joke.clone()),
            Message::Cancel => Action::Cancel,
            Message::ChangeContent(content) => {
                self.joke = content;
                Action::None
            }
            Message::RandomJoke => Action::Run(Self::random_joke_task()),
        }
    }

    fn random_joke_task() -> iced::Task<Message> {
        iced::Task::future(async {
            // Fetch a joke from the internet
            let client = reqwest::Client::new();
            let response: serde_json::Value = client
                .get("https://icanhazdadjoke.com")
                .header("Accept", "application/json")
                .send()
                .await
                .unwrap()
                .json()
                .await
                .unwrap();

            // Parse the response
            let joke = response["joke"].as_str().unwrap();

            // Return the joke as a message
            Message::ChangeContent(joke.to_owned())
        })
    }
}

Your state should have a function, which creates a new instance.


#[derive(Debug, Clone)]
pub enum Message {
    ChangeContent(String),
    RandomJoke,
    Submit,
    Cancel,
}

pub enum Action {
    // The user was happy with the joke and wants to submit it to the list
    Submit(String),
    // The user wants to cancel adding a new joke
    Cancel,
    // The components needs to run a task
    Run(iced::Task<Message>),
    // The component does not require any additional actions
    None,
}

pub struct NewJoke {
    joke: String,
}

impl NewJoke {
    pub fn new() -> Self {
        Self {
            joke: String::new(),
        }
    }
}

impl NewJoke {
    pub fn view(&self) -> iced::Element<Message> {
        iced::widget::column![
            iced::widget::text_input("Content", &self.joke)
                // on_input expects a closure, which would usually look like this:
                // |new_value| Message::ChangeContent(new_value)
                // Thankfully, you can just use the enum variants name directly
                .on_input(Message::ChangeContent),
            iced::widget::button("Random Joke").on_press(Message::RandomJoke),
            iced::widget::row![
                iced::widget::button("Cancel").on_press(Message::Cancel),
                iced::widget::button("Submit").on_press(Message::Submit)
            ]
            .spacing(10),
        ]
        .padding(10)
        .spacing(10)
        .into()
    }
}

impl NewJoke {
    #[must_use]
    pub fn update(&mut self, message: Message) -> Action {
        match message {
            Message::Submit => Action::Submit(self.joke.clone()),
            Message::Cancel => Action::Cancel,
            Message::ChangeContent(content) => {
                self.joke = content;
                Action::None
            }
            Message::RandomJoke => Action::Run(Self::random_joke_task()),
        }
    }

    fn random_joke_task() -> iced::Task<Message> {
        iced::Task::future(async {
            // Fetch a joke from the internet
            let client = reqwest::Client::new();
            let response: serde_json::Value = client
                .get("https://icanhazdadjoke.com")
                .header("Accept", "application/json")
                .send()
                .await
                .unwrap()
                .json()
                .await
                .unwrap();

            // Parse the response
            let joke = response["joke"].as_str().unwrap();

            // Return the joke as a message
            Message::ChangeContent(joke.to_owned())
        })
    }
}

Handling lazy loading

If you want to lazily load some data from an API, or do something else in the background, you can use an enum to indicate the state. You can either hold that enum as part of your State struct, or just use the enum directly.

pub enum LazyImage {
    Loading,
    Loaded {
        title: String,
        image: Vec<u8>,
    },
    Error(String),
}

If you need to execute some code asynchronously after your component is created, you can instead return (Self, iced::Task<Message>). This mirrors iced's run_with function, which allows you to run a task when starting your applocation.

impl LazyImage {
    pub fn new(url: String) -> (Self, iced::Task<Message>) {
        let task = iced::Task::spawn(async move {
            // load your image data
            let response = todo!().await;
            match image {
                Ok(data) => Message::Loaded {
                    title: data.title,
                    image: data.body
                },
                Err(error) => Message::Error(error.to_string())
            }
        });

        (Self::Loading, task)
    }
}

Message

Your component will have it's own internal messages, which work just like in your top level iced application. For a LoginForm they might look like this:


#[derive(Debug, Clone)]
pub enum Message {
    ChangeContent(String),
    RandomJoke,
    Submit,
    Cancel,
}

pub enum Action {
    // The user was happy with the joke and wants to submit it to the list
    Submit(String),
    // The user wants to cancel adding a new joke
    Cancel,
    // The components needs to run a task
    Run(iced::Task<Message>),
    // The component does not require any additional actions
    None,
}

pub struct NewJoke {
    joke: String,
}

impl NewJoke {
    pub fn new() -> Self {
        Self {
            joke: String::new(),
        }
    }
}

impl NewJoke {
    pub fn view(&self) -> iced::Element<Message> {
        iced::widget::column![
            iced::widget::text_input("Content", &self.joke)
                // on_input expects a closure, which would usually look like this:
                // |new_value| Message::ChangeContent(new_value)
                // Thankfully, you can just use the enum variants name directly
                .on_input(Message::ChangeContent),
            iced::widget::button("Random Joke").on_press(Message::RandomJoke),
            iced::widget::row![
                iced::widget::button("Cancel").on_press(Message::Cancel),
                iced::widget::button("Submit").on_press(Message::Submit)
            ]
            .spacing(10),
        ]
        .padding(10)
        .spacing(10)
        .into()
    }
}

impl NewJoke {
    #[must_use]
    pub fn update(&mut self, message: Message) -> Action {
        match message {
            Message::Submit => Action::Submit(self.joke.clone()),
            Message::Cancel => Action::Cancel,
            Message::ChangeContent(content) => {
                self.joke = content;
                Action::None
            }
            Message::RandomJoke => Action::Run(Self::random_joke_task()),
        }
    }

    fn random_joke_task() -> iced::Task<Message> {
        iced::Task::future(async {
            // Fetch a joke from the internet
            let client = reqwest::Client::new();
            let response: serde_json::Value = client
                .get("https://icanhazdadjoke.com")
                .header("Accept", "application/json")
                .send()
                .await
                .unwrap()
                .json()
                .await
                .unwrap();

            // Parse the response
            let joke = response["joke"].as_str().unwrap();

            // Return the joke as a message
            Message::ChangeContent(joke.to_owned())
        })
    }
}

Embedding our state

To use our component, we'll need to add it to our application state.

Depending on your use case, you can embed it directly, as an Option or inside another enum. For demonstration purposes, we'll add our own little enum.

mod list_item;
mod new_joke;

fn main() {
    iced::run("Project Structure Example", App::update, App::view).unwrap();
}

#[derive(Debug, Clone)]
enum Message {
    // This message is used to handle the new views message
    NewJoke(new_joke::Message),
    OpenNewJokeComponent,
    Delete(usize),
}

#[derive(Default)]
enum View {
    #[default]
    ListJokes,
    // This holds our new joke components state
    NewJoke(new_joke::NewJoke),
}

#[derive(Default)]
struct App {
    view: View,
    items: Vec<String>,
}

impl App {
    fn update(&mut self, message: Message) -> iced::Task<Message> {
        match message {
            Message::NewJoke(view_message) => {
                // as with all enums in rust, we'll need to use an if-let expression
                // to get access to our component from the `View` enum
                if let View::NewJoke(edit) = &mut self.view {
                    // Call the update method of the edit view
                    // and handle the returned action
                    match edit.update(view_message) {
                        // The none action is a no-op
                        new_joke::Action::None => {}

                        // If the action is a task, we'll need to map it, to ensure it returns the right Message type.
                        // This is the exact same as with `view` and the returned `iced::Element`
                        new_joke::Action::Run(task) => return task.map(Message::NewJoke),

                        // If the action is a cancel, switch back to the list view
                        new_joke::Action::Cancel => {
                            self.view = View::ListJokes;
                        }

                        // If the action is a submit, add the new joke before returning to the list view
                        new_joke::Action::Submit(new_joke_content) => {
                            self.view = View::ListJokes;
                            self.items.push(new_joke_content);
                        }
                    }
                }
            }
            Message::OpenNewJokeComponent => {
                // Create a new component
                let component = new_joke::NewJoke::new();
                self.view = View::NewJoke(component);
            }
            Message::Delete(index) => {
                self.items.remove(index);
            }
        }
        iced::Task::none()
    }

    fn view(&self) -> iced::Element<Message> {
        match &self.view {
            View::ListJokes => {
                let items = self
                    .items
                    .iter()
                    // since we want deletion, we'll need the index of each item, so we know which one to delete
                    .enumerate()
                    .map(|(index, item)| {
                        // create a listitem for each joke
                        list_item::ListItem::new(iced::widget::text(item))
                            // save the index to delete in the message
                            .on_delete(Message::Delete(index))
                            // since we implemented the `From` trait, we can just use into() to create an element,
                            // just as if we were using a widget
                            .into()
                    })
                    .collect();

                iced::widget::column![
                    iced::widget::button("New").on_press(Message::OpenNewJokeComponent),
                    iced::widget::Column::from_vec(items)
                ]
                // Some spacing goes a long way to make your UI more visually appealing
                .spacing(10)
                .into()
            }
            // If the view is an edit view, call the view method of the edit view
            // and map the returned message to the higher level message
            View::NewJoke(new_joke) => new_joke.view().map(Message::NewJoke),
        }
    }
}

Now that we have a nice way to specify out view, let's add it to the apps state:

mod list_item;
mod new_joke;

fn main() {
    iced::run("Project Structure Example", App::update, App::view).unwrap();
}

#[derive(Debug, Clone)]
enum Message {
    // This message is used to handle the new views message
    NewJoke(new_joke::Message),
    OpenNewJokeComponent,
    Delete(usize),
}

#[derive(Default)]
enum View {
    #[default]
    ListJokes,
    // This holds our new joke components state
    NewJoke(new_joke::NewJoke),
}

#[derive(Default)]
struct App {
    view: View,
    items: Vec<String>,
}

impl App {
    fn update(&mut self, message: Message) -> iced::Task<Message> {
        match message {
            Message::NewJoke(view_message) => {
                // as with all enums in rust, we'll need to use an if-let expression
                // to get access to our component from the `View` enum
                if let View::NewJoke(edit) = &mut self.view {
                    // Call the update method of the edit view
                    // and handle the returned action
                    match edit.update(view_message) {
                        // The none action is a no-op
                        new_joke::Action::None => {}

                        // If the action is a task, we'll need to map it, to ensure it returns the right Message type.
                        // This is the exact same as with `view` and the returned `iced::Element`
                        new_joke::Action::Run(task) => return task.map(Message::NewJoke),

                        // If the action is a cancel, switch back to the list view
                        new_joke::Action::Cancel => {
                            self.view = View::ListJokes;
                        }

                        // If the action is a submit, add the new joke before returning to the list view
                        new_joke::Action::Submit(new_joke_content) => {
                            self.view = View::ListJokes;
                            self.items.push(new_joke_content);
                        }
                    }
                }
            }
            Message::OpenNewJokeComponent => {
                // Create a new component
                let component = new_joke::NewJoke::new();
                self.view = View::NewJoke(component);
            }
            Message::Delete(index) => {
                self.items.remove(index);
            }
        }
        iced::Task::none()
    }

    fn view(&self) -> iced::Element<Message> {
        match &self.view {
            View::ListJokes => {
                let items = self
                    .items
                    .iter()
                    // since we want deletion, we'll need the index of each item, so we know which one to delete
                    .enumerate()
                    .map(|(index, item)| {
                        // create a listitem for each joke
                        list_item::ListItem::new(iced::widget::text(item))
                            // save the index to delete in the message
                            .on_delete(Message::Delete(index))
                            // since we implemented the `From` trait, we can just use into() to create an element,
                            // just as if we were using a widget
                            .into()
                    })
                    .collect();

                iced::widget::column![
                    iced::widget::button("New").on_press(Message::OpenNewJokeComponent),
                    iced::widget::Column::from_vec(items)
                ]
                // Some spacing goes a long way to make your UI more visually appealing
                .spacing(10)
                .into()
            }
            // If the view is an edit view, call the view method of the edit view
            // and map the returned message to the higher level message
            View::NewJoke(new_joke) => new_joke.view().map(Message::NewJoke),
        }
    }
}

Before we can enjoy our beautiful component, we'll need to actually create the component's state somewhere. In our case, we want to show it, after the user clicks an "Add Joke" button.

For that we'll just add a button to the app's view method and edit the app's update function to include this:

impl App {
    fn update(&mut self, message: Message) -> iced::Task<Message> {
        match message {
            Message::OpenNewJokeComponent => {
                // Create a new component
                let component = new_joke::NewJoke::new();
                self.view = View::NewJoke(component);
            }
            // ...
        }
    }
}

View & Mapping

Following the trend of mirroring what an iced application does, you'll also want to implement a view function.


#[derive(Debug, Clone)]
pub enum Message {
    ChangeContent(String),
    RandomJoke,
    Submit,
    Cancel,
}

pub enum Action {
    // The user was happy with the joke and wants to submit it to the list
    Submit(String),
    // The user wants to cancel adding a new joke
    Cancel,
    // The components needs to run a task
    Run(iced::Task<Message>),
    // The component does not require any additional actions
    None,
}

pub struct NewJoke {
    joke: String,
}

impl NewJoke {
    pub fn new() -> Self {
        Self {
            joke: String::new(),
        }
    }
}

impl NewJoke {
    pub fn view(&self) -> iced::Element<Message> {
        iced::widget::column![
            iced::widget::text_input("Content", &self.joke)
                // on_input expects a closure, which would usually look like this:
                // |new_value| Message::ChangeContent(new_value)
                // Thankfully, you can just use the enum variants name directly
                .on_input(Message::ChangeContent),
            iced::widget::button("Random Joke").on_press(Message::RandomJoke),
            iced::widget::row![
                iced::widget::button("Cancel").on_press(Message::Cancel),
                iced::widget::button("Submit").on_press(Message::Submit)
            ]
            .spacing(10),
        ]
        .padding(10)
        .spacing(10)
        .into()
    }
}

impl NewJoke {
    #[must_use]
    pub fn update(&mut self, message: Message) -> Action {
        match message {
            Message::Submit => Action::Submit(self.joke.clone()),
            Message::Cancel => Action::Cancel,
            Message::ChangeContent(content) => {
                self.joke = content;
                Action::None
            }
            Message::RandomJoke => Action::Run(Self::random_joke_task()),
        }
    }

    fn random_joke_task() -> iced::Task<Message> {
        iced::Task::future(async {
            // Fetch a joke from the internet
            let client = reqwest::Client::new();
            let response: serde_json::Value = client
                .get("https://icanhazdadjoke.com")
                .header("Accept", "application/json")
                .send()
                .await
                .unwrap()
                .json()
                .await
                .unwrap();

            // Parse the response
            let joke = response["joke"].as_str().unwrap();

            // Return the joke as a message
            Message::ChangeContent(joke.to_owned())
        })
    }
}

Now we'll obviously want to use this view as part of our main view. But our main view expects a return value of iced::Element<crate::Message>, while our view returns iced::Element<new_joke__message>. Thankfully, iced allows us to map them.

First we'll need to add an message variant to our main Message.

// Main Application Message
pub enum Message
{
    LoginForm(new_joke::Message),
    // ...
}

After that we can actually call the view method of our new component.

To map our Message, we can simply use iced::Element<component::Message>.map(crate::Message::Component).

Note: this is a shortcut for iced::Element<component::Message>.map(|component_message| crate::Message::Component(component_message))

impl App {
    fn view(&self) -> iced::Element<Message> {
        match &self.view {
            View::NewJoke(new_joke) => new_joke.view().map(Message::NewJoke),
            // ...
        }
    }
}

The .map function takes a closure that takes the message and converts it into another message. This often requires having a dedicated message variant on the application level that contains the view message.

Map functions like this are available on Task::map, Subscription::map, Element::map.

Update & Action

As already hinted in the beginning, the update function of a component does have a significant change compared to a normal iced application.

Instead of return an iced::Task, we return an Action. An Action allows us to communicate with the parent of our component. In that regard, they are similar to events from other UI frameworks.

Some applications, like Halloy actually do call this type Event instead of Action.

First we'll start by defining our component's action type:


#[derive(Debug, Clone)]
pub enum Message {
    ChangeContent(String),
    RandomJoke,
    Submit,
    Cancel,
}

pub enum Action {
    // The user was happy with the joke and wants to submit it to the list
    Submit(String),
    // The user wants to cancel adding a new joke
    Cancel,
    // The components needs to run a task
    Run(iced::Task<Message>),
    // The component does not require any additional actions
    None,
}

pub struct NewJoke {
    joke: String,
}

impl NewJoke {
    pub fn new() -> Self {
        Self {
            joke: String::new(),
        }
    }
}

impl NewJoke {
    pub fn view(&self) -> iced::Element<Message> {
        iced::widget::column![
            iced::widget::text_input("Content", &self.joke)
                // on_input expects a closure, which would usually look like this:
                // |new_value| Message::ChangeContent(new_value)
                // Thankfully, you can just use the enum variants name directly
                .on_input(Message::ChangeContent),
            iced::widget::button("Random Joke").on_press(Message::RandomJoke),
            iced::widget::row![
                iced::widget::button("Cancel").on_press(Message::Cancel),
                iced::widget::button("Submit").on_press(Message::Submit)
            ]
            .spacing(10),
        ]
        .padding(10)
        .spacing(10)
        .into()
    }
}

impl NewJoke {
    #[must_use]
    pub fn update(&mut self, message: Message) -> Action {
        match message {
            Message::Submit => Action::Submit(self.joke.clone()),
            Message::Cancel => Action::Cancel,
            Message::ChangeContent(content) => {
                self.joke = content;
                Action::None
            }
            Message::RandomJoke => Action::Run(Self::random_joke_task()),
        }
    }

    fn random_joke_task() -> iced::Task<Message> {
        iced::Task::future(async {
            // Fetch a joke from the internet
            let client = reqwest::Client::new();
            let response: serde_json::Value = client
                .get("https://icanhazdadjoke.com")
                .header("Accept", "application/json")
                .send()
                .await
                .unwrap()
                .json()
                .await
                .unwrap();

            // Parse the response
            let joke = response["joke"].as_str().unwrap();

            // Return the joke as a message
            Message::ChangeContent(joke.to_owned())
        })
    }
}

With our action ready, we can add our update function. To make the update method easier to read, we handle task creation in a seperate function.


#[derive(Debug, Clone)]
pub enum Message {
    ChangeContent(String),
    RandomJoke,
    Submit,
    Cancel,
}

pub enum Action {
    // The user was happy with the joke and wants to submit it to the list
    Submit(String),
    // The user wants to cancel adding a new joke
    Cancel,
    // The components needs to run a task
    Run(iced::Task<Message>),
    // The component does not require any additional actions
    None,
}

pub struct NewJoke {
    joke: String,
}

impl NewJoke {
    pub fn new() -> Self {
        Self {
            joke: String::new(),
        }
    }
}

impl NewJoke {
    pub fn view(&self) -> iced::Element<Message> {
        iced::widget::column![
            iced::widget::text_input("Content", &self.joke)
                // on_input expects a closure, which would usually look like this:
                // |new_value| Message::ChangeContent(new_value)
                // Thankfully, you can just use the enum variants name directly
                .on_input(Message::ChangeContent),
            iced::widget::button("Random Joke").on_press(Message::RandomJoke),
            iced::widget::row![
                iced::widget::button("Cancel").on_press(Message::Cancel),
                iced::widget::button("Submit").on_press(Message::Submit)
            ]
            .spacing(10),
        ]
        .padding(10)
        .spacing(10)
        .into()
    }
}

impl NewJoke {
    #[must_use]
    pub fn update(&mut self, message: Message) -> Action {
        match message {
            Message::Submit => Action::Submit(self.joke.clone()),
            Message::Cancel => Action::Cancel,
            Message::ChangeContent(content) => {
                self.joke = content;
                Action::None
            }
            Message::RandomJoke => Action::Run(Self::random_joke_task()),
        }
    }

    fn random_joke_task() -> iced::Task<Message> {
        iced::Task::future(async {
            // Fetch a joke from the internet
            let client = reqwest::Client::new();
            let response: serde_json::Value = client
                .get("https://icanhazdadjoke.com")
                .header("Accept", "application/json")
                .send()
                .await
                .unwrap()
                .json()
                .await
                .unwrap();

            // Parse the response
            let joke = response["joke"].as_str().unwrap();

            // Return the joke as a message
            Message::ChangeContent(joke.to_owned())
        })
    }
}

As with the view before, we'll now need to call our component from the apps main update function. Our component's update function now returns an Action which we'll want to react to.

Note: As with our view before, we'll have to map the task, should one be returned.

impl App {
    fn update(&mut self, message: Message) -> iced::Task<Message> {
        match message {
            Message::NewJoke(view_message) => {
                // as with all enums in rust, we'll need to use an if-let expression
                // to get access to our component from the `View` enum
                if let View::NewJoke(edit) = &mut self.view {
                    // Call the update method of the edit view
                    // and handle the returned action
                    match edit.update(view_message) {
                        // The none action is a no-op
                        new_joke::Action::None => {}

                        // If the action is a task, we'll need to map it, to ensure it returns the right Message type.
                        // This is the exact same as with `view` and the returned `iced::Element`
                        new_joke::Action::Run(task) => return task.map(Message::NewJoke),

                        // If the action is a cancel, switch back to the list view
                        new_joke::Action::Cancel => {
                            self.view = View::ListJokes;
                        }

                        // If the action is a submit, add the new joke before returning to the list view
                        new_joke::Action::Submit(new_joke_content) => {
                            self.view = View::ListJokes;
                            self.items.push(new_joke_content);
                        }
                    }
                }
            }
            // ...
        }
    }
}

After hooking up the view and update functions, we're done.

Conclusion

The component pattern is the default way to divide your state and update logic into smaller pieces. Keep in mind, that default doesn't neccesarily mean, it's the right solution for you.

This pattern is great, because it structured just like your iced application and encompasses everything you need for that part of the application.

It does however introduce a lot of builderplate code, which isn't always warranted.

If you don't need the internal state and update logic, you might instead be more interested in the Viewable Pattern.