mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 06:44:17 +00:00
Fixing/updating SSR/hydration and some bugs in new reactivity
This commit is contained in:
parent
7a04411b64
commit
3dd3870dbf
15 changed files with 76 additions and 112 deletions
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
0
examples/todomvc-ssr/todomvc-ssr-client/build.sh
Normal file → Executable 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();
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}");
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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!(
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue