Skip to content
Mercury
English
Design Book
Menu

Reflect.3. Keeping scopes separate

Two copies of a form may use the same model without sharing values. A Provider selects the scope; Reflect reads its stores and binds its actions there.

Reflect 4.1.1 · Interactive exercise

Open the exercise on its own page

Give the application ownership

Declare the model once. Create a scope for each independent workspace or request, then render under Provider from @fiberfx/hypergraph/hyperscript. Reflect uses that existing context; it does not create a second scope system.

import {createElement as h} from "@fiberfx/hyperscript";
import {event, store, fork, dispose, scopeBind} from "@fiberfx/hypergraph";
import {Provider, setScopeMode} from "@fiberfx/hypergraph/hyperscript";
import {reflect} from "@fiberfx/reflect";

setScopeMode("required");
const added = event<void>();
const reset = event<void>();
const $count = store(0).on(added, n => n + 1).reset(reset);
const Counter = reflect({
  view: (p: {name: string; count: number; add: () => void; clear: () => void}) =>
    h("section", {"aria-label": p.name},
      h("output", null, String(p.count)),
      h("button", {onClick: p.add}, "Add"),
      h("button", {onClick: p.clear}, "Reset")),
  bind: {count: $count, add: () => added(), clear: () => reset()},
});
const left = fork();
const right = fork();
export const Example = () => h("div", null,
  h(Provider, {value: left}, h(Counter, {name: "Left"})),
  h(Provider, {value: right}, h(Counter, {name: "Right"})),
);
export const readLeft = () => scopeBind(() => $count.getState(), {scope: left})();
// The owner calls this after unmounting Example:
export const releaseScopes = () => { dispose(left); dispose(right); };

The example makes ownership explicit: the application creates and releases both scopes. Unmount views before disposing their scopes so cleanup hooks can still run in a live scope. In a long-lived application, tie this ownership to the actual workspace lifetime.

setScopeMode("required") makes the shared binding refuse missing Provider context rather than silently reading global state. It does not create a Provider for you. Outside a render, scopeBind is an explicit way to inspect one scope. See scopes for the model API.

Work in two independent panels

Open the scopes experiment. Change the left name and add one twice. Then add one on the right. You should have different names and counts 2 and 1.

Use the left panel's Reset exercise button. This is a scoped model reset, unlike the footer's Restart the whole exercise, which recreates the exercise stage. Make a prediction for the right panel before pressing either.

Read the untouched side too

After the left reset, expect left count 0 and right count 1. The right name should remain unchanged. A test should also read the global store and confirm it stayed at its initial value; the browser panels alone do not prove that third condition.

The bound callback remembers its originating scope. If you pass it into a child under another Provider, it does not migrate to that child's scope. A child that calls its own useUnit obtains actions for the Provider where that child renders. Replacing a Provider creates new bindings for its descendants.

Expose a missing boundary

For a negative test, render a reflected component without a Provider while required mode is enabled. The shared binding should refuse that render. Do this in an isolated test, not by breaking the working lab.

A raw event called outside a scoped callback is another boundary to inspect: it does not become scoped merely because the page happens to contain Providers. Keep event invocation behind bind, useUnit, or an explicit scopeBind. Do not promise that arbitrary foreign asynchronous callbacks retain context; the Reflect suite specifically covers awaiting a Hypergraph effect.

Choose the reset you mean

First reset only one panel and confirm the other retains its data. Then restart the whole exercise and confirm both start again. In your own application, separate “clear this form” from “close this workspace”; one changes model values, while the other ends owned resources.

Repeat the sequence with the right side first. Symmetry helps expose an accidental global callback that a one-sided test missed.

Isolate a second editor

Render two editors for different documents from one model. Give each its own scope and bind its Save action through Reflect. Test the non-target editor as carefully as the target editor.

Keep request-specific scopes out of module-level shared server state. Create one per request and release it at the owner's boundary. The next chapter adds collections inside a scope; a scope separates workspaces, while a key separates rows within one workspace.

Previous · Course · Next