From d8cfa973f1e7a6e914fa157f4079b612fdde3e4a Mon Sep 17 00:00:00 2001 From: Greg Johnston Date: Sun, 25 Sep 2022 22:10:37 -0400 Subject: [PATCH] Get router working with SSR --- router/src/components/route.rs | 18 ++--- router/src/components/router.rs | 2 + router/src/data/loader.rs | 137 ++++++++++++++++++++++++++------ router/src/history/mod.rs | 2 + router/src/history/state.rs | 8 ++ 5 files changed, 132 insertions(+), 35 deletions(-) diff --git a/router/src/components/route.rs b/router/src/components/route.rs index a03b48a2e..fa665da01 100644 --- a/router/src/components/route.rs +++ b/router/src/components/route.rs @@ -1,4 +1,4 @@ -use std::{any::Any, borrow::Cow, rc::Rc}; +use std::{any::Any, borrow::Cow, future::Future, pin::Pin, rc::Rc}; use leptos_core::IntoVec; use leptos_dom::{Child, Element, IntoChild}; @@ -72,14 +72,12 @@ impl RouteContext { .unwrap_or_default() }); - let data = loader.map(|loader| (loader.data)(cx, params, location.clone())); - Some(Self { inner: Rc::new(RouteContextInner { cx, base_path: base.to_string(), child: Box::new(child), - data, + loader, action, path, original_path: route.original_path.to_string(), @@ -97,12 +95,12 @@ impl RouteContext { &self.inner.path } - pub fn params(&self) -> ParamsMap { - self.inner.params.get() + pub fn params(&self) -> Memo { + self.inner.params } - pub fn data(&self) -> &Option> { - &self.inner.data + pub fn loader(&self) -> &Option { + &self.inner.loader } pub fn base(cx: Scope, path: &str, fallback: Option Element>) -> Self { @@ -111,7 +109,7 @@ impl RouteContext { cx, base_path: path.to_string(), child: Box::new(|| None), - data: None, + loader: None, action: None, path: path.to_string(), original_path: path.to_string(), @@ -138,7 +136,7 @@ pub(crate) struct RouteContextInner { cx: Scope, base_path: String, pub(crate) child: Box Option>, - pub(crate) data: Option>, + pub(crate) loader: Option, pub(crate) action: Option, pub(crate) path: String, pub(crate) original_path: String, diff --git a/router/src/components/router.rs b/router/src/components/router.rs index d3ef5e72d..f4b95d17e 100644 --- a/router/src/components/router.rs +++ b/router/src/components/router.rs @@ -11,6 +11,8 @@ use leptos_reactive::{ }; use thiserror::Error; use typed_builder::TypedBuilder; + +#[cfg(not(feature = "ssr"))] use wasm_bindgen::JsCast; #[cfg(feature = "transition")] diff --git a/router/src/data/loader.rs b/router/src/data/loader.rs index cd1f5d448..3b4e6f764 100644 --- a/router/src/data/loader.rs +++ b/router/src/data/loader.rs @@ -1,49 +1,136 @@ -use std::{any::Any, fmt::Debug, rc::Rc}; +use std::{any::Any, fmt::Debug, future::Future, rc::Rc}; -use leptos_reactive::{debug_warn, Memo, Scope}; +use leptos_reactive::{create_resource, debug_warn, Memo, Resource, Scope}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use crate::{use_route, Location, ParamsMap}; +use crate::{use_location, use_params_map, use_route, Location, ParamsMap, PinnedFuture, Url}; -pub fn use_loader(cx: Scope) -> T +// SSR and CSR both do the work in their own environment and return it as a resource +#[cfg(not(feature = "hydrate"))] +pub fn use_loader(cx: Scope) -> Resource<(ParamsMap, Url), T> where - T: Clone + Debug + 'static, + T: Clone + Debug + Serialize + DeserializeOwned + 'static, { let route = use_route(cx); - let data = match route.data().as_ref() { - Some(data) => data, - None => { - debug_warn!( - "(use_loader) could not find any data for route {}", - route.path() - ); - panic!() - } + let params = use_params_map(cx); + let loader = route.loader().clone().unwrap_or_else(|| { + debug_warn!( + "use_loader() called on a route without a loader: {:?}", + route.path() + ); + panic!() + }); + + let location = use_location(cx); + let url = move || Url { + origin: String::default(), + pathname: location.pathname.get(), + search: location.search.get(), + hash: location.hash.get(), }; - let data = match data.downcast_ref::() { - Some(data) => data, - None => { - debug_warn!("(use_loader) could not downcast data to requested type"); - panic!() - } + + let loader = loader.data.clone(); + + create_resource( + cx, + move || (params.get(), url()), + move |(params, url)| { + let loader = loader.clone(); + async move { + let any_data = (loader.clone())(cx, params, url).await; + any_data.downcast_ref::().cloned().unwrap_or_else(|| { + debug_warn!( + "use_loader() could not downcast to {:?}", + std::any::type_name::(), + ); + panic!() + }) + } + }, + ) +} + +// In hydration mode, only run the loader on the server +#[cfg(feature = "hydrate")] +pub fn use_loader(cx: Scope) -> Resource<(ParamsMap, Url), T> +where + T: Clone + Debug + Serialize + DeserializeOwned + 'static, +{ + use wasm_bindgen::{JsCast, UnwrapThrowExt}; + + let route = use_route(cx); + let params = use_params_map(cx); + + let location = use_location(cx); + let url = move || Url { + origin: String::default(), + pathname: location.pathname.get(), + search: location.search.get(), + hash: location.hash.get(), }; - data.clone() + + create_resource( + cx, + move || (params.get(), url()), + move |(params, url)| async move { + let route = use_route(cx); + let mut opts = web_sys::RequestInit::new(); + opts.method("GET"); + let url = route.path(); + + let request = web_sys::Request::new_with_str_and_init(&url, &opts).unwrap_throw(); + request + .headers() + .set("Accept", "application/json") + .unwrap_throw(); + + let window = web_sys::window().unwrap_throw(); + let resp_value = + wasm_bindgen_futures::JsFuture::from(window.fetch_with_request(&request)) + .await + .unwrap_throw(); + let resp = resp_value.unchecked_into::(); + let text = wasm_bindgen_futures::JsFuture::from(resp.text().unwrap_throw()) + .await + .unwrap_throw() + .as_string() + .unwrap_throw(); + let decoded = window.atob(&text).unwrap_throw(); + bincode::deserialize(&decoded.as_bytes()).unwrap_throw() + //serde_json::from_str(&json.as_string().unwrap_throw()).unwrap_throw() + }, + ) } #[derive(Clone)] pub struct Loader { - pub(crate) data: Rc, Location) -> Box>, + #[cfg(not(feature = "hydrate"))] + pub(crate) data: Rc PinnedFuture>>, } -impl From for Loader +impl From for Loader where - F: Fn(Scope, Memo, Location) -> T + 'static, + F: Fn(Scope, ParamsMap, Url) -> Fu + 'static, + Fu: Future + 'static, T: Any + 'static, { + #[cfg(not(feature = "hydrate"))] fn from(f: F) -> Self { + let wrapped_fn = move |cx, params, url| { + let res = f(cx, params, url); + Box::pin(async move { Box::new(res.await) as Box }) + as PinnedFuture> + }; + Self { - data: Rc::new(move |cx, params, location| Box::new(f(cx, params, location))), + data: Rc::new(wrapped_fn), } } + + #[cfg(feature = "hydrate")] + fn from(f: F) -> Self { + Self {} + } } impl std::fmt::Debug for Loader { diff --git a/router/src/history/mod.rs b/router/src/history/mod.rs index 78a66c14b..9581ab353 100644 --- a/router/src/history/mod.rs +++ b/router/src/history/mod.rs @@ -1,4 +1,6 @@ use leptos_reactive::{create_signal, use_context, ReadSignal, Scope}; + +#[cfg(not(feature = "ssr"))] use wasm_bindgen::UnwrapThrowExt; mod location; diff --git a/router/src/history/state.rs b/router/src/history/state.rs index 06b60fa3d..40c3c250a 100644 --- a/router/src/history/state.rs +++ b/router/src/history/state.rs @@ -1,9 +1,16 @@ +#[cfg(not(feature = "ssr"))] use wasm_bindgen::JsValue; #[derive(Debug, Clone, Default, PartialEq)] +#[cfg(not(feature = "ssr"))] pub struct State(pub Option); +#[derive(Debug, Clone, Default, PartialEq)] +#[cfg(feature = "ssr")] +pub struct State(pub Option<()>); + impl State { + #[cfg(not(feature = "ssr"))] pub fn to_js_value(&self) -> JsValue { match &self.0 { Some(v) => v.clone(), @@ -12,6 +19,7 @@ impl State { } } +#[cfg(not(feature = "ssr"))] impl From for State where T: Into,