Remove route data loaders for now.

This commit is contained in:
Greg Johnston
2022-11-05 09:32:12 -04:00
parent 2595ffe10e
commit 2483616d0d
6 changed files with 21 additions and 282 deletions

View File

@@ -31,46 +31,26 @@ async fn render_app(req: HttpRequest) -> impl Responder {
view! { cx, <App/> }
};
let accepts_type = req.headers().get("Accept").map(|h| h.to_str());
match accepts_type {
// if asks for JSON, send the loader function JSON or 404
Some(Ok("application/json")) => {
let json = loader_to_json(app).await;
let head = r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<script type="module">import init, { main } from '/pkg/hackernews_client.js'; init().then(main);</script>"#;
let tail = "</body></html>";
let res = if let Some(json) = json {
HttpResponse::Ok()
.content_type("application/json")
.body(json)
} else {
HttpResponse::NotFound().body(())
};
res
}
// otherwise, send HTML
_ => {
let head = r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<script type="module">import init, { main } from '/pkg/hackernews_client.js'; init().then(main);</script>"#;
let tail = "</body></html>";
HttpResponse::Ok().content_type("text/html").streaming(
futures::stream::once(async { head.to_string() })
.chain(render_to_stream(move |cx| {
let app = app(cx);
let head = use_context::<MetaContext>(cx)
.map(|meta| meta.dehydrate())
.unwrap_or_default();
format!("{head}</head><body>{app}")
}))
.chain(futures::stream::once(async { tail.to_string() }))
.map(|html| Ok(web::Bytes::from(html)) as Result<web::Bytes>),
)
}
}
HttpResponse::Ok().content_type("text/html").streaming(
futures::stream::once(async { head.to_string() })
.chain(render_to_stream(move |cx| {
let app = app(cx);
let head = use_context::<MetaContext>(cx)
.map(|meta| meta.dehydrate())
.unwrap_or_default();
format!("{head}</head><body>{app}")
}))
.chain(futures::stream::once(async { tail.to_string() }))
.map(|html| Ok(web::Bytes::from(html)) as Result<web::Bytes>),
)
}
#[actix_web::main]

View File

