Skip to content

Commit

Permalink
docs(book): wrote docs on reactive state
Browse files Browse the repository at this point in the history
  • Loading branch information
arctic-hen7 committed Jan 22, 2022
1 parent 9ee419e commit f5a7fbd
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 0 deletions.
3 changes: 3 additions & 0 deletions docs/next/en-US/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
- [Incremental Generation](/docs/reference/strategies/incremental)
- [State Amalgamation](/docs/reference/strategies/amalgamation)
- [Hydration](/docs/reference/hydration)
- [Reactive State](/docs/reference/state/rx)
- [Global State](/docs/reference/state/global)
- [State Freezing](/docs/reference/state/freezing)
- [CLI](/docs/reference/cli)
- [Ejecting](/docs/reference/ejecting)
- [Snooping](/docs/reference/snooping)
Expand Down
21 changes: 21 additions & 0 deletions docs/next/en-US/reference/state/freezing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# State Freezing

If you use the reactive and global state systems to their full potential, your entire app can be represented as its state. So what if you could make all that state unreactive again, serialize it to a string, and keep it for later? Well, you'd be able to let your users pick up at the *exact* same place they were when they come back later. Imagine you're in the middle of filling out some forms and then your computer crashes. You boot back up and go to the website you were on. If it's built with Perseus and state freezing occurred just before the crash, you're right back to where you were. Same page, same inputs, same everything.

