From 6768f4e05dbd0147205d78cb469a988c6d1e545b Mon Sep 17 00:00:00 2001 From: arctic-hen7 Date: Sat, 18 Feb 2023 07:51:58 +1100 Subject: [PATCH] feat: added hsr ignoring feature applied by default to unreactive state This can be easily added to a reactive state with `#[rx(hsr_ignore)]`, which allows more convenient development with state that's often being changed, such as documentation, or the contents of a blog post. --- docs/next/en-US/state/browser.md | 6 ++- examples/core/rx_state/src/templates/index.rs | 5 +++ .../core/unreactive/src/templates/index.rs | 9 +++-- packages/perseus-macro/src/rx_state.rs | 6 +++ packages/perseus/src/reactor/global_state.rs | 10 +++++ packages/perseus/src/reactor/state.rs | 11 ++++++ packages/perseus/src/state/rx_state.rs | 39 +++++++++++++++++-- 7 files changed, 78 insertions(+), 8 deletions(-) diff --git a/docs/next/en-US/state/browser.md b/docs/next/en-US/state/browser.md index 8f790e1d54..e54af8a7fa 100644 --- a/docs/next/en-US/state/browser.md +++ b/docs/next/en-US/state/browser.md @@ -49,8 +49,12 @@ Note that it's perfectly fine for you to write out the full lifetime bounds if y When you're using unreactive state, none of this is necessary, because Perseus just gives you an owned copy of your state to do with as you please, and you don't need `#[auto_scope]` or any special lifetimes. (You can even use a normal `Scope`, which is a white lie to Rust's type system, but it's totally immaterial to the output, so it's a useful elision.) +Note that it's often a good idea to use unreactive state, even if you think you may as well use reactive state, because it gives you the added benefit of automatically being excluded from Perseus' HSR system, which means that, when you change some unreactive state in development, and your browser automatically reloads after rebuilding, that state will be reflected, rather than the old version. If you did this with reactive state, you'd find the old state again, because Perseus would try to be helpful by restoring it. Of course, you could get to the new stuff by just reloading the page, but this is inconvenient for some state types (e.g. the contents of a blog post that you want to preview by continually changing with `perseus serve -w` running). The same effect of HSR ignoring can be achieved by adding `#[rx(hsr_ignore)]` to any reacctive state type in your app, just below `#[rx(alias = "..")]`. This will have no impact on anything other than HSR, and your app will function exactly as you'd expect, just with a little more convenience. Of course, if you're continually changing some reactive state for debugging or the like, you might like to temporarily add this helper macro for your own convenience. At this stage, Perseus does not support excluding single fields from HSR (although this *may* be supported in future if there is sufficient demand). + ## Nested state When you're using the `ReactiveState` derive macro, it's common to want to have some types use nested state, so that you can do something like `state.foo.bar.get()`, rather than `state.foo.get().bar`. This can enable greater flexibility and granularity, and is supported through the `#[rx(nested)]` helper macro, which will assume the type of the field it annotates has had `ReactiveState` derived on it. -If you want to use some more complex types of nested state, such as nested `Vec`s or `HashMap`s, take a look at [this module](=state/rx_collections), and enable the `rx-collections` feature flag on Perseus. +If you want to use some more complex types of nested state, such as nested `Vec`s or `HashMap`s, take a look at [this module](=state/rx_collections@perseus), and enable the `rx-collections` feature flag on Perseus. + +Note that it's not necessary to specify `#[rx(hsr_ignore)]` on all nested state types when you want to exclude one state from HSR, you can just specify it at the top-level. Asking Perseus to ignore a nested field from HSR will have no effect, as it only checks the top level during HSR thawing. diff --git a/examples/core/rx_state/src/templates/index.rs b/examples/core/rx_state/src/templates/index.rs index e3b2344e6d..e376cfad82 100644 --- a/examples/core/rx_state/src/templates/index.rs +++ b/examples/core/rx_state/src/templates/index.rs @@ -18,6 +18,11 @@ fn index_page<'a, G: Html>(cx: BoundedScope<'_, 'a>, state: &'a IndexPageStateRx // go to another page, and then come back, *two* elements will have been // added in total. The state is preserved across routes! To avoid this, use // unreactive state. + // + // ADVANCED: Note also that, even if we ignored this page's state type from HSR, + // this would still be performed for HSR, because the state restoration + // process will double-execute this logic. That's why things like this + // should generally be done with suspended state. state .test .modify() diff --git a/examples/core/unreactive/src/templates/index.rs b/examples/core/unreactive/src/templates/index.rs index 50561eedf6..a89dba3a3d 100644 --- a/examples/core/unreactive/src/templates/index.rs +++ b/examples/core/unreactive/src/templates/index.rs @@ -2,11 +2,9 @@ use perseus::prelude::*; use serde::{Deserialize, Serialize}; use sycamore::prelude::*; -// Without `#[make_rx(...)]`, we have to manually derive `Serialize` and -// `Deserialize` -// We derive `UnreactiveState` too, which actually creates a pseudo-reactive +// We derive `UnreactiveState`, which actually creates a pseudo-reactive // wrapper for this unreactive type, allowing it to work with Perseus; -// rather strict state platform (this is just a marker trait though) +// rather strict state platform (this is just a marker trait though). #[derive(Serialize, Deserialize, Clone, UnreactiveState)] struct IndexPageState { pub greeting: String, @@ -42,6 +40,9 @@ fn head(cx: Scope, _props: IndexPageState) -> View { #[engine_only_fn] async fn get_build_state(_info: StateGeneratorInfo<()>) -> IndexPageState { IndexPageState { + // Unreactive state is automatically excluded from HSR, so changing + // this will update it in your browser in development, without state + // restoration getting in your way greeting: "Hello World!".to_string(), } } diff --git a/packages/perseus-macro/src/rx_state.rs b/packages/perseus-macro/src/rx_state.rs index 07441eb87c..872d91ab01 100644 --- a/packages/perseus-macro/src/rx_state.rs +++ b/packages/perseus-macro/src/rx_state.rs @@ -11,6 +11,9 @@ pub struct ReactiveStateDeriveInput { /// `struct` for ease of reference. #[darling(default)] alias: Option, + /// If specified, the type should be ignored by HSR. + #[darling(default)] + hsr_ignore: bool, ident: Ident, vis: Visibility, @@ -155,6 +158,7 @@ pub fn make_rx_impl(input: ReactiveStateDeriveInput) -> TokenStream { vis, attrs: attrs_vec, alias, + hsr_ignore, .. } = input; let mut attrs = quote!(); @@ -206,6 +210,8 @@ pub fn make_rx_impl(input: ReactiveStateDeriveInput) -> TokenStream { impl ::perseus::state::MakeRx for #ident { type Rx = #intermediate_ident; + #[cfg(debug_assertions)] + const HSR_IGNORE: bool = #hsr_ignore; fn make_rx(self) -> Self::Rx { use ::perseus::state::MakeRx; Self::Rx { diff --git a/packages/perseus/src/reactor/global_state.rs b/packages/perseus/src/reactor/global_state.rs index a3cc2b3998..fa2a6b9bab 100644 --- a/packages/perseus/src/reactor/global_state.rs +++ b/packages/perseus/src/reactor/global_state.rs @@ -155,6 +155,10 @@ impl Reactor { /// the thaw preferences have already been accounted for. /// /// This assumes that the app actually supports global state. + /// + /// In HSR, this will thaw leniently, and, if the type is explicitly being + /// ignored from HSR, this will return `Ok(None)` (e.g. for unreactive state + /// types). #[cfg(any(client, doc))] fn get_frozen_global_state_and_register(&self) -> Result, ClientError> where @@ -169,6 +173,12 @@ impl Reactor { "attempted to invoke hsr-style thaw in non-hsr environment" ); + // If this is an HSR thaw, and this type is to be ignored from HSR, then ignore + // it + if *is_hsr && S::HSR_IGNORE { + return Ok(None); + } + match &frozen_app.global_state { FrozenGlobalState::Some(state_str) => { // Deserialize into the unreactive version diff --git a/packages/perseus/src/reactor/state.rs b/packages/perseus/src/reactor/state.rs index 560a88f133..9d2f0afadb 100644 --- a/packages/perseus/src/reactor/state.rs +++ b/packages/perseus/src/reactor/state.rs @@ -383,6 +383,10 @@ impl Reactor { /// Attempts to extract the frozen state for the given page from any /// currently registered frozen app, registering what it finds. This /// assumes that the thaw preferences have already been accounted for. + /// + /// In HSR, this will thaw leniently, and, if the type is explicitly being + /// ignored from HSR, this will return `Ok(None)` (e.g. for unreactive state + /// types). #[cfg(any(client, doc))] fn get_frozen_state_and_register( &self, @@ -400,6 +404,13 @@ impl Reactor { !is_hsr, "attempted to invoke hsr-style thaw in non-hsr environment" ); + + // If this is an HSR thaw, and this type is to be ignored from HSR, then ignore + // it + if *is_hsr && S::HSR_IGNORE { + return Ok(None); + } + // Get the serialized and unreactive frozen state from the store match frozen_app.state_store.get(&url) { Some(state_str) => { diff --git a/packages/perseus/src/state/rx_state.rs b/packages/perseus/src/state/rx_state.rs index e65af41c21..7d0fdb026e 100644 --- a/packages/perseus/src/state/rx_state.rs +++ b/packages/perseus/src/state/rx_state.rs @@ -12,6 +12,32 @@ pub trait MakeRx { /// unreactive, meaning greater inference and fewer arguments that the /// user needs to provide to macros. type Rx: MakeUnrx; + /// This should be set to `true` to have this type be ignored by the hot + /// state reloading system, which can be useful when working with state + /// that you will regularly update in development. + /// + /// For example, take a documentation website that converts Markdown to HTML + /// in its state generation process, storing that in reactive state. If you, + /// in development, then wanted to change some of the documentation and see + /// the result immediately, HSR would override the new state with the + /// old, preserving your 'position' in the development cycle. Only a + /// manual reload of the page would prevent this from continuing + /// forever. Setting this associated constant to `true` will tell the + /// HSR system to ignore this type from HSR thawing, meaning + /// the new state will always be used. Note that, depending on the structure + /// of your application, this can sometimes cause state inconsistencies + /// (e.g. if one state object expects another one to be in a certain + /// state, but then one of them is ignored by HSR and resets). + /// + /// This is a development-only setting, and does not exist in production. If + /// the `hsr` feature flag is disabled, this will have no effect. + /// + /// Typically, you would set this using the `#[rx(hsr_ignore)]` derive + /// helper macro with `#[derive(ReactiveState, ..)]`. Note that you only + /// need to set this to `true` for the top-level state type for a page, + /// not for any nested components. + #[cfg(debug_assertions)] + const HSR_IGNORE: bool = false; /// Transforms an instance of the `struct` into its reactive version. fn make_rx(self) -> Self::Rx; } @@ -88,9 +114,12 @@ impl std::fmt::Debug for (dyn AnyFreeze + 'static) { } /// A marker trait for types that you want to be able to use with the Perseus -/// state platform, without using `#[make_rx]`. If you want to use unreactive -/// state, implement this, and you'll automatically be able to use your -/// unreactive type without problems! +/// state platform, without using `#[derive(ReactiveState, ..)]`. If you want to +/// use unreactive state, implement this, and you'll automatically be able to +/// use your unreactive type without problems! +/// +/// Since unreactive state will never be changed on the client-side, it is +/// automatically ignored y the hot state reloading system, if it is enabled. pub trait UnreactiveState {} /// A wrapper for storing unreactive state in Perseus, and allowing it to @@ -113,6 +142,10 @@ pub struct UnreactiveStateWrapper< // `UnreactiveStateWrapper` as the reactive type impl Deserialize<'de> + UnreactiveState + Clone> MakeRx for T { type Rx = UnreactiveStateWrapper; + // Unreactive state will never change on the client-side, and should therefore + // by default be ignored by HSR + #[cfg(debug_assertions)] + const HSR_IGNORE: bool = true; fn make_rx(self) -> Self::Rx { UnreactiveStateWrapper(self) }