Make State Management ssr safe

This commit is contained in:
Jeremy Wall 2022-08-15 16:58:35 -04:00
parent c9bcfe220e
commit a83b60e016
3 changed files with 63 additions and 33 deletions

View File

@ -21,23 +21,26 @@ pub struct TabState<G: GenericNode> {
#[component(TabbedView<G>)] #[component(TabbedView<G>)]
pub fn tabbed_view(state: TabState<G>) -> View<G> { pub fn tabbed_view(state: TabState<G>) -> View<G> {
cloned!((state) => view! { cloned!((state) => view! {
header(class="no-print") { // NOTE(jwall): this needs to be a single node or the Router freaks out.
nav { div {
ul { header(class="no-print") {
li { a(href="/ui/plan", class="no-print") { "Plan" } " > " nav {
ul {
li { a(href="/ui/plan", class="no-print") { "Plan" } " > "
}
li { a(href="/ui/inventory", class="no-print") { "Inventory" } " > "
}
li { a(href="/ui/cook", class="no-print") { "Cook" }
}
} }
li { a(href="/ui/inventory", class="no-print") { "Inventory" } " > " ul {
li { a(href="https://github.com/zaphar/kitchen") { "Github" } }
} }
li { a(href="/ui/cook", class="no-print") { "Cook" }
}
}
ul {
li { a(href="https://github.com/zaphar/kitchen") { "Github" } }
} }
} }
} main(class=".conatiner-fluid") {
main(class=".conatiner-fluid") { (state.inner)
(state.inner) }
} }
}) })
} }

View File

@ -13,9 +13,15 @@
// limitations under the License. // limitations under the License.
use std::collections::{BTreeMap, BTreeSet}; use std::collections::{BTreeMap, BTreeSet};
use serde_json::{from_str, to_string}; #[cfg(target_arch = "wasm32")]
use serde_json::from_str;
#[cfg(target_arch = "wasm32")]
use serde_json::to_string;
use sycamore::{context::use_context, prelude::*}; use sycamore::{context::use_context, prelude::*};
use tracing::{debug, error, info, instrument, warn}; use tracing::{debug, instrument, warn};
#[cfg(target_arch = "wasm32")]
use tracing::{error, info};
#[cfg(target_arch = "wasm32")]
use web_sys::{window, Storage}; use web_sys::{window, Storage};
use recipe_store::*; use recipe_store::*;
@ -56,6 +62,7 @@ where
} }
} }
#[cfg(target_arch = "wasm32")]
fn get_storage(&self) -> Result<Option<Storage>, String> { fn get_storage(&self) -> Result<Option<Storage>, String> {
window() window()
.unwrap() .unwrap()
@ -63,9 +70,19 @@ where
.map_err(|e| format!("{:?}", e)) .map_err(|e| format!("{:?}", e))
} }
#[cfg(not(target_arch = "wasm32"))]
#[instrument(skip(self))] #[instrument(skip(self))]
async fn synchronize(&self) -> Result<(), String> { pub async fn init(&self, force: bool) -> Result<(), String> {
// TODO(jwall): Allow this to work for the ssr case.
todo!()
}
#[cfg(target_arch = "wasm32")]
#[instrument(skip(self))]
pub async fn init(&self, force: bool) -> Result<(), String> {
info!("Synchronizing Recipes"); info!("Synchronizing Recipes");
// TODO(jwall): Check to see if we already have them in storage
// only fetch via http if force is true.
let storage = self.get_storage()?.unwrap(); let storage = self.get_storage()?.unwrap();
let recipes = self let recipes = self
.store .store
@ -96,10 +113,9 @@ where
Ok(()) Ok(())
} }
#[cfg(target_arch = "wasm32")]
#[instrument(skip(self))] #[instrument(skip(self))]
pub fn fetch_categories_from_storage( fn fetch_categories_from_storage(&self) -> Result<Option<BTreeMap<String, String>>, String> {
&self,
) -> Result<Option<BTreeMap<String, String>>, String> {
let storage = self.get_storage()?.unwrap(); let storage = self.get_storage()?.unwrap();
match storage match storage
.get_item("categories") .get_item("categories")
@ -119,8 +135,9 @@ where
} }
} }
#[cfg(target_arch = "wasm32")]
#[instrument(skip(self))] #[instrument(skip(self))]
pub fn fetch_recipes_from_storage( fn fetch_recipes_from_storage(
&self, &self,
) -> Result<(Option<Recipe>, Option<BTreeMap<String, Recipe>>), String> { ) -> Result<(Option<Recipe>, Option<BTreeMap<String, Recipe>>), String> {
let storage = self.get_storage()?.unwrap(); let storage = self.get_storage()?.unwrap();
@ -153,19 +170,32 @@ where
} }
} }
#[cfg(target_arch = "wasm32")]
async fn fetch_recipes( async fn fetch_recipes(
&self, &self,
) -> Result<(Option<Recipe>, Option<BTreeMap<String, Recipe>>), String> { ) -> Result<(Option<Recipe>, Option<BTreeMap<String, Recipe>>), String> {
Ok(self.fetch_recipes_from_storage()?) Ok(self.fetch_recipes_from_storage()?)
} }
#[cfg(not(target_arch = "wasm32"))]
async fn fetch_recipes(
&self,
) -> Result<(Option<Recipe>, Option<BTreeMap<String, Recipe>>), String> {
Ok((None, None))
}
#[cfg(target_arch = "wasm32")]
async fn fetch_categories(&self) -> Result<Option<BTreeMap<String, String>>, String> { async fn fetch_categories(&self) -> Result<Option<BTreeMap<String, String>>, String> {
Ok(self.fetch_categories_from_storage()?) Ok(self.fetch_categories_from_storage()?)
} }
#[cfg(not(target_arch = "wasm32"))]
async fn fetch_categories(&self) -> Result<Option<BTreeMap<String, String>>, String> {
Ok(None)
}
// FIXME(jwall): Stays public
#[instrument(skip(self))] #[instrument(skip(self))]
pub async fn refresh(&mut self) -> Result<(), String> { pub async fn refresh(&mut self) -> Result<(), String> {
self.synchronize().await?; self.init(true).await?;
debug!("refreshing recipes"); debug!("refreshing recipes");
if let (staples, Some(r)) = self.fetch_recipes().await? { if let (staples, Some(r)) = self.fetch_recipes().await? {
self.set_recipes(r); self.set_recipes(r);
@ -182,6 +212,7 @@ where
self.recipes.get().get(idx).map(|r| r.clone()) self.recipes.get().get(idx).map(|r| r.clone())
} }
// FIXME(jwall): Stays public
#[instrument(skip(self))] #[instrument(skip(self))]
pub fn get_shopping_list( pub fn get_shopping_list(
&self, &self,
@ -243,7 +274,7 @@ where
.collect() .collect()
} }
pub fn set_recipes(&mut self, mut recipes: BTreeMap<String, Recipe>) { pub fn set_recipes(&mut self, recipes: BTreeMap<String, Recipe>) {
self.recipes.set( self.recipes.set(
recipes recipes
.iter() .iter()

View File

@ -27,7 +27,7 @@ fn route_switch<G: Html>(route: ReadSignal<AppRoutes>) -> View<G> {
// NOTE(jwall): This needs to not be a dynamic node. The rules around // NOTE(jwall): This needs to not be a dynamic node. The rules around
// this are somewhat unclear and underdocumented for Sycamore. But basically // this are somewhat unclear and underdocumented for Sycamore. But basically
// avoid conditionals in the `view!` macro calls here. // avoid conditionals in the `view!` macro calls here.
cloned!((route) => match route.get().as_ref() { let node = cloned!((route) => match route.get().as_ref() {
AppRoutes::Plan => view! { AppRoutes::Plan => view! {
PlanPage() PlanPage()
}, },
@ -50,7 +50,9 @@ fn route_switch<G: Html>(route: ReadSignal<AppRoutes>) -> View<G> {
"Error: " (e) "Error: " (e)
} }
} }
}) });
debug!(node=?node, "routing to new page");
node
} }
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
@ -95,17 +97,11 @@ pub fn ui(route: Option<AppRoutes>) -> View<G> {
children: || { children: || {
create_effect(move || { create_effect(move || {
spawn_local_in_scope({ spawn_local_in_scope({
let mut app_service = app_service.clone(); let app_service = app_service.clone();
async move { async move {
debug!("fetching recipes"); debug!("fetching recipes");
match app_service.fetch_recipes_from_storage() { if let Err(msg) = app_service.init(false).await {
Ok((_, Some(recipes))) => { error!("Failed to get recipes {}", msg)
app_service.set_recipes(recipes);
}
Ok((_, None)) => {
error!("No recipes to find");
}
Err(msg) => error!("Failed to get recipes {}", msg),
} }
} }
}); });