Fixing/updating SSR/hydration and some bugs in new reactivity

This commit is contained in:
Greg Johnston 2022-10-15 22:17:58 -04:00
parent 7a04411b64
commit 3dd3870dbf
15 changed files with 76 additions and 112 deletions

View file

@ -61,26 +61,30 @@ pub fn Stories(cx: Scope) -> Element {
let hide_more_link = move || stories.read().unwrap_or(Err(())).unwrap_or_default().len() < 28;
view! { cx,
view! {
cx,
<div class="news-view">
<div class="news-list-nav">
{move || if page() > 1 {
view! {
cx,
<a class="page-link"
href=move || format!("/{}?page={}", story_type(), page() - 1)
attr:aria_label="Previous Page"
>
"< prev"
</a>
}
} else {
view! { cx,
<span class="page-link disabled" aria-hidden="true">
"< prev"
</span>
}
}}
<span>
{move || if page() > 1 {
view! {
cx,
<a class="page-link"
href=move || format!("/{}?page={}", story_type(), page() - 1)
attr:aria_label="Previous Page"
>
"< prev"
</a>
}
} else {
view! {
cx,
<span class="page-link disabled" aria-hidden="true">
"< prev"
</span>
}
}}
</span>
<span>"page " {page}</span>
<span class="page-link"
class:disabled=hide_more_link

View file

@ -26,7 +26,6 @@ pub fn Story(cx: Scope) -> Element {
"("{story.domain}")"
</span>
{story.user.map(|user| view! { cx, <p class="meta">
// TODO issue here in renderer
{story.points}
" points | by "
<A href=format!("/users/{}", user)>{user.clone()}</A>

View file

@ -7,7 +7,7 @@ edition = "2021"
console_log = "0.2"
log = "0.4"
leptos = { path = "../../leptos", features = ["csr"] }
leptos_router = { path = "../../router", features = ["csr"], version = "0.0.4" }
leptos_router = { path = "../../router", features = ["csr"], version = "0.0" }
serde = { version = "1", features = ["derive"] }
wee_alloc = "0.4"
futures = "0.3"

View file

@ -23,7 +23,7 @@ async fn contact_data(_cx: Scope, params: ParamsMap, _url: Url) -> Option<Contac
}
pub fn router_example(cx: Scope) -> Element {
view! { cx,
view! { cx,
<div id="root">
<Router>
<nav>
@ -72,18 +72,18 @@ pub fn ContactList(cx: Scope) -> Element {
let contacts = use_loader::<Vec<ContactSummary>>(cx);
log::debug!("rendering <ContactList/>");
view! { cx,
view! { cx,
<div class="contact-list">
<h1>"Contacts"</h1>
<ul>
<Suspense fallback=move || view! { cx, <p>"Loading contacts..."</p> }>{
move || {
contacts.read().map(|contacts| view! { cx,
contacts.read().map(|contacts| view! { cx,
<For each=move || contacts.clone() key=|contact| contact.id>
{move |cx, contact: &ContactSummary| {
let id = contact.id;
let name = format!("{} {}", contact.first_name, contact.last_name);
view! { cx,
view! { cx,
<li><A href=id.to_string()><span>{name.clone()}</span></A></li>
}
}}
@ -101,10 +101,10 @@ pub fn ContactList(cx: Scope) -> Element {
pub fn Contact(cx: Scope) -> Element {
let contact = use_loader::<Option<Contact>>(cx);
view! { cx,
view! { cx,
<div class="contact">
<Suspense fallback=move || view! { cx, <p>"Loading..."</p> }>{
move || contact.read().map(|contact| contact.map(|contact| view! { cx,
move || contact.read().map(|contact| contact.map(|contact| view! { cx,
<section class="card">
<h1>{contact.first_name} " " {contact.last_name}</h1>
<p>{contact.address_1}<br/>{contact.address_2}</p>
@ -117,7 +117,7 @@ pub fn Contact(cx: Scope) -> Element {
#[component]
pub fn About(_cx: Scope) -> Vec<Element> {
view! { cx,
view! { cx,
<>
<h1>"About"</h1>
<p>"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."</p>
@ -127,7 +127,7 @@ pub fn About(_cx: Scope) -> Vec<Element> {
#[component]
pub fn Settings(_cx: Scope) -> Vec<Element> {
view! { cx,
view! { cx,
<>
<h1>"Settings"</h1>
<form>

0
examples/todomvc-ssr/todomvc-ssr-client/build.sh Normal file → Executable file
View file

View file

@ -136,7 +136,8 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> Element {
let title = event_target_value(&ev);
let title = title.trim();
if !title.is_empty() {
set_todos.update(|t| t.add(Todo::new(cx, next_id, title.to_string())));
let new = Todo::new(cx, next_id, title.to_string());
set_todos.update(|t| t.add(new));
next_id += 1;
target.set_value("");
}
@ -288,6 +289,7 @@ pub fn Todo(cx: Scope, todo: Todo) -> Element {
</li>
};
#[cfg(not(feature = "ssr"))]
create_effect(cx, move |_| {
if editing() {
_ = input.unchecked_ref::<HtmlInputElement>().focus();

View file

@ -4,7 +4,7 @@ use std::collections::{HashMap, HashSet};
use std::{collections::HashMap, future::Future, pin::Pin};
#[cfg(any(feature = "hydrate"))]
use crate::{Scope, StreamingResourceId};
use crate::{ResourceId, Scope};
#[derive(Default)]
pub struct SharedContext {
@ -16,9 +16,9 @@ pub struct SharedContext {
#[cfg(feature = "hydrate")]
pub registry: HashMap<String, web_sys::Element>,
#[cfg(feature = "hydrate")]
pub pending_resources: HashSet<StreamingResourceId>,
pub pending_resources: HashSet<ResourceId>,
#[cfg(feature = "hydrate")]
pub resolved_resources: HashMap<StreamingResourceId, String>,
pub resolved_resources: HashMap<ResourceId, String>,
#[cfg(feature = "ssr")]
pub pending_fragments: HashMap<String, Pin<Box<dyn Future<Output = String>>>>,
}
@ -64,7 +64,7 @@ impl SharedContext {
&web_sys::window().unwrap(),
&wasm_bindgen::JsValue::from_str("__LEPTOS_PENDING_RESOURCES"),
);
let pending_resources: HashSet<StreamingResourceId> = pending_resources
let pending_resources: HashSet<ResourceId> = pending_resources
.map_err(|_| ())
.and_then(|pr| serde_wasm_bindgen::from_value(pr).map_err(|_| ()))
.unwrap_or_default();
@ -75,15 +75,8 @@ impl SharedContext {
)
.unwrap_or(wasm_bindgen::JsValue::NULL);
let resolved_resources = match serde_wasm_bindgen::from_value(resolved_resources) {
Ok(v) => v,
Err(e) => {
log::debug!(
"(create_resource) error deserializing __LEPTOS_RESOLVED_RESOURCES\n\n{e}"
);
HashMap::default()
}
};
let resolved_resources =
serde_wasm_bindgen::from_value(resolved_resources).unwrap_or_default();
Self {
completed: Default::default(),

View file

@ -61,6 +61,7 @@ where
cx.runtime.create_memo(f)
}
#[derive(Debug, PartialEq, Eq)]
pub struct Memo<T>(pub(crate) ReadSignal<Option<T>>)
where
T: 'static;

View file

@ -87,7 +87,9 @@ where
{
let resolved = initial_value.is_some();
let (value, set_value) = create_signal(cx, initial_value);
let (loading, set_loading) = create_signal(cx, false);
let (track, trigger) = create_signal(cx, 0);
let fetcher = Rc::new(move |s| Box::pin(fetcher(s)) as Pin<Box<dyn Future<Output = T>>>);
let source = create_memo(cx, move |_| source());
@ -141,34 +143,19 @@ where
{
use wasm_bindgen::{JsCast, UnwrapThrowExt};
log::debug!("[create_resource] load_resource in hydrate mode");
if let Some(ref mut context) = *cx.runtime.shared_context.borrow_mut() {
let resource_id = StreamingResourceId(cx.id, id);
log::debug!(
"(create_resource) resolved resources = {:#?}",
context.resolved_resources
);
log::debug!(
"(create_resource) pending resources = {:#?}",
context.pending_resources
);
if let Some(data) = context.resolved_resources.remove(&resource_id) {
log::debug!("(create_resource) resource already resolved from server");
context.pending_resources.remove(&resource_id); // no longer pending
if let Some(data) = context.resolved_resources.remove(&id) {
context.pending_resources.remove(&id); // no longer pending
r.resolved.set(true);
//let decoded = base64::decode(&data).unwrap_throw();
//let res = bincode::deserialize(&decoded).unwrap_throw();
let res = serde_json::from_str(&data).unwrap_throw();
log::debug!("deserialized = {res:?}");
r.set_value.update(|n| *n = Some(res));
r.set_loading.update(|n| *n = false);
// for reactivity
_ = r.source.get();
} else if context.pending_resources.remove(&resource_id) {
log::debug!("(create_resource) resource pending from server");
} else if context.pending_resources.remove(&id) {
r.set_loading.update(|n| *n = true);
r.trigger.update(|n| *n += 1);
@ -203,14 +190,9 @@ where
// for reactivity
_ = r.source.get();
} else {
log::debug!(
"(create_resource) resource not found in hydration context, loading\n\n{:#?}",
context.pending_resources
);
r.load(false);
}
} else {
log::debug!("(create_resource) no hydration context, loading resource");
r.load(false)
}
}
@ -243,7 +225,7 @@ where
T: Serialize + DeserializeOwned,
{
self.runtime
.resource((self.scope, self.id), |resource: &ResourceState<S, T>| {
.resource(self.id, |resource: &ResourceState<S, T>| {
resource.to_serialization_resolver(self.id)
})
.await
@ -430,8 +412,8 @@ where
#[cfg(feature = "ssr")]
pub fn resource_to_serialization_resolver(
&self,
id: StreamingResourceId,
) -> std::pin::Pin<Box<dyn futures::Future<Output = (StreamingResourceId, String)>>>
id: ResourceId,
) -> std::pin::Pin<Box<dyn futures::Future<Output = (ResourceId, String)>>>
where
T: Serialize,
{
@ -450,8 +432,8 @@ pub(crate) trait AnyResource {
#[cfg(feature = "ssr")]
fn to_serialization_resolver(
&self,
id: StreamingResourceId,
) -> Pin<Box<dyn Future<Output = (StreamingResourceId, String)>>>;
id: ResourceId,
) -> Pin<Box<dyn Future<Output = (ResourceId, String)>>>;
}
impl<S, T> AnyResource for ResourceState<S, T>
@ -466,8 +448,8 @@ where
#[cfg(feature = "ssr")]
fn to_serialization_resolver(
&self,
id: StreamingResourceId,
) -> Pin<Box<dyn Future<Output = (StreamingResourceId, String)>>> {
id: ResourceId,
) -> Pin<Box<dyn Future<Output = (ResourceId, String)>>> {
let fut = self.resource_to_serialization_resolver(id);
Box::pin(fut)
}

View file

@ -23,7 +23,7 @@ pub(crate) struct Runtime {
pub scope_children: RefCell<SparseSecondaryMap<ScopeId, RefCell<Vec<ScopeId>>>>,
#[allow(clippy::type_complexity)]
pub scope_contexts: RefCell<SparseSecondaryMap<ScopeId, HashMap<TypeId, Box<dyn Any>>>>,
pub signals: RefCell<SlotMap<SignalId, RefCell<Box<dyn Any>>>>,
pub signals: RefCell<SlotMap<SignalId, Rc<RefCell<dyn Any>>>>,
pub signal_subscribers: RefCell<SecondaryMap<SignalId, RefCell<HashSet<EffectId>>>>,
pub effects: RefCell<SlotMap<EffectId, Rc<RefCell<dyn AnyEffect>>>>,
pub effect_sources: RefCell<SecondaryMap<EffectId, RefCell<HashSet<SignalId>>>>,
@ -80,7 +80,7 @@ impl Runtime {
let id = self
.signals
.borrow_mut()
.insert(RefCell::new(Box::new(value)));
.insert(Rc::new(RefCell::new(value)));
(
ReadSignal {
runtime: self,
@ -102,7 +102,7 @@ impl Runtime {
let id = self
.signals
.borrow_mut()
.insert(RefCell::new(Box::new(value)));
.insert(Rc::new(RefCell::new(value)));
RwSignal {
runtime: self,
id,
@ -135,9 +135,9 @@ impl Runtime {
T: Clone + PartialEq + Any + 'static,
{
let (read, write) = self.create_signal(None);
self.create_effect(move |prev| {
let new = { f(prev.clone()) };
if prev.as_ref() != Some(&new) {
write(Some(new.clone()));
}
@ -228,13 +228,7 @@ impl Runtime {
std::pin::Pin<Box<dyn futures::Future<Output = (ResourceId, String)>>>,
> {
let f = futures::stream::futures_unordered::FuturesUnordered::new();
for (id, resource) in self.scopes.borrow().iter().flat_map(|(scope_id, scope)| {
scope
.resources
.iter()
.enumerate()
.map(move |(idx, resource)| (idx, resource))
}) {
for (id, resource) in self.resources.borrow().iter() {
f.push(resource.to_serialization_resolver(id));
}
f

View file

@ -57,7 +57,7 @@ pub fn run_scope_undisposed<T>(f: impl FnOnce(Scope) -> T + 'static) -> (T, Scop
///
/// Every other function in this crate takes a `Scope` as its first argument. Since `Scope`
/// is [Copy] and `'static` this does not add much overhead or lifetime complexity.
#[derive(Copy, Clone)]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct Scope {
pub(crate) runtime: &'static Runtime,
pub(crate) id: ScopeId,
@ -312,7 +312,7 @@ impl Scope {
pub fn serialization_resolvers(
&self,
) -> futures::stream::futures_unordered::FuturesUnordered<
std::pin::Pin<Box<dyn futures::Future<Output = (StreamingResourceId, String)>>>,
std::pin::Pin<Box<dyn futures::Future<Output = (ResourceId, String)>>>,
> {
self.runtime.serialization_resolvers()
}
@ -343,10 +343,7 @@ impl Scope {
create_isomorphic_effect(*self, move |fut| {
let pending = context.pending_resources.get();
if pending == 0 {
log::debug!("\n\n\npending_resources.get() == 0");
tx.try_send(());
} else {
log::debug!("\n\n\npending_resources.get() == {pending}");
}
});

View file

@ -357,10 +357,12 @@ impl SignalId {
}
// get the value
let value = runtime.signals.borrow();
let value = value
.get(*self)
.unwrap_or_else(|| panic!("tried to access a signal that has been disposed: {self:?}"));
let value = {
let signals = runtime.signals.borrow();
signals.get(*self).cloned().unwrap_or_else(|| {
panic!("tried to access a signal that has been disposed: {self:?}")
})
};
let value = value.borrow();
let value = value.downcast_ref::<T>().unwrap_or_else(|| {
panic!(
@ -378,10 +380,12 @@ impl SignalId {
{
// update the value
{
let value = runtime.signals.borrow();
let value = value.get(*self).unwrap_or_else(|| {
panic!("tried to access a signal that has been disposed: {self:?}")
});
let value = {
let signals = runtime.signals.borrow();
signals.get(*self).cloned().unwrap_or_else(|| {
panic!("tried to access a signal that has been disposed: {self:?}")
})
};
let mut value = value.borrow_mut();
let value = value.downcast_mut::<T>().unwrap_or_else(|| {
panic!(

View file

@ -122,17 +122,8 @@ impl RouterContext {
let LocationChange { value, state, .. } = source();
cx.untrack(move || {
if value != reference() {
#[cfg(feature = "transition")]
transition.start(move || {
set_reference.update(move |r| *r = value.clone());
set_state.update(move |s| *s = state.clone());
});
#[cfg(not(feature = "transition"))]
{
set_reference.update(move |r| *r = value.clone());
set_state.update(move |s| *s = state.clone());
}
set_reference.update(move |r| *r = value);
set_state.update(move |s| *s = state);
}
});
});

View file

@ -68,13 +68,10 @@ impl History for BrowserIntegration {
Ok(_) => log::debug!("navigated"),
Err(e) => log::error!("{e:#?}"),
};
set_location.update(|change| *change = Self::current());
set_location(Self::current());
} else {
log::debug!("RouterContext not found");
}
//Self::navigate(&Self {}, &Self::current());
//set_location.update(|change| *change = Self::current());
});
location

View file

@ -1,6 +1,6 @@
use crate::ParamsMap;
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Url {
pub origin: String,
pub pathname: String,