Skip to content

Commit

Permalink
feat: added hsr ignoring feature applied by default to unreactive state
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
arctic-hen7 committed Feb 17, 2023
1 parent d297af1 commit 6768f4e
Show file tree
Hide file tree
Showing 7 changed files with 78 additions and 8 deletions.
6 changes: 5 additions & 1 deletion docs/next/en-US/state/browser.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
5 changes: 5 additions & 0 deletions examples/core/rx_state/src/templates/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
9 changes: 5 additions & 4 deletions examples/core/unreactive/src/templates/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -42,6 +40,9 @@ fn head(cx: Scope, _props: IndexPageState) -> View<SsrNode> {
#[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(),
}
}
6 changes: 6 additions & 0 deletions packages/perseus-macro/src/rx_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ pub struct ReactiveStateDeriveInput {
/// `struct` for ease of reference.
#[darling(default)]
alias: Option<Ident>,
/// If specified, the type should be ignored by HSR.
#[darling(default)]
hsr_ignore: bool,

ident: Ident,
vis: Visibility,
Expand Down Expand Up @@ -155,6 +158,7 @@ pub fn make_rx_impl(input: ReactiveStateDeriveInput) -> TokenStream {
vis,
attrs: attrs_vec,
alias,
hsr_ignore,
..
} = input;
let mut attrs = quote!();
Expand Down Expand Up @@ -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 {
Expand Down
10 changes: 10 additions & 0 deletions packages/perseus/src/reactor/global_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,10 @@ impl<G: Html> Reactor<G> {
/// 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<S>(&self) -> Result<Option<S::Rx>, ClientError>
where
Expand All @@ -169,6 +173,12 @@ impl<G: Html> Reactor<G> {
"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
Expand Down
11 changes: 11 additions & 0 deletions packages/perseus/src/reactor/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,10 @@ impl<G: Html> Reactor<G> {
/// 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<S>(
&self,
Expand All @@ -400,6 +404,13 @@ impl<G: Html> Reactor<G> {
!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) => {
Expand Down
39 changes: 36 additions & 3 deletions packages/perseus/src/state/rx_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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
Expand All @@ -113,6 +142,10 @@ pub struct UnreactiveStateWrapper<
// `UnreactiveStateWrapper` as the reactive type
impl<T: Serialize + for<'de> Deserialize<'de> + UnreactiveState + Clone> MakeRx for T {
type Rx = UnreactiveStateWrapper<T>;
// 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)
}
Expand Down

0 comments on commit 6768f4e

Please sign in to comment.