Docs

Client helpers

Compile small pure Rust functions into the RSJS browser graph.

Open Markdown

RSJS evaluates a #[rsjs::client] function during the server render, then reruns it in the browser when its result depends on live data. This page covers annotating a helper, path resolution, purity limits, and the data that crosses.

Annotating a helper

Use #[rsjs::client] when a small, portable calculation must produce the same result during SSR and rerun after a browser-visible input changes. Annotate the function definition once. A reachable call creates a symbolic helper edge automatically; the consuming component declares no allowlist.

src/islands/results.rs
mod labels {
    #[rsjs::client]
    pub fn result_label(count: i64) -> String {
        if count == 1_i64 {
            "result".to_string()
        } else {
            "results".to_string()
        }
    }
}

use labels::result_label as label_for_count;

Call the helper through an ordinary Rust path inside an analyzed component:

src/islands/results.rs
use proa_core::{DataLoader, WebContext, WebRenderSync, WriteError};
use proa_macros::html_sync;
use rsjs::{event_handler, rsjs, signal};

pub struct Results;

#[rsjs(component, client)]
impl<L: DataLoader> WebRenderSync<L> for Results {
    fn render(self, cx: &mut WebContext<L>) -> Result<(), WriteError> {
        let count = signal(1_i64);
        let increment = event_handler(|_: rsjs::MouseEvent<rsjs::elem::Button>| {
            count.update(|value| value + 1_i64);
        });

        html_sync! {
            <div>
                <button type="button" on_click={increment()}>"Add"</button>
                <p>{count.get()}" "{label_for_count(count.get())}</p>
            </div>
        }
        .render(cx)
    }
}

The helper evaluates for the server render and becomes part of the browser owner because its result depends on count. The semantic linker resolves label_for_count to its real Rust definition, follows the helper dependency closure, and emits only reachable helpers. The next section covers which path spellings that resolution accepts.

Calling helpers through ordinary Rust paths

Helper calls may use:

Path spelling is not helper identity. Rustc-selected relocation targets give the linker the resolved definition. Authors do not need to replace imported names with canonical paths.

Because the linker resolves identity this way, RSJS removed the component-level clients(...) list:

src/islands/results.rs
// Rejected:
#[rsjs(component, client, clients(crate::labels))]

The supported form is the annotation alone:

src/pricing.rs
#[rsjs::client]
pub fn subtotal(price: i64, quantity: i64) -> i64 {
    price * quantity
}

Then call subtotal(...) through any ordinary resolved Rust path. If that call is unreachable from the final browser graph, neither the function nor its helper dependencies ship. Modules group helpers without changing that rule, as the next section shows.

Grouping helpers in a module

#[rsjs::client_module] remains available for bundling or namespace-level validation. It is optional, and it does not make every function eligible. Each portable helper still carries its own #[rsjs::client] annotation.

src/pricing.rs
#[rsjs::client_module]
pub mod pricing {
    #[rsjs::client]
    pub fn subtotal(price: i64, quantity: i64) -> i64 {
        price * quantity
    }

    #[rsjs::client]
    pub fn discounted(total: i64, enabled: bool) -> i64 {
        if enabled { total - 10_i64 } else { total }
    }
}

Grouping changes organization and bundle metadata, not reachability: unused helpers in the module still do not become browser code. Every helper in a module obeys the purity limits below.

Keeping helpers pure and portable

A client helper is an origin-free calculation over its parameters and local values. It may use the Rust expressions, control flow, primitive types, collections, and string operations that the RSJS client IR supports.

It cannot:

Use compiler-known handlers, browser capabilities, queries, and mutations for effects. Unsupported helper syntax fails during analysis or linking instead of silently running only on one side. The values you pass into a pure helper carry their own constraints.

Crossing data into a helper

Arguments that reach a client helper are browser-visible and must have a supported client representation. Runtime values that cross through a signal, live component prop, or payload use the rsjs::ClientEncode contract. Prefer small client-shaped inputs such as fixed-width numbers, booleans, short strings, enum tags, ISO timestamps, or explicit projections.

If a helper needs a server-loaded component field, first make the intended crossing explicit:

src/islands/price.rs
let cents = signal(self.cents);

html_sync! {
    <output>{money::format_cents(cents.get())}</output>
}

Do not pass secrets or a large server object to format one label. Keep server-only calculations in ordinary Rust prelude code and render their result through SSR. Some results never need to cross at all.

When not to use a client helper?

If a result never changes in the browser, use a normal Rust function:

src/islands/record_row.rs
let label = format_server_label(self.record);

html_sync! {
    <span>{label}</span>
}

That value remains opaque SSR and adds no helper edge or JavaScript. Reach for #[rsjs::client] only when the calculation must rerun from live browser data. The table below collects every rule on this page.

Rules

RuleWhy
Annotate each portable function once with #[rsjs::client]Eligibility belongs to the definition, not every caller.
Use ordinary imports, aliases, re-exports, and pathsThe linker uses rustc-resolved identity.
Do not add clients(...) to a componentRSJS removed component allowlists.
Keep helpers pure and origin-freeEffects require explicit compiler-known capabilities.
Treat arguments as public browser dataRSJS delivers reachable values to client code.
Keep server-only formatting as ordinary RustStatic results do not need a browser helper.
Let reachability decide emissionUncalled and unreachable helpers do not ship.

Next steps

Search

Type at least 2 characters