DioxusLabs / dioxus

Fullstack app framework for web, desktop, mobile, and more.
https://dioxuslabs.com
Apache License 2.0
19.58k stars 759 forks source link

Implement Struct-Based Components in `rsx` Macro #2600

Closed mrgzi closed 6 days ago

mrgzi commented 3 weeks ago

Feature Request

I would like to propose a feature where we can use structs directly as components within the rsx! macro in Dioxus. This feature would let us group related components and define both the data and the rendering logic in the same struct.

Let’s look at this code:

#[component]
pub fn Alert(#[props(optional)] class: String, #[props(optional)] children: Element) -> Element {
    rsx! {
        div {
            class: "bottom-4 right-4 flex items-center p-4 text-sm border rounded-lg bg-gray-800",
            class: class,
            {children}
        }
    }
}

#[component]
pub fn InfoAlert(#[props(optional)] classes: String, #[props(optional)] children: Element) -> Element {
    rsx! {
        Alert {
            class: "text-blue-400 border-blue-800",
            div {
                span { class: "font-medium", "Info! " }
                {children}
            }
        }
    }
}

#[component]
pub fn WarningAlert(#[props(optional)] class: String, #[props(optional)] children: Element) -> Element {
    rsx! {
        Alert {
            class: "text-yellow-300 border-yellow-800",
            div {
                span { class: "font-medium", "Warning! " }
                {children}
            }
        }
    }
}

These components are related to each other. With the Struct-Based Component feature, we could group these functions.

struct Alert {}

impl Alert {
    #[component]
    pub fn Base(#[props(optional)] class: String, #[props(optional)] children: Element) -> Element {
        rsx! {
            div {
                class: "bottom-4 right-4 flex items-center p-4 text-sm border rounded-lg bg-gray-800",
                class: class,
                {children}
            }
        }
    }

    #[component]
    pub fn Info(#[props(optional)] classes: String, #[props(optional)] children: Element) -> Element {
        rsx! {
            Base {
                class: "text-blue-400 border-blue-800",
                div {
                    span { class: "font-medium", "Info! " }
                    {children}
                }
            }
        }
    }

    #[component]
    pub fn Warning(#[props(optional)] class: String, #[props(optional)] children: Element) -> Element {
        rsx! {
            Base {
                class: "text-yellow-300 border-yellow-800",
                div {
                    span { class: "font-medium", "Warning! " }
                    {children}
                }
            }
        }
    }
}

And usage in the rsx! macro:

pub fn my_comp() -> Element {
    rsx! {
        Alert::Info{}
    }
}

This new approach would simplify how we organize and manage our code, making it cleaner and easier to maintain.

ealmloff commented 3 weeks ago

You can use a module to group components together under a namespace:

#[allow(non_snake_case)]
mod Alert {
    #[component]
    pub fn Base(#[props(optional)] class: String, #[props(optional)] children: Element) -> Element {
        rsx! {
            div {
                class: "bottom-4 right-4 flex items-center p-4 text-sm border rounded-lg bg-gray-800",
                class: class,
                {children}
            }
        }
    }

    #[component]
    pub fn Info(#[props(optional)] classes: String, #[props(optional)] children: Element) -> Element {
        rsx! {
            Base {
                class: "text-blue-400 border-blue-800",
                div {
                    span { class: "font-medium", "Info! " }
                    {children}
                }
            }
        }
    }

    #[component]
    pub fn Warning(#[props(optional)] class: String, #[props(optional)] children: Element) -> Element {
        rsx! {
            Base {
                class: "text-yellow-300 border-yellow-800",
                div {
                    span { class: "font-medium", "Warning! " }
                    {children}
                }
            }
        }
    }
}

If you have a lot of shared props, you can use #[derive(Props)] with one struct and use that struct in many components:

#[allow(non_snake_case)]
mod Alert {
    #[derive(Props)]
    struct AlertProps {
        #[props(optional)] class: String,
        #[props(optional)] children: Element
    }

    #[component]
    pub fn Base(
        props: AlertProps,
    ) -> Element {
        rsx! {
            div {
                class: "bottom-4 right-4 flex items-center p-4 text-sm border rounded-lg bg-gray-800",
                class: props.class,
                {props.children}
            }
        }
    }

    #[component]
    pub fn Info(props: AlertProps) -> Element {
        rsx! {
            Base {
                class: "text-blue-400 border-blue-800",
                div {
                    span { class: "font-medium", "Info! " }
                    {props.children}
                }
            }
        }
    }

    #[component]
    pub fn Warning(props: AlertProps) -> Element {
        rsx! {
            Base {
                class: "text-yellow-300 border-yellow-800",
                div {
                    span { class: "font-medium", "Warning! " }
                    {props.children}
                }
            }
        }
    }
}

Could you elaborate on how struct based components would improve the existing solutions?

mrgzi commented 3 weeks ago

@ealmloff, your approach is indeed a good solution. However, if we want to prepare our struct in advance and use it elsewhere, I can imagine scenarios, especially complex ones, where this approach might be the better solution. It also allows us to develop the app with an OOP approach. There could even be cases where using generics would be beneficial.

jkelleyrtp commented 6 days ago

This isn't something we want to add today but if it becomes useful maybe we'll reconsider it. It's hard to support lots of variants of app architectures while also shipping other important things, so not planned as of today.

mrgzi commented 6 days ago

but if it becomes useful maybe we'll reconsider it.

Can you explain in more detail? How will we understand that if it will become useful?

mrgzi commented 6 days ago

I think I can provide an example. Let's assume we want to initialize a component, but there are three different types of components that can be initialized based on the data. If we follow this implementation, it would be easier because MyStruct::Component{} can return various components. We prepare the data beforehand, and the Component method examines this data to return different components accordingly. Currently, we must prepare the struct and pass it as parameters to the component function. However, if we want to initialize the component by sending the struct as a parameter, we must either manage all views in the same function or make comparisons in the parent rsx macro, which decreases code readability. This implementation proposes writing Dioxus in a more abstracted and readable manner.

thorn132 commented 6 days ago

different types of components that can be initialized based on the data. If we follow this implementation, it would be easier because MyStruct::Component{} can return various components

Function components can also return various components, e.g. by dispatching with a match statement on the data that determines which one to initialize:

mod Alert {
    #[derive(Props)]
    struct AlertProps {
        #[props(optional)] kind: String,
        #[props(optional)] msg: String,
    }

    #[component]
    fn Generic(props: AlertProps) -> Element {
        match props.kind {
            "error" => rsx! { Error { ..props } },
            "warning" => rsx! { Warning { ..props } },
            _ => unreachable!()
        }
    }

    #[component]
    fn Error(props: AlertProps) -> Element {
        rsx! { p { "error {props.msg}" } }
    }

    #[component]
    fn Warning(props: AlertProps) -> Element {
        rsx! { p { "warning {props.msg}" } }
    }
}

fn App() -> Element {
    rsx! { 
        Alert::Generic { kind: "error", msg: "friends don't let friends use OOP" },
        Alert::Error { msg: "react deprecated class components years ago" }
        Alert::Warning { msg: "also I don't see how your idea resolves any of your complaints" }
    }
}

You can imagine variations of this that use enum structs for the alert type/values if they have different ones, or some overly-complicated Rc<dyn Alertable> technique to display arbitrary values in an alert.

thorn132 commented 6 days ago

Another idea:

impl Into<Element> for MyStruct {
    fn into(self) -> Element {
        rsx! {
            ...
        }
    }
}