Skip to content

Commit

Permalink
copy redux state mgmt from softw-eng
Browse files Browse the repository at this point in the history
  • Loading branch information
senekor committed Nov 12, 2023
1 parent 6c916fc commit 58e0473
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 0 deletions.
10 changes: 10 additions & 0 deletions app/src/state/action.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use models::TemperatureMeasurement;

use super::State;

#[derive(Debug)]
pub enum Action {
Overwrite(State),
Insert(TemperatureMeasurement),
Clear,
}
48 changes: 48 additions & 0 deletions app/src/state/middleware.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//! In the redux pattern, reducers must be pure functions. This means that they
//! cannot perform side effects. This has several benefits, but it means that we
//! need to do some extra work to perform side effects. This is where middleware
//! comes in. Instead of dispatching an action directly, we process it by a
//! middleware first. The middleware can then perform side effects and dispatch
//! a new action. This new action is then passed to the reducer. Pre- and
//! post-middleware actions have different types, so there is no risk of
//! forgetting to run a middleware outside of the store module.
use gloo::net::http::Request;
use models::TemperatureMeasurement;

use super::{action::Action, State};

pub enum PreMiddlewareAction {
Reload,
InsertRandom,
DeleteAll,
}

pub async fn process(action: PreMiddlewareAction) -> Action {
match action {
PreMiddlewareAction::Reload => {
let state = Request::get("/api/measurements")
.send()
.await
.unwrap()
.json::<State>()
.await
.unwrap();
Action::Overwrite(state)
}
PreMiddlewareAction::InsertRandom => {
let measurement = Request::post("/api/measurements/random")
.send()
.await
.unwrap()
.json::<TemperatureMeasurement>()
.await
.unwrap();
Action::Insert(measurement)
}
PreMiddlewareAction::DeleteAll => {
Request::delete("/api/measurements").send().await.unwrap();
Action::Clear
}
}
}
16 changes: 16 additions & 0 deletions app/src/state/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use models::MeasurementList;

mod action;
mod middleware;
mod reducer;
mod store;

use self::middleware::PreMiddlewareAction;
pub use self::store::{provide_store, use_store};

/// The type of data managed by the store.
type State = MeasurementList;

// Users of the store only need to know about the pre-middleware actions.
// The fact that middleware is run is an implementation detail.
pub type Action = PreMiddlewareAction;
13 changes: 13 additions & 0 deletions app/src/state/reducer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
use super::{action::Action, State};

pub fn reduce(state: &State, action: Action) -> State {
match action {
Action::Overwrite(state) => state,
Action::Insert(measurement) => {
let mut new_state = state.clone();
new_state.insert(measurement);
new_state
}
Action::Clear => State::default(),
}
}
94 changes: 94 additions & 0 deletions app/src/state/store.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
use leptos::*;
use leptos_use::{use_websocket, UseWebsocketReturn};
use models::Notification;

use super::{
action::Action,
middleware::{self, PreMiddlewareAction},
reducer::reduce,
State,
};

#[derive(Debug, Clone, Copy)]
pub struct Store {
state: RwSignal<State>,
}

/// Basically a wrapper around [leptos::RwSignal]. The only difference is that
/// updates have to go through the `dispatch` method.
impl Store {
fn new() -> Self {
let store = Self {
state: RwSignal::new(Default::default()),
};
// Load initial state
store.dispatch(PreMiddlewareAction::Reload);

let UseWebsocketReturn { message, .. } = use_websocket("/api/notifications");
create_effect(move |_| {
if let Some(message) = message.get() {
let notification = serde_json::from_str::<Notification>(&message).unwrap();
let action = match notification {
Notification::New(measurements) => Action::Insert(measurements),
Notification::Cleared => Action::Clear,
};
store.dispatch_without_middleware(action);
}
});

store
}

fn dispatch_without_middleware(&self, action: Action) {
let new_state = reduce(&self.state.get_untracked(), action);
if new_state != self.state.get_untracked() {
self.state.set(new_state);
}
}

pub fn dispatch(self, action: PreMiddlewareAction) {
spawn_local(async move {
let action = middleware::process(action).await;
self.dispatch_without_middleware(action);
});
}
}

pub fn provide_store() {
provide_context(Store::new());
}

pub fn use_store() -> Store {
use_context().expect("should find store context")
}

/// These impls are a little gnarly looking.
/// They are not necessary, but they make the store callable.
/// Essentially, it turns the [Store] into a custom closure type.
/// This is what leptos does for its own signal types, so I did it here too.
/// This is a nightly feature, activated by `#![feature(fn_traits)]`.
mod impl_fn_for_store {
use leptos::SignalGet;

use super::{State, Store};

impl FnOnce<()> for Store {
type Output = State;

extern "rust-call" fn call_once(self, _: ()) -> Self::Output {
self.state.get()
}
}

impl FnMut<()> for Store {
extern "rust-call" fn call_mut(&mut self, _: ()) -> Self::Output {
self.state.get()
}
}

impl Fn<()> for Store {
extern "rust-call" fn call(&self, _: ()) -> Self::Output {
self.state.get()
}
}
}

0 comments on commit 58e0473

Please sign in to comment.