One name for a piece of data, so nobody has to pass it down
There is a moment in every app where props stop being the answer. The signed-in user is needed by the header, by the comment box, and by a button four levels down. You can thread it through every component in between, and everyone who has done that knows how it ends.
shared() gives that data a name. A component that wants it calls the name.
This page is shorter and more careful than the rest of the section, because this is the one part of the authoring surface that this site could not get working on the version you can install today. What is below is the design, plus exactly what happened when we tried it.
The shape of it
A module holds a definition, not an instance:
// session.tsrx
import { computed, shared, state } from '@markless/core';
import { loginUser } from './login.ts';
export const session = shared(() => {
const s = state({ user: null, status: 'anonymous' });
const signedIn = computed(() => s.user !== null);
return {
...s,
signedIn,
async login(creds) {
s.status = 'loading';
s.user = await loginUser(creds);
s.status = 'ready';
},
};
});
A component that needs it calls it:
import Avatar from './avatar.tsrx';
import { session } from './session.tsrx';
export default function Header() @{
const s = session();
@if (s.signedIn) {
<Avatar user={s.user} />
}
}
That call means: resolve this named dataflow instance for the current graph context, and that context is usually the request, the container or the page.
A few consequences worth knowing before you reach for it. There is no provider component to place
and no orphan-provider error to handle, because the boundary is the dataflow rather than a spot in
the component tree. Writing to it inside one container is an ordinary graph write, so the pieces of
the page that read that path change and nothing else does. A shared factory may call another shared
factory, and a cycle between them is rejected at compile time
(MARKLESS_SHARED_DEFINITION_CYCLE). The scope is a second argument when you want something other
than the default: shared(factory, { scope: 'request' }), with request, container and page
the three choices.
So there is no demo box on this page, and there will not be one until that build finishes. Every other widget on this site is a real component doing the real thing, and a fake one here would be worth less than an honest gap.
What to do until then
Pass the data through props. It is more typing and it is not worse code: a prop is a live read of the parent's value, so a value threaded through three components is still one piece of state and still updates only the text that reads it. See components for that mechanism.
For something the whole page needs, lift it to the page component under pages/ and pass it down
from there. For a choice that should survive a reload, storage is a
module-scope binding already, and any component in the module tree can import and read it.
What we can and cannot say about it today
What is certain: the API exists and is exported from @markless/core, the specification describes it in detail, and the compiler carries diagnostics for its failure modes, including cycle detection and an invalid-scope error. What is not established: that a definition in one module resolves from another module. The compiler pass that resolves a shared() call looks for a definition in the same module, and no .tsrx file anywhere in the framework's own repository uses shared() at all, so there is no worked example to copy from. Our two-module test is the one attempt on record, and it did not get past the build.
Coming from another framework?
Whatever you have used before, session() is not run a hook: it is a resolution of a named dataflow instance for the graph context you are already in, and there is no call-order rule attached to it. This is the slot that context and the small global stores usually fill, and the design difference is worth one paragraph. Context makes you create a boundary in the component tree, a provider, and usually a wrapper function to read it: the specification's own rationale notes that about three quarters of context call sites in public code are already hidden behind a hand-written bare-call wrapper, which is the argument for making the bare call the primitive. A store gets closer to that shape but is a separate object graph the framework does not know about, so reading from it costs a selector and a subscription. Here it is meant to be the same reactive graph as everything else, which is what makes a write to it a targeted update rather than a notification.
Try it yourself
If you are on a newer release than 0.3.1, re-run the test above: a module exporting
shared(() => state({ count: 0 })), a second module reading .count, a third writing to it, and
both on one page. If the build finishes and the number moves, this page is out of date and we would
like to know.
That is the end of this section. Next: make a file, get a URL.