@@ -5,7 +5,7 @@ use typed_builder::TypedBuilder;
use crate::{
matching::{resolve_path, PathMatch, RouteDefinition, RouteMatch},
Loader, ParamsMap, RouterContext,
ParamsMap, RouterContext,
};
/// Properties that can be passed to a [Route] component, which describes
@@ -25,10 +25,6 @@ where
/// that takes a [Scope] and returns an [Element] (like `|cx| view! { cx, <p>"Show this"</p> })`
/// or `|cx| view! { cx, <MyComponent/>` } or even, for a component with no props, `MyComponent`).
pub element: F,
/// A data loader is a function that will be run to begin loading data as soon as you navigate to a route.
/// These are run in parallel for all nested routes, to avoid data-fetching waterfalls.
#[builder(default, setter(strip_option))]
pub loader: Option<Loader>,
/// `children` may be empty or include nested routes.
#[builder(default, setter(strip_option))]
pub children: Option<Box<dyn Fn() -> Vec<RouteDefinition>>>,
@@ -44,7 +40,6 @@ where
{
RouteDefinition {
path: props.path,
loader: props.loader,
children: props.children.map(|c| c()).unwrap_or_default(),
element: Rc::new(move |cx| (props.element)(cx).into_child(cx)),
}
@@ -67,9 +62,7 @@ impl RouteContext {
let base = base.path();
let RouteMatch { path_match, route } = matcher()?;
let PathMatch { path, .. } = path_match;
let RouteDefinition {
element, loader, ..
} = route.key;
let RouteDefinition { element, .. } = route.key;
let params = create_memo(cx, move |_| {
matcher()
.map(|matched| matched.path_match.params)
@@ -81,7 +74,6 @@ impl RouteContext {
cx,
base_path: base.to_string(),
child: Box::new(child),
loader,
path,
original_path: route.original_path.to_string(),
params,
@@ -105,18 +97,12 @@ impl RouteContext {
self.inner.params
}
/// The data loader for the current route.
pub fn loader(&self) -> &Option<Loader> {
&self.inner.loader
}
pub(crate) fn base(cx: Scope, path: &str, fallback: Option<fn() -> Element>) -> Self {
Self {
inner: Rc::new(RouteContextInner {
cx,
base_path: path.to_string(),
child: Box::new(|| None),
loader: None,
path: path.to_string(),
original_path: path.to_string(),
params: create_memo(cx, |_| ParamsMap::new()),
@@ -145,7 +131,6 @@ pub(crate) struct RouteContextInner {
cx: Scope,
base_path: String,
pub(crate) child: Box<dyn Fn() -> Option<RouteContext>>,
pub(crate) loader: Option<Loader>,
pub(crate) path: String,
pub(crate) original_path: String,
pub(crate) params: Memo<ParamsMap>,

View File

@@ -1,207 +0,0 @@
use std::{any::Any, fmt::Debug, future::Future, rc::Rc};
use leptos::*;
use crate::{use_location, use_params_map, use_route, ParamsMap, PinnedFuture, Url};
// 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<T>(cx: Scope) -> Resource<(ParamsMap, Url), T>
where
T: Clone + Debug + Serializable + 'static,
{
let route = use_route(cx);
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 route = use_route(cx);
let url = move || Url {
origin: String::default(), // don't care what the origin is for this purpose
pathname: route.path().into(), // only use this route path, not all matched routes
search: location.search.get(), // reload when any of query string changes
hash: String::default(), // hash is only client-side, shouldn't refire
};
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
.as_any()
.downcast_ref::<T>()
.cloned()
.unwrap_or_else(|| {
panic!(
"use_loader() could not downcast to {:?}",
std::any::type_name::<T>(),
)
})
}
},
)
}
// In hydration mode, only run the loader on the server
#[cfg(feature = "hydrate")]
pub fn use_loader<T>(cx: Scope) -> Resource<(ParamsMap, Url), T>
where
T: Clone + Debug + Serializable + 'static,
{
use wasm_bindgen::{JsCast, UnwrapThrowExt};
use crate::use_query_map;
let route = use_route(cx);
let params = use_params_map(cx);
let location = use_location(cx);
let route = use_route(cx);
let url = move || Url {
origin: String::default(), // don't care what the origin is for this purpose
pathname: route.path().into(), // only use this route path, not all matched routes
search: location.search.get(), // reload when any of query string changes
hash: String::default(), // hash is only client-side, shouldn't refire
};
create_resource(
cx,
move || (params.get(), url()),
move |(params, url)| async move {
let route = use_route(cx);
let query = use_query_map(cx);
let mut opts = web_sys::RequestInit::new();
opts.method("GET");
let url = format!("{}{}", route.path(), query.get().to_query_string());
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::<web_sys::Response>();
let text = wasm_bindgen_futures::JsFuture::from(resp.text().unwrap_throw())
.await
.unwrap_throw()
.as_string()
.unwrap_throw();
T::from_json(&text).expect_throw(
"couldn't deserialize loader data from serde-lite intermediate format",
)
},
)
}
pub trait AnySerialize {
fn serialize(&self) -> Option<String>;
fn as_any(&self) -> &dyn Any;
}
impl<T> AnySerialize for T
where
T: Any + Serializable + 'static,
{
fn serialize(&self) -> Option<String> {
self.to_json().ok()
}
fn as_any(&self) -> &dyn Any {
self
}
}
#[derive(Clone)]
pub struct Loader {
#[allow(clippy::type_complexity)]
#[cfg(not(feature = "hydrate"))]
pub(crate) data: Rc<dyn Fn(Scope, ParamsMap, Url) -> PinnedFuture<Box<dyn AnySerialize>>>,
}
impl Loader {
#[cfg(not(feature = "hydrate"))]
pub fn call_loader(&self, cx: Scope) -> PinnedFuture<Box<dyn AnySerialize>> {
let route = use_route(cx);
let params = use_params_map(cx).get();
let location = use_location(cx);
let url = Url {
origin: String::default(), // don't care what the origin is for this purpose
pathname: route.path().into(), // only use this route path, not all matched routes
search: location.search.get(), // reload when any of query string changes
hash: String::default(), // hash is only client-side, shouldn't refire
};
(self.data)(cx, params, url)
}
}
impl<F, Fu, T> From<F> for Loader
where
F: Fn(Scope, ParamsMap, Url) -> Fu + 'static,
Fu: Future<Output = T> + 'static,
T: Any + Serializable + '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<dyn AnySerialize> })
as PinnedFuture<Box<dyn AnySerialize>>
};
Self {
data: Rc::new(wrapped_fn),
}
}
#[cfg(feature = "hydrate")]
fn from(f: F) -> Self {
Self {}
}
}
impl std::fmt::Debug for Loader {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Loader").finish()
}
}
#[cfg(all(feature = "ssr", not(feature = "hydrate")))]
pub async fn loader_to_json(view: impl Fn(Scope) -> String + 'static) -> Option<String> {
let (data, _, disposer) = run_scope_undisposed(move |cx| async move {
let _shell = view(cx);
let mut route = use_context::<crate::RouteContext>(cx)?;
// get the innermost route matched by this path
while let Some(child) = route.child() {
route = child;
}
let data = route
.loader()
.as_ref()
.map(|loader| loader.call_loader(cx))?;
data.await.serialize()
});
let data = data.await;
disposer.dispose();
data
}

View File

@@ -1,7 +0,0 @@
mod loader;
use std::{future::Future, pin::Pin};
pub use loader::*;
pub(crate) type PinnedFuture<T> = Pin<Box<dyn Future<Output = T>>>;

View File

@@ -67,7 +67,6 @@
//! element=move |cx| view! { cx, <Contact/> }
//! />
//! // a fallback if the /:id segment is missing from the URL
//! // doesn't need any data, so no loader is provided
//! <Route
//! path=""
//! element=move |_| view! { cx, <p class="contact">"Select a contact."</p> }
@@ -85,10 +84,6 @@
//! }
//! }
//!
//! // Loaders are async functions that have access to the reactive scope,
//! // map of matched URL params for that route, and the URL
//! // They are reloaded whenever the params or URL change
//!
//! type ContactSummary = (); // TODO!
//! type Contact = (); // TODO!()
//!
@@ -145,14 +140,12 @@
#![feature(type_name_of_val)]
mod components;
mod data;
mod fetch;
mod history;
mod hooks;
mod matching;
pub use components::*;
pub use data::*;
pub use fetch::*;
pub use history::*;
pub use hooks::*;

View File

@@ -3,12 +3,9 @@ use std::rc::Rc;
use leptos::leptos_dom::Child;
use leptos::*;
use crate::Loader;
#[derive(Clone)]
pub struct RouteDefinition {
pub path: &'static str,
pub loader: Option<Loader>,
pub children: Vec<RouteDefinition>,
pub element: Rc<dyn Fn(Scope) -> Child>,
}
@@ -17,7 +14,6 @@ impl std::fmt::Debug for RouteDefinition {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RouteDefinition")
.field("path", &self.path)
.field("loader", &self.loader)
.field("children", &self.children)
.finish()
}
@@ -33,7 +29,6 @@ impl Default for RouteDefinition {
fn default() -> Self {
Self {
path: Default::default(),
loader: Default::default(),
children: Default::default(),
element: Rc::new(|_| Child::Null),
}