Navicula: A composable architecture for Rust-Dioxus

Published: 2023-07-07 10:30:30

This is a simplified implementation of the SwiftUI Composable Architecture (TCA) for the Rust Dioxus Library.

Navicula: A composable architecture for Rust-Dioxus

This is a simplified implementation of the SwiftUI Composable Architecture (TCA) for the Rust Dioxus Library. There're many similarities to the Elm architecture as well as various React Redux models. This is currently being used for the Ebou Mastodon App. It is an early alpha.

Here's example code showcasing a view / reducer for editing a chat message

pub struct EditReducer;

#[derive(Default)]
pub struct EditState {
    pub message: Option<Message>,
}

#[derive(Clone)]
pub enum ChildMessage {}

#[derive(Clone)]
pub enum DelegateMessage {
    Done,
}

#[derive(Clone, Debug)]
pub enum EditAction {
    Initial,
    ReceivedMessage(Option<Message>),
    Done,
}

impl Reducer for EditReducer {
    type Message = ChildMessage;
    type DelegateMessage = DelegateMessage;
    type Action = EditAction;
    type State = EditState;
    type Environment = crate::model::Environment;

    fn reduce<'a, 'b>(
        context: &'a impl MessageContext<Self::Action, Self::DelegateMessage, Self::Message>,
        action: Self::Action,
        state: &'a mut Self::State,
        environment: &'a Self::Environment,
    ) -> Effect<'b, Self::Action> {
        match action {
            EditAction::Initial => {
                return environment
                    .selected
                    .subscribe("selected", context, |message| {
                        let m = message.clone();
                        EditAction::ReceivedMessage(m)
                    });
            }
            EditAction::ReceivedMessage(message) => {
                state.message = message;
            }
            EditAction::Done => context.send_parent(DelegateMessage::Done),
        }
        Effect::NONE
    }

    fn initial_action() -> Option<Self::Action> {
        Some(EditAction::Initial)
    }
}

impl ChildReducer<ChatChildReducer> for EditReducer {
    fn to_child(
        _message: <ChatChildReducer as Reducer>::Message,
    ) -> Option<<Self as Reducer>::Action> {
        None
    }

    fn from_child(
        message: <Self as Reducer>::DelegateMessage,
    ) -> Option<<ChatChildReducer as Reducer>::Action> {
        match message {
            DelegateMessage::Done => Some(<ChatChildReducer as Reducer>::Action::FinishEdit),
        }
    }
}

#[inline_props]
pub fn root<'a>(cx: Scope<'a>, store: ViewStore<'a, EditReducer>) -> Element<'a> {
    let Some(message) = store.message.as_ref() else {
        return render!(div{})
    };
    let content = match message {
        Message::Received(ref m) => m,
        Message::Send(ref m) => m,
    };
    render! {
        div {
            display: "flex",
            flex_direction: "column",
            "Edit: "
            a {
                onclick: move |_| store.send(EditAction::Done),
                "CLOSE"
            }
            textarea {
                onchange: move |v| println!("{}", v.value),
                "{content}"
            }
        }
    }
}