Skip to content

Commit

Permalink
docs(book): documented idb state system
Browse files Browse the repository at this point in the history
  • Loading branch information
arctic-hen7 committed Jan 25, 2022
1 parent 362d5ca commit 68a467c
Show file tree
Hide file tree
Showing 3 changed files with 35 additions and 2 deletions.
18 changes: 18 additions & 0 deletions docs/next/en-US/reference/state/freezing.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,21 @@ Recovering your app's state from a frozen state is called *thawing* in Perseus (
One important thing to understand about thawing though is how Perseus decided what state to use for a template, because there can be up to three options. Every template that accepts state will have generated state that's provided to it from the generation proceses on the server, but there could also be a frozen state and an active state (some state that's already been made reactive). The server-generated state is always the lowest priority, and it will be used if no active or frozen state is available. However, deciding between frozen and active state is more complicated. If only one is available, it will of course be used, but it both are available, the choice is yours. You can represent this choice through the `ThawPrefs` `struct`, which must be provided to a call to `.thaw()` as the second argument. This has two fields, one for page state, and another for global state. For global state, you can set the `global_prefers_frozen` field to `true` if you want to override active global state with a frozen one. For page state, you'll use `PageThawPrefs`, which can be set to `IncludeAll` (all pages will prefer frozen state), `Include(Vec<String>)` (the listed pages will prefer frozen state, all others will prefer active state), or `Exclude(Vec<String>)` (the listed pages will prefer active state, all others will prefer frozen state). There's no `ExcludeAll` option because that would defeat the entire purpose of thawing.

It may at first be tempting to use `IncludeAll`, but this is an important UX decision that you should consider carefully. Using frozen state when active state isn't available is automatic, but preferring frozen state *over* active state translates to something like this: a user does some stuff, then state is thawed, everything they did at the start is gone and replaced with whatever they did in the previous session. This might be entirely reasonable in pages that can only be accessed after thawing is complete, but in pages that are accessible at all times, this could be extremely irritating to your users!

<details>
<summary>Thawing isn't working...</summary>

It may seem sometimes like thawing has completely failed, and this is usually for one of two reasons.

1. You're extracting the state of another page.
2. You're getting the global state without having it as the second argument to your template function. (In other words, you're getting it manually through `perseus::get_render_ctx!().global_state.borrow()`).

In the first case, the reasoning is simple. Statw thawing is a gradual process, so the state for a page won't be thawed until the user actually visits that page. This is why it's much better to use global state for state that needs to be shared between pages, and you should generally avoid extracting state from other pages.

In the second case, the reason is similar. When you get the global state directly in this way, you bypass the thawing process altogether, meaning thawed state won't show up. If you need to access the global state, you should do it by making it the second argument to your template function (as documented [here](:reference/state/global)).

*Note: in a future version of Perseus, thawing logic may be moved so that direct access does become possible, but it's currently not.*

</details>

*Note: currently, Perseus' thawing process requires navigating to another page to work due to problems in Sycamore's router. These will be resolved in some form before v0.3.3.*
18 changes: 17 additions & 1 deletion docs/next/en-US/reference/state/idb-freezing.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,20 @@ One of the most common places to store frozen state is inside the browser, which

To use this system, you'll need to enable the `idb-freezing` feature flag, and then you can use the system as per the below example.

*Further documentation on this system will be written after it's been built, which it will be by v0.3.3. Sorry for the wait!*
# Example

The following code is taken from [here](https://github.com/arctic-hen7/perseus/tree/main/examples/rx_state/src/idb.rs).

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

This example is very contrived, but it illustrates the fundamentals of freezing and thawing to IndexedDB. You'll need to perform most of this logic in a `wasm_bindgen_futures::spawn_local()`, a function that spawns a future in the browser, because the IndexedDB API is asynchronous (so that costly DB operations don't block the main UI thread). The first button we have in this example has its `on:click` handler set to one of these futures, and it then freezes the state, initializes the database (which will either create it or open it if it already exists), and then calls `.set()` to set the new frozen state (which will remove previously stored frozen states in the background). The rest of the code here is just boilerplate for reporting successes or failures to the user.

Notably, the operations you'll perform through `IdbFrozenStateStore` are all fallible, they can all return an `Err`. These cases should be handled carefully, because there are a myriad number of causes (filesystem errors in the browser, invalid data, etc.). Perseus tries to shield you from these as much as possible, but you should be wary of potentially extremely strange errors when working with IndexedDB (they should be very rare though). If your app experiences an error, it's often worth retrying the operation once to see if it works the second time. If you're having trouble in local development, you should use your browser's developer tools to delete the `perseus` database.

As for thawing, the process is essentially the same, except in reverse, and it should be noted that the `.thaw()` method is fallible, while the `.freeze()` method is not. This is due to the potential issues of accepting a frozen state of unknown origin.

One thing that may seem strange here is that we get the render context outside the click handlers. The reason for this is that the render context is composed almost entirely of `Signal`s and the like, so once you have one instance, it will update. Further, we actually couldn't get the render context in the futures even if we tried, since once we go into the future, we decouple from Sycamore's rendering system, so the context no longer exists as far as it's concerned. We can work around this, but for simplicity it's best to just get the render context at the beginning and use it later.

It's also important to understand that we don't freeze straight away, but only when the user presses the button, since the result of `.freeze()` is an unreactive `String`, which won't update with changes to our app's state.
1 change: 0 additions & 1 deletion examples/rx_state/src/idb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ pub struct TestProps {
pub username: String,
}

// This special macro (normally we'd use `template(IndexProps)`) converts the state we generate elsewhere to a reactive version
#[perseus::template_rx(IdbPage)]
pub fn idb_page(TestPropsRx { username }: TestPropsRx) -> View<G> {
let username_2 = username.clone(); // This is necessary until Sycamore's new reactive primitives are released
Expand Down

0 comments on commit 68a467c

Please sign in to comment.