Skip to content

Commit 82430db

Browse files
committed
feat: added lazy global state instantiation
This makes the `global_state` macro parameter only required on templates that actually use the global state, which is much more ergonomic.
1 parent a5fcc56 commit 82430db

File tree

4 files changed

+42
-50
lines changed

4 files changed

+42
-50
lines changed

examples/rx_state/src/about.rs

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
use crate::global_state::AppState;
21
use crate::index::IndexPropsRx;
32
use perseus::{get_render_ctx, Html, Template};
43
use sycamore::prelude::{view, Signal};
54
use sycamore::view::View;
65

76
// This template doesn't have any properties, so there's no point in using the special `template_with_rx_state` macro (but we could)
8-
#[perseus::template_with_rx_state(component = "AboutPage", global_state = "AppState")]
7+
#[perseus::template_with_rx_state(component = "AboutPage")]
98
pub fn about_page() -> View<G> {
109
// Get the page state store manually
1110
// The index page is just an empty string

packages/perseus-macro/src/template2.rs

+30-39
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,9 @@ pub struct TemplateArgs {
106106
/// The name of the type parameter to use (default to `G`).
107107
#[darling(default)]
108108
type_param: Option<Ident>,
109-
// /// The identifier of the global state type, if there is one.
110-
// #[darling(default)]
111-
// global_state: Option<Ident>,
112-
/// The identifier of the global state.
113-
global_state: Ident,
109+
/// The identifier of the global state type, if this template needs it.
110+
#[darling(default)]
111+
global_state: Option<Ident>,
114112
/// The name of the unreactive properties, if there are any.
115113
#[darling(default)]
116114
unrx_props: Option<Ident>,
@@ -129,23 +127,22 @@ pub fn template_impl(input: TemplateFn, args: TemplateArgs) -> TokenStream {
129127
} = input;
130128

131129
let component_name = &args.component;
132-
let global_state = &args.global_state;
133130
let type_param = match &args.type_param {
134131
Some(type_param) => type_param.clone(),
135132
None => Ident::new("G", Span::call_site()),
136133
};
137134
// This is only optional if the second argument wasn't provided
138-
// let global_state = if fn_args.len() == 2 {
139-
// match &args.global_state {
140-
// Some(global_state) => global_state.clone(),
141-
// None => return syn::Error::new_spanned(&fn_args[0], "template functions with two arguments must declare their global state type (`global_state = `)").to_compile_error()
142-
// }
143-
// } else {
144-
// match &args.global_state {
145-
// Some(global_state) => global_state.clone(),
146-
// None => Ident::new("Dummy", Span::call_site()),
147-
// }
148-
// };
135+
let global_state = if fn_args.len() == 2 {
136+
match &args.global_state {
137+
Some(global_state) => global_state.clone(),
138+
None => return syn::Error::new_spanned(&fn_args[0], "template functions with two arguments must declare their global state type (`global_state = `)").to_compile_error()
139+
}
140+
} else {
141+
match &args.global_state {
142+
Some(global_state) => global_state.clone(),
143+
None => Ident::new("Dummy", Span::call_site()),
144+
}
145+
};
149146
// This is only optional if the first argument wasn't provided
150147
let unrx_props = if !fn_args.is_empty() {
151148
match &args.unrx_props {
@@ -159,25 +156,6 @@ pub fn template_impl(input: TemplateFn, args: TemplateArgs) -> TokenStream {
159156
}
160157
};
161158

162-
let manage_global_state = quote! {
163-
// Deserialize the global state, make it reactive, and register it with the `RenderCtx`
164-
// If it's already there, we'll leave it
165-
// This means that we can pass an `Option<String>` around safely and then deal with it at the template site
166-
let global_state_refcell = ::perseus::get_render_ctx!().global_state;
167-
let global_state = global_state_refcell.borrow();
168-
if (&global_state).downcast_ref::<::std::option::Option::<()>>().is_some() {
169-
// We can downcast it as the type set by the core render system, so we're the first page to be loaded
170-
// In that case, we'll set the global state properly
171-
drop(global_state);
172-
let mut global_state = global_state_refcell.borrow_mut();
173-
// This will be defined if we're the first page
174-
let global_state_props = &props.global_state.unwrap();
175-
let new_global_state = ::serde_json::from_str::<#global_state>(global_state_props).unwrap().make_rx();
176-
*global_state = ::std::boxed::Box::new(new_global_state);
177-
// The component function can now access this in `RenderCtx`
178-
}
179-
};
180-
181159
// We create a wrapper function that can be easily provided to `.template()` that does deserialization automatically if needed
182160
// This is dependent on what arguments the template takes
183161
if fn_args.len() == 2 {
@@ -193,7 +171,22 @@ pub fn template_impl(input: TemplateFn, args: TemplateArgs) -> TokenStream {
193171
};
194172
quote! {
195173
#vis fn #name<G: ::sycamore::prelude::Html>(props: ::perseus::templates::PageProps) -> ::sycamore::prelude::View<G> {
196-
#manage_global_state
174+
// Deserialize the global state, make it reactive, and register it with the `RenderCtx`
175+
// If it's already there, we'll leave it
176+
// This means that we can pass an `Option<String>` around safely and then deal with it at the template site
177+
let global_state_refcell = ::perseus::get_render_ctx!().global_state;
178+
let global_state = global_state_refcell.borrow();
179+
if (&global_state).downcast_ref::<::std::option::Option::<()>>().is_some() {
180+
// We can downcast it as the type set by the core render system, so we're the first page to be loaded
181+
// In that case, we'll set the global state properly
182+
drop(global_state);
183+
let mut global_state = global_state_refcell.borrow_mut();
184+
// This will be defined if we're the first page
185+
let global_state_props = &props.global_state.unwrap();
186+
let new_global_state = ::serde_json::from_str::<#global_state>(global_state_props).unwrap().make_rx();
187+
*global_state = ::std::boxed::Box::new(new_global_state);
188+
// The component function can now access this in `RenderCtx`
189+
}
197190
// The user's function
198191
// We know this won't be async because Sycamore doesn't allow that
199192
#(#attrs)*
@@ -236,7 +229,6 @@ pub fn template_impl(input: TemplateFn, args: TemplateArgs) -> TokenStream {
236229
let arg = &fn_args[0];
237230
quote! {
238231
#vis fn #name<G: ::sycamore::prelude::Html>(props: ::perseus::templates::PageProps) -> ::sycamore::prelude::View<G> {
239-
#manage_global_state
240232
// The user's function, with Sycamore component annotations and the like preserved
241233
// We know this won't be async because Sycamore doesn't allow that
242234
#(#attrs)*
@@ -270,7 +262,6 @@ pub fn template_impl(input: TemplateFn, args: TemplateArgs) -> TokenStream {
270262
// There are no arguments
271263
quote! {
272264
#vis fn #name<G: ::sycamore::prelude::Html>(props: ::perseus::templates::PageProps) -> ::sycamore::prelude::View<G> {
273-
#manage_global_state
274265
// The user's function, with Sycamore component annotations and the like preserved
275266
// We know this won't be async because Sycamore doesn't allow that
276267
#(#attrs)*

packages/perseus/src/html_shell.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,12 @@ impl<'a> HtmlShell<'a> {
110110
"None".to_string()
111111
};
112112

113-
// We put these at the very end of the head (after the delimiter comment) because it doesn't matter if they're expunged on subsequent loads
113+
// We put this at the very end of the head (after the delimiter comment) because it doesn't matter if it's expunged on subsequent loads
114114
let initial_state = format!("window.__PERSEUS_INITIAL_STATE = `{}`;", initial_state);
115115
self.scripts_after_boundary.push(initial_state.into());
116+
// But we'll need the global state as a variable until a template accesses it, so we'll keep it around (even though it should actually instantiate validly and not need this after the initial load)
116117
let global_state = format!("window.__PERSEUS_GLOBAL_STATE = `{}`;", global_state);
117-
self.scripts_after_boundary.push(global_state.into());
118+
self.scripts_before_boundary.push(global_state.into());
118119
// Interpolate the document `<head>` (this should of course be removed between page loads)
119120
self.head_after_boundary.push((&page_data.head).into());
120121
// And set the content

packages/perseus/src/shell.rs

+8-7
Original file line numberDiff line numberDiff line change
@@ -295,19 +295,20 @@ pub async fn app_shell(
295295
checkpoint("initial_state_present");
296296
// Unset the initial state variable so we perform subsequent renders correctly
297297
// This monstrosity is needed until `web-sys` adds a `.set()` method on `Window`
298+
// We don't do this for the global state because it should hang around uninitialized until a template wants it (if we remove it before then, we're stuffed)
298299
Reflect::set(
299300
&JsValue::from(web_sys::window().unwrap()),
300301
&JsValue::from("__PERSEUS_INITIAL_STATE"),
301302
&JsValue::undefined(),
302303
)
303304
.unwrap();
304-
// Also do this for the global state
305-
Reflect::set(
306-
&JsValue::from(web_sys::window().unwrap()),
307-
&JsValue::from("__PERSEUS_GLOBAL_STATE"),
308-
&JsValue::undefined(),
309-
)
310-
.unwrap();
305+
// // Also do this for the global state
306+
// Reflect::set(
307+
// &JsValue::from(web_sys::window().unwrap()),
308+
// &JsValue::from("__PERSEUS_GLOBAL_STATE"),
309+
// &JsValue::undefined(),
310+
// )
311+
// .unwrap();
311312
// We need to move the server-rendered content from its current container to the reactive container (otherwise Sycamore can't work with it properly)
312313
let initial_html = initial_container.inner_html();
313314
container_rx_elem.set_inner_html(&initial_html);

0 commit comments

Comments
 (0)