Skip to content

Sphere and lifetime

A sphere is the unit of reactive lifetime. Every stock, memo, effect, callback, action, and resource belongs to the sphere it was created in.

If you are using the JS bridge, you never create or clear one yourself. Opening a pier creates a sphere, reading a hail creates a sphere, and unmounting the component clears them. The adapter does it.

So read this page for the model, not for an API you have to call. What matters day to day is one sentence: state lives as long as the scope it was created in, and that scope is your provider.

The rest of this page is the machinery underneath: useful if you are writing your own integration, or debugging why something outlived what you expected.

let (id, result) = make_sphere(parent_id, || {
let state = Stock::new(State::default());
provide_context(state);
state
});

Everything created inside the closure is owned by the new sphere. You get back its id and whatever the closure returned.

make_top_sphere() creates an empty sphere with no parent, for a root.

This is what PierProvider calls for you.

clear_sphere(id);

That frees every state the sphere owns. You never free individual values, and with an adapter, you do not call this either: it runs from the framework’s own cleanup hook when your provider unmounts.

Two properties make this safe to wire into a UI framework:

  • It cascades. Spheres form a parent-child tree, and clearing one clears its whole subtree.
  • It is idempotent and order-independent. Clearing an already-cleared sphere does nothing.

Together these mean a host can register one clear_sphere per component and stay correct no matter which order things unmount in. Whether a child clears itself first or the parent’s cascade reaches it first, the second call finds nothing to do.

The parent link also drives context lookup.

provide_context(Stock::new(State::default()));
let state = use_context::<Stock<State>>().unwrap();

use_context walks up the parent chain and returns None if no ancestor provided that type. Context is keyed by type, and the nearest one wins.

Because it is keyed by type, two values of the same type collide. Wrap them:

#[derive(Clone, Copy)]
struct PanelTitle(Stock<String>);
#[derive(Clone, Copy)]
struct PanelSubtitle(Stock<String>);

Context values must be Clone. Stocks and callbacks are Copy, so in practice you are storing handles, not data.

current_sphere_id() returns the sphere currently being built or run, if any.

if let Some(id) = current_sphere_id() { /* ... */ }

You mostly need this when writing your own integration rather than using an adapter.

batch groups writes into one propagation. batch_with_sphere does the same while entering a specific sphere first: useful when you are writing state from outside any runner.

batch_with_sphere(sphere_id, || {
state.count().set(0);
state.items().write().clear();
});

One last piece: how a derived stock finds its way back to the root, and the one knob you might want to turn. See the getter pipeline.