This version of the documentation is for a version that has not yet been released, and features documented here may not be present in the latest release. You can see the latest stable version of the docs here.

Global state

Like any state management system worth its salt, Perseus also has a global state system, which is managed analogously to templates, through the GlobalStateCreator API. As with templates, you can generate build-time global state, request-time global state, and you can even amalgamate the two! Here's an example of all three working together:

use perseus::{prelude::*, state::GlobalStateCreator};
use serde::{Deserialize, Serialize};

pub fn get_global_state_creator() -> GlobalStateCreator {
    GlobalStateCreator::new()
        .build_state_fn(get_build_state)
        .request_state_fn(get_request_state)
        .amalgamate_states_fn(amalgamate_states)
}

#[derive(Serialize, Deserialize, ReactiveState)]
#[rx(alias = "AppStateRx")]
pub struct AppState {
    pub test: String,
}

// All the below functions can return either `AppState`, or `Result<AppState,
// E>`, where `E` is some error type. For concision, these examples cannot
// return errors. Request state and state amalgamation use `BlamedError`s if
// they're fallible.

// Global state will be generated for each locale in your app (but we don't
// worry about that in this example)
#[engine_only_fn]
async fn get_build_state() -> AppState {
    AppState {
        test: "Hello from the build process!".to_string(),
    }
}

// This will be executed every time there's a request to any page in your app
// (you should avoid doing heavy work here if possible). Note that using *only*
// request-time global state generation, without anything at build-time, would
// prevent your app from accessing global state during the build process, so be
// certain that's what you want if you go down that path.
#[engine_only_fn]
async fn get_request_state(_req: Request) -> AppState {
    AppState {
        test: "Hello from the server!".to_string(),
    }
}

// You can even combine build state with request state, just like in a template!
#[engine_only_fn]
async fn amalgamate_states(build_state: AppState, request_state: AppState) -> AppState {
    AppState {
        test: format!(
            "Message from the builder: '{}' Message from the server: '{}'",
            build_state.test, request_state.test,
        ),
    }
}

Note the definition of a reactive state type AppState (which does not have to be Clone, unlike page state types), and the lack of StateGeneratorInfo: since there are no paths or helper state in the global state system, all you get is a locale (since some apps will need locale-specific global state). As with other state generator functions, these are async and can either be fallible or infallible, at your choice. Due to the lack of revalidation or incremental generation support (neither of which are planned) in global state, the build state generator can never be run at request-time, and therefore returns unblamed errors. To learn more about state generator error handling, see here.

Notice also the use of a get_global_state_creator() function that returns an instance of GlobalStateCreator. This is analogous to the get_template() functions you may be used to.

Using global state

Since it's not provided as an argument to your views, you can access your global state through the reactor, as in this example:

use crate::global_state::AppStateRx;
use perseus::prelude::*;
use sycamore::prelude::*;

// Note that this template takes no state of its own in this example, but it
// certainly could
fn index_page<G: Html>(cx: Scope) -> View<G> {
    // We access the global state through the render context, extracted from
    // Sycamore's context system
    let global_state = Reactor::<G>::from_cx(cx).get_global_state::<AppStateRx>(cx);

    view! { cx,
        // The user can change the global state through an input, and the changes they make will be reflected throughout the app
        p { (global_state.test.get()) }
        input(bind:value = global_state.test)

        a(href = "about", id = "about-link") { "About" }
    }
}

#[engine_only_fn]
fn head(cx: Scope) -> View<SsrNode> {
    view! { cx,
        title { "Index Page" }
    }
}

pub fn get_template<G: Html>() -> Template<G> {
    Template::build("index").view(index_page).head(head).build()
}

The .get_global_state::<T>() method is used, where T is the reactive version of the global state type. Providing the wrong type here will lead to a panic, and you can use .try_get_global_state() instead if you wish. These functions return a reference to the reactive version of your global state, with the lifetime of the page they were executed in, allowing interpolation.

Note that the global state has its own version of the page state store, and is cached in a reactive fashion, meaning updates to it on one page will be preserved on other pages. This makes the global state extremely helpful for storing things like user volume preferences in a music app, which might be changed from any song page.

Pitfalls of request-time global state

Absolutely critically, if you are using only request-time global state, and you use the .get_global_state() method anywhere in your app that uses build state, you will experience panics at build-time. This is because Perseus does not attempt to prevent pages from accessing global state at build-time, even if it won't exist until request-time: policing this is your responsibility. Further, even if you set sensible defaults at build-time and override these at request-time, any pages that only use build state will never see the request-time state until the client-side, which can lead to hydration errors. In general, be cautious when using request-time global state, as improper usage of it abounds, and it can be highly error-prone. If in doubt, avoid it.