Search code examples
rustyew

In Rust Yew, is there a way to use use_reducer with mutable actions?


Here is a simplified version of what I'm trying to do:

pub struct EntryManager {
    pub entries: Vec<String>,
}

impl EntryManager {
  pub fn add_entry(&mut self, text: String) {
    self.entries.push(text);
  }
}

pub enum EntryManagerAction {
    Add(String),
}

impl Reducible for EntryManager {
    type Action = EntryManagerAction;

    fn reduce(self: Rc<Self>, action: Self::Action) -> Rc<Self> {
        match action {

            // this works
            EntryManagerAction::Add(text) => {
                let mut manager = (*self).clone();
                manager.add_entry(text);
                manager.into()
            },

            // this doesn't (self is immutable, doesn't return Self)
            EntryManagerAction::Add(text) => self.add_entry(text),
        }
    }
}

In my case, this EntryManager would have many more properties and cloning at every action seems very expensive. It would be very convenient to have a mutable access to EntryManager within reduce. Is there another solution or suggestions?


Solution

  • I don't know yew then I don't know if the self: Rc<Self> argument of reduce() is supposed to be the only reference to the EntryManager at the moment this function is called.
    If this is the case, but I really doubt it is, then the V1 variant in the example below could suffice.
    It relies on Rc::get_mut() which checks if there is no other reference to the inner data before allowing the mutation.
    But if it is actually shared, the reduction has no effect!

    On the other hand, if an EntryManager is fundamentally shared in this reduction, and I guess it is, because of Rc, then the only solution is interior mutability (i.e. enable mutation even on shared states, at the cost of a minimal runtime check).
    In the V2 variant below, the vector of strings is wrapped inside a RefCell.
    The .borrow_mut() access performs a cheap runtime check in order to be sure there is no other borrow (exclusive or shared) at the same time (from an upstream operation accessing this vector while triggering reduce() for example).
    Then, the most noticeable difference with V1 is that now add_entry() of V2 does not require &mut self anymore (only &self), which eases dealing with the implicit shared situation implied by Rc.

    use std::{cell::RefCell, rc::Rc};
    
    enum EntryManagerAction {
        Add(String),
    }
    
    #[derive(Debug)]
    struct EntryManagerV1 {
        entries: Vec<String>,
    }
    
    impl EntryManagerV1 {
        fn add_entry(
            &mut self,
            text: String,
        ) {
            self.entries.push(text);
        }
    
        fn simulate_reduce(
            mut self: Rc<Self>,
            action: EntryManagerAction,
        ) -> Rc<Self> {
            match action {
                EntryManagerAction::Add(text) => {
                    if let Some(em) = Rc::get_mut(&mut self) {
                        em.add_entry(text);
                    } else {
                        println!("!!! EntryManager is shared !!!")
                    }
                    self
                }
            }
        }
    }
    
    #[derive(Debug)]
    struct EntryManagerV2 {
        entries: RefCell<Vec<String>>,
    }
    
    impl EntryManagerV2 {
        fn add_entry(
            &self, // no &mut here, thanks to RefCell
            text: String,
        ) {
            self.entries.borrow_mut().push(text);
        }
    
        fn simulate_reduce(
            self: Rc<Self>,
            action: EntryManagerAction,
        ) -> Rc<Self> {
            match action {
                EntryManagerAction::Add(text) => {
                    self.add_entry(text);
                    self
                }
            }
        }
    }
    
    fn main() {
        {
            println!("~~~~ V1 ~~~~");
            let em = Rc::new(EntryManagerV1 {
                entries: Vec::new(),
            });
            let em =
                em.simulate_reduce(EntryManagerAction::Add("AAA".to_owned()));
            println!("first add: {:?}", em);
            let em2 = Rc::clone(&em); // shared between two references now!
            let em2 =
                em2.simulate_reduce(EntryManagerAction::Add("BBB".to_owned()));
            println!("second add: {:?}", em2);
        }
        {
            println!("~~~~ V2 ~~~~");
            let em = Rc::new(EntryManagerV2 {
                entries: RefCell::new(Vec::new()),
            });
            let em =
                em.simulate_reduce(EntryManagerAction::Add("AAA".to_owned()));
            println!("first add: {:?}", em);
            let em2 = Rc::clone(&em); // shared between two references now!
            let em2 =
                em2.simulate_reduce(EntryManagerAction::Add("BBB".to_owned()));
            println!("second add: {:?}", em2);
        }
    }
    /*
    ~~~~ V1 ~~~~
    first add: EntryManagerV1 { entries: ["AAA"] }
    !!! EntryManager is shared !!!
    second add: EntryManagerV1 { entries: ["AAA"] }
    ~~~~ V2 ~~~~
    first add: EntryManagerV2 { entries: RefCell { value: ["AAA"] } }
    second add: EntryManagerV2 { entries: RefCell { value: ["AAA", "BBB"] } }
    */