Specifically, Perseus achieves this by serializing the global state and the page state store, along with the route that the user's currently on. You can invoke this easily by running `.freeze()` on the render context, which you can access with `perseus::get_render_ctx!()`. Best of all, if state hasn't been used yet (e.g. a page ahsn't been visited), it won't be cached, because it doesn't need to be. That also applies to global state, meaning the size of your frozen output is minimized.

## Example

You can easily imperatively instruct your app to freeze itself like so (see [here](https://github.com/arctic-hen7/perseus/tree/main/examples/rx_state/src/index.rs)):

```rust
{{#include ../../../../examples/rx_state/src/index.rs}}
```

## Thawing

Recovering your app's state from a frozen state is called *thawing* in Perseus (basically like hydration for state, but remember that hydration is for views and thawing is for state!), and it's completely automatic. Once you give Perseus a way to get your frozen state, it'll check when your app loads up and thaw as necessary.

*The process of telling Perseus how to get frozen state is still in development, but it'll be ready for v0.3.3!*

TODO
31 changes: 31 additions & 0 deletions docs/next/en-US/reference/state/global.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Global State

As you've seen, Perseus has full support for reactive state in templates, but what about state that's not associated with any template? The usual example is something like dark mode, which the user might manually disable. In most JavaScript frameworks, you'd bring in some bloated state management system to handle this, but Perseus has global state built in. To declare it, you create a `GlobalStateCreator`, which will be used to generate some state, and then that'll be made reactive and passed to your templates as their second argument (if they have one, and you'll have to use the `#[template_rx(...)]` macro).

The essence of global state is that you can generate it at build-time (though with something like setting dark mode, you'll probably want to ignore whatever was set at build time until you know the browser's preferences) and access it seamlessly from any template. Just like usual [reactive state](:reference/state/rx), you can make it reactive with `#[make_rx(...)]`, and you essentially get app-wide MVC with just a few lines of code (and no extra dependencies, all this is completely built into Perseus).

## Example

All the following examples are taken from [here](https://github.com/arctic-hen7/perseus/blob/main/examples/rx_state).

To being with, you'll need to set up a `GlobalStateCreator`, which will look something like this (it's supposed to be fairly similar to the process of generating state for a `Template`, but it currently only supports build-time state generation):

```rust
{{#include ../../../../examples/rx_state/src/global_state.rs}}
```

Then, you can tell Perseus about that by adding it to `define_app!` like so:

```rust
{{#include ../../../../examples/rx_state/src/lib.rs}}
```

Finally, you can use it like so (note the second argument to `index_page`):

```rust
{{#include ../../../../examples/rx_state/src/index.rs}}
```

## Potential Issues

Global state has a quirk that shouldn't be an issue for most, but that can be very helpful to know about if you start to dig into the internals of Perseus. Global state is passed down from the server as a window-level JS variable (as with template state), but it doesn't immediately get deserialized and registered, it's loaded lazily. So, if the user loads fifty templates that don't access global state, your app won't initialize the global state. But, the moment you take it as an argument to a template, it will be set up. This means that, while you can access the global state through the render context (with `perseus::get_render_ctx!()`), you shouldn't do this except in templates that already take the global state as an argument. It may seem tempting to assume that the user has already gone to another page which has set up global state, but no matter how the flow of your app works, you mustn't assume this because of [state freezing](:reference/state/freezing), which can break such flows. Basically, don't access the global state through the render context, you almost never need to and it may be wrong. Trust in `#[template_rx(...)]`.
31 changes: 31 additions & 0 deletions docs/next/en-US/reference/state/rx.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Reactive State

Since v0.3.3, Perseus added support for *reactive state*. Up until now, all our templates have generated state to create one or more pages, and then they've simply used that state to render some stuff. However, in reality, we'll have much more complex data models that involve user interaction. For example, we might have inputs on a page that might change aspects of the view displayed to the user. This used to be done with Sycamore's reactivity system on its own, but Perseus now provides a mechanism to make the state you provide to templates automatically reactive. That means every single property becomes reactive.

Just annotate a state `struct` with `#[perseus::make_rx(RxName)]`, where `RxName` is the name of the new reactive `struct` (e.g. `IndexState` might become `IndexStateRx`). This macro wraps every single property in your `struct` in a `Signal` and produces a new reactive version that way, implementing `perseus::state::MakeRx` on the original to provide a method `.make_rx()` that can be used to convert from the unreactive version to the reactive one (there's also the reverse through `perseus::state::MakeUnrx`, which is implemented on the new, reactive version). If you have fields on your `struct` that are themselves `struct`s, you'll need to nest that reactivity, which you can do by adding `#[rx::nested("field", FieldRxName)]` just underneath the `#[make_rx(...)]` macro, providing it the name of the field and the type of the reactive version (which you'd generated with `#[make_rx(...)]`). Notably, `#[make_rx(...)]` automatically derives `Serialize`, `Deserialize`, and `Clone` on your `struct` (so don't derive them yourself!).

*Note: Sycamore has a proposal to support fine-grained reactivity like this through observers, which will supersede this when they're released, and they'll make all this even faster! Right now, everything has to be cloned unfortunately.*

Once you've got some reactive versions of your state `struct`s ready, you should generate the unreactive versions as usual, but then set the first argument on your template function to the reactive version. This requires Perseus to convert between the unreactive and reactive versions in the background, which you can enable by changing `#[template(...)]` to `#[template_rx(...)]` and removing the Sycamore `#[component]` annotation (this is added automatically by `#[template_rx(...)]`). Behind the scenes, you've just enabled the world's most powerful state platform, and not only will your state be made reactive for you, it will be added to the *page state store*, a global store that enables Perseus to cache the state of a page. So, if your users start filling out forms on page 1 and then go to page 2, and then come back to page 1, their forms will be just how they left them. (Not sure about you, but it feels to us like it's about time this was the default on the web!)

You may be wondering what the benefits of having a reactive state are though. Well, the intention is this: every possible state your page can be in should be representable in your state. That means that, whenever you'd usually declare a new variable in a `Signal` to handle some state, you can move it into your template's state and handle it there instead, making things cleaner and taking advantage of Perseus' state caching system.

## Example

This can all be a bit hard to imagine, so here's how it looks in practice with a simple state involving a `username` that the user can type in, and then it'll be displayed back to them. You can see the source [here](https://github.com/arctic-hen7/perseus/blob/main/examples/rx_state/src/index.rs). Note that this example also uses [global state](:reference/state/global), which is documented in the next chapter, but you can ignore everything except that first `p` and `input` for now.

```rust
{{#include ../../../../examples/rx_state/src/index.rs}}
```

The only unergonomic thing here is that we have to `.clone()` the `username` so that we can both `bind:value` to it and display it. Note that this will be made unnecessary with Sycamore's new reactive primitives (which will be released soon).

## Accessing Another Page's State

Because every template that uses this pattern will have its state added to a special *page state store*, you can actually access the state of another page quite easily. However, you must be careful doing this, because the other page's state will only be available if it's been loaded by the user. On the server, every page is loaded in its own little silo to prevent corruption, so no other page will ever have been 'loaded'. As for in the browser, you might design an app in which it's only possible to get to a certain page by going through another, but you still can't assume that that page has been loaded, because [state freezing](:reference/state/freezing) can let a user pick up from any page in your app, and such special rendering flows will be shattered.

All that said, you can access another page's state like so (see [here](https://github.com/arctic-hen7/perseus/blob/main/examples/rx_state/src/about.rs)):

```rust
{{#include ../../../../examples/rx_state/src/about.rs}}
```

0 comments on commit f5a7fbd

Please sign in to comment.