@blueshed/railroad 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,86 @@
1
+ # Railroad — Skill for Claude
2
+
3
+ You are helping a developer build a UI with `@blueshed/railroad`, a micro reactive framework for Bun.
4
+
5
+ ## What Railroad Is
6
+
7
+ - ~400 lines. Zero dependencies. Real DOM — no virtual DOM, no compiler, no build step.
8
+ - Designed for Bun. TypeScript source files are the distribution (no transpile step).
9
+ - Four concerns: **signals** (state), **JSX** (DOM), **routes** (navigation), **shared** (dependency injection).
10
+
11
+ ## When to Use This Skill
12
+
13
+ Use railroad when the developer has it installed (`@blueshed/railroad` in package.json) or asks to build a UI with signals and JSX for Bun. Do NOT mix railroad with React, Preact, Solid, or any other framework — they are incompatible.
14
+
15
+ ## Critical Setup
16
+
17
+ ### Automatic runtime (recommended)
18
+
19
+ No JSX imports needed — the compiler inserts them automatically.
20
+
21
+ ```json
22
+ {
23
+ "compilerOptions": {
24
+ "jsx": "react-jsx",
25
+ "jsxImportSource": "@blueshed/railroad"
26
+ }
27
+ }
28
+ ```
29
+
30
+ ### Classic runtime
31
+
32
+ If the project uses explicit imports, every `.tsx` file that uses JSX **must** import `createElement` (and `Fragment` if using `<>...</>`):
33
+
34
+ ```json
35
+ {
36
+ "compilerOptions": {
37
+ "jsx": "react",
38
+ "jsxFactory": "createElement",
39
+ "jsxFragmentFactory": "Fragment"
40
+ }
41
+ }
42
+ ```
43
+
44
+ Check the project's `tsconfig.json` to see which mode is in use. Prefer the automatic runtime for new projects.
45
+
46
+ A minimal Bun server to serve the app:
47
+
48
+ ```ts
49
+ import home from "./index.html";
50
+ Bun.serve({ routes: { "/": home } });
51
+ ```
52
+
53
+ ## How to Import
54
+
55
+ ```ts
56
+ // Everything from one place:
57
+ import { createElement, Fragment, signal, computed, effect, batch, text, when, list, routes, navigate, key, provide, inject } from "@blueshed/railroad";
58
+
59
+ // Or by concern:
60
+ import { signal, computed, effect, batch } from "@blueshed/railroad/signals";
61
+ import { createElement, Fragment, text, when, list } from "@blueshed/railroad/jsx";
62
+ import { routes, navigate, route, matchRoute } from "@blueshed/railroad/routes";
63
+ import { key, provide, inject } from "@blueshed/railroad/shared";
64
+ ```
65
+
66
+ ## Reference Docs
67
+
68
+ Detailed usage for each concern is in the sibling files. Read them before generating railroad code:
69
+
70
+ - `signals.md` — reactive state: signal, computed, effect, batch, dispose, mutate, patch
71
+ - `jsx.md` — DOM creation: createElement, Fragment, text, when, list, props, events, refs
72
+ - `routes.md` — hash-based client router: routes, navigate, route, matchRoute
73
+ - `shared.md` — typed dependency injection: key, provide, inject
74
+
75
+ ## Anti-Patterns — Do NOT Do These
76
+
77
+ 1. **No React patterns.** There is no `useState`, `useEffect`, `useRef`, `useCallback`, `useMemo`, or any hooks. There are no lifecycle methods. There is no `React.memo`. Do not import from `react`.
78
+ 2. **No virtual DOM.** `createElement` produces real DOM nodes. There is no reconciler, no diffing (except in `list()`), no re-rendering of component trees.
79
+ 3. **Components run once.** A component function is called once and returns a DOM node. Reactivity comes from signals, not from re-calling the component.
80
+ 4. **No JSX without setup.** In classic mode, every `.tsx` file needs `import { createElement } from "@blueshed/railroad"`. In automatic mode (`react-jsx` + `jsxImportSource`), no import is needed — the compiler handles it. Check tsconfig to know which mode.
81
+ 5. **Do not call `.get()` in JSX children.** Pass the signal directly: `<p>{count}</p>`, not `<p>{count.get()}</p>`. The latter creates a static text node that never updates.
82
+ 6. **Do not use `text()` for attributes.** `text()` creates a DOM text node — use `computed()` for reactive attribute values: `class={computed(() => active.get() ? "on" : "off")}`.
83
+ 7. **Do not create signals inside components** unless you want fresh state on every mount. Module-level signals are shared state; component-level signals are local/ephemeral.
84
+ 8. **Do not forget `batch()`** when setting multiple signals that feed the same effect — without it, the effect runs once per set.
85
+ 9. **Do not rebuild JSX in effects without dispose scopes.** Any effect that does `container.innerHTML = ""; container.appendChild(<JSX/>)` **must** use the full dispose scope pattern — see `jsx.md` Dispose Scopes section. Both the re-run cleanup (`if (childDispose) childDispose()` at top) AND the effect cleanup return (`return () => { ... }`) are required. Without the return, child effects leak when the parent is torn down.
86
+ 10. **Do not use `transition-all` in CSS** for elements near layout boundaries (cards, panels). Use specific properties like `transition-colors` or `transition-[width,border-color]` to avoid animating layout properties unintentionally.
@@ -0,0 +1,325 @@
1
+ # JSX — Real DOM Elements Backed by Signals
2
+
3
+ Railroad's JSX produces real DOM nodes, not a virtual DOM. Components are plain functions called once. Reactivity comes from signals, not re-rendering.
4
+
5
+ ## JSX Setup
6
+
7
+ Railroad supports two JSX modes. Check the project's `tsconfig.json` to know which is in use.
8
+
9
+ ### Automatic runtime (recommended)
10
+
11
+ ```json
12
+ { "jsx": "react-jsx", "jsxImportSource": "@blueshed/railroad" }
13
+ ```
14
+
15
+ No imports needed — the compiler auto-inserts the runtime. Just write JSX:
16
+
17
+ ```tsx
18
+ import { signal } from "@blueshed/railroad";
19
+ const count = signal(0);
20
+ function App() {
21
+ return <div>{count}</div>; // works, no createElement import
22
+ }
23
+ ```
24
+
25
+ ### Classic runtime
26
+
27
+ ```json
28
+ { "jsx": "react", "jsxFactory": "createElement", "jsxFragmentFactory": "Fragment" }
29
+ ```
30
+
31
+ Every `.tsx` file must import `createElement` (and `Fragment` for `<>...</>`):
32
+
33
+ ```tsx
34
+ import { createElement, Fragment } from "@blueshed/railroad";
35
+ ```
36
+
37
+ ## Components
38
+
39
+ A component is a function that receives props and returns a DOM Node. It runs **once** per mount.
40
+
41
+ ```tsx
42
+ function Greeting({ name }: { name: string }) {
43
+ return <h1>Hello {name}</h1>;
44
+ }
45
+
46
+ // Usage:
47
+ <Greeting name="World" />
48
+ ```
49
+
50
+ ### Children
51
+
52
+ Children are passed as part of props:
53
+
54
+ ```tsx
55
+ function Card({ children }: { children: any[] }) {
56
+ return <div class="card">{children}</div>;
57
+ }
58
+
59
+ <Card><p>Content</p></Card>
60
+ ```
61
+
62
+ ### Components Run Once
63
+
64
+ This is the most important thing to understand. Unlike React, the component function body executes once. To make things reactive, use signals:
65
+
66
+ ```tsx
67
+ // WRONG — static, never updates:
68
+ function Counter() {
69
+ let count = 0;
70
+ return <p>{count}</p>; // always shows 0
71
+ }
72
+
73
+ // RIGHT — reactive via signal:
74
+ function Counter() {
75
+ const count = signal(0);
76
+ return (
77
+ <div>
78
+ <p>{count}</p> {/* auto-updates when count changes */}
79
+ <button onclick={() => count.update(n => n + 1)}>+</button>
80
+ </div>
81
+ );
82
+ }
83
+ ```
84
+
85
+ ## Props and Attributes
86
+
87
+ ### Event Handlers
88
+
89
+ Lowercase `on` prefix, directly on the element. The handler name after `on` is lowercased and passed to `addEventListener`.
90
+
91
+ ```tsx
92
+ <button onclick={() => doSomething()}>Click</button>
93
+ <input oninput={(e) => query.set(e.target.value)} />
94
+ <form onsubmit={(e) => { e.preventDefault(); save(); }}>
95
+ ```
96
+
97
+ ### Class
98
+
99
+ Use `class` or `className` — both work. Signals are supported:
100
+
101
+ ```tsx
102
+ <div class="active">static</div>
103
+ <div class={activeClass}>reactive</div> {/* activeClass is a Signal<string> */}
104
+ ```
105
+
106
+ For derived/conditional class values, use `computed()`:
107
+
108
+ ```tsx
109
+ import { computed } from "@blueshed/railroad";
110
+
111
+ <button class={computed(() => `btn ${active.get() ? "active" : ""}`)}>
112
+ ```
113
+
114
+ **Do NOT use `text()` for attributes** — `text()` creates a text DOM node, not a string. Use `computed()` for any reactive attribute value (class, style, data-*, etc.).
115
+
116
+ ### Style
117
+
118
+ Pass a plain object (not a signal) to set inline styles:
119
+
120
+ ```tsx
121
+ <div style={{ color: "red", fontSize: "16px" }}>styled</div>
122
+ ```
123
+
124
+ ### DOM Properties
125
+
126
+ These are set as element properties (not attributes) and support signals: `value`, `checked`, `disabled`, `selected`, `src`, `srcdoc`.
127
+
128
+ ```tsx
129
+ const inputValue = signal("");
130
+ <input value={inputValue} oninput={(e) => inputValue.set(e.target.value)} />
131
+
132
+ const isDisabled = signal(false);
133
+ <button disabled={isDisabled}>Submit</button>
134
+ ```
135
+
136
+ ### innerHTML
137
+
138
+ Set raw HTML (supports signals):
139
+
140
+ ```tsx
141
+ <div innerHTML={htmlSignal} />
142
+ ```
143
+
144
+ ### Ref
145
+
146
+ Get a reference to the underlying DOM element:
147
+
148
+ ```tsx
149
+ <input ref={(el) => el.focus()} />
150
+ ```
151
+
152
+ The ref callback fires immediately after element creation.
153
+
154
+ ### Regular Attributes
155
+
156
+ Anything not matching the above is set via `setAttribute`. Signals auto-update. `false` and `null`/`undefined` remove the attribute.
157
+
158
+ ```tsx
159
+ <a href="/about">About</a>
160
+ <div data-id={itemId}>...</div> {/* itemId can be a Signal */}
161
+ <input aria-label={label} />
162
+ ```
163
+
164
+ ## Signals as Children
165
+
166
+ Pass a signal directly as a child to create a reactive text node:
167
+
168
+ ```tsx
169
+ const count = signal(0);
170
+ <p>Count: {count}</p> // updates automatically
171
+ ```
172
+
173
+ **Do NOT call `.get()` in JSX children** — this evaluates once and creates a static string:
174
+
175
+ ```tsx
176
+ // WRONG — static text, never updates:
177
+ <p>Count: {count.get()}</p>
178
+
179
+ // RIGHT — reactive:
180
+ <p>Count: {count}</p>
181
+ ```
182
+
183
+ ## Null, Boolean, and Undefined Children
184
+
185
+ `null`, `undefined`, `true`, and `false` are ignored (not rendered). Use this for conditional inline content:
186
+
187
+ ```tsx
188
+ <div>{showWarning.peek() && <span>Warning!</span>}</div>
189
+ ```
190
+
191
+ But prefer `when()` for reactive conditionals — see below.
192
+
193
+ ## `text(fn)` — Reactive Computed Text
194
+
195
+ For expressions more complex than a single signal, use `text()`:
196
+
197
+ ```tsx
198
+ import { text } from "@blueshed/railroad";
199
+
200
+ <span>{text(() => count.get() > 5 ? "High" : "Low")}</span>
201
+ <p>{text(() => `${firstName.get()} ${lastName.get()}`)}</p>
202
+ ```
203
+
204
+ `text()` creates a computed signal internally and keeps its text node updated.
205
+
206
+ ## `when(condition, truthy, falsy?)` — Conditional Rendering
207
+
208
+ Swaps DOM nodes when the truthiness of the condition changes.
209
+
210
+ ```tsx
211
+ import { when } from "@blueshed/railroad";
212
+
213
+ // With a signal:
214
+ {when(isLoggedIn, () => <Dashboard />, () => <Login />)}
215
+
216
+ // With a function (becomes a computed internally):
217
+ {when(() => user.get() !== null, () => <Profile />, () => <SignIn />)}
218
+ ```
219
+
220
+ ### Key Behavior
221
+
222
+ - Swaps **only on truthiness transitions** (falsy to truthy, or truthy to falsy).
223
+ - Value changes within the same branch (e.g., user changes from one user to another — both truthy) do **not** re-render the branch.
224
+ - Components inside each branch should read signals to react to value changes.
225
+ - Each branch gets its own dispose scope — effects created inside are cleaned up on swap.
226
+
227
+ ## `list(items, keyFn?, render)` — Keyed List Rendering
228
+
229
+ Renders a reactive list with DOM diffing by key.
230
+
231
+ ```tsx
232
+ import { list, text } from "@blueshed/railroad";
233
+
234
+ const todos = signal([
235
+ { id: 1, text: "Buy milk" },
236
+ { id: 2, text: "Walk dog" },
237
+ ]);
238
+ ```
239
+
240
+ ### Keyed form (recommended)
241
+
242
+ The render function receives **`Signal<T>`** and **`Signal<number>`** — not raw values. When the array updates and an existing key's value changes, the item signal is updated in place, so effects inside the rendered DOM react automatically without recreating the node.
243
+
244
+ ```tsx
245
+ {list(todos, (t) => t.id, (todo, idx) =>
246
+ <li class={computed(() => todo.get().done ? "done" : "")}>
247
+ {text(() => todo.get().text)}
248
+ </li>
249
+ )}
250
+ ```
251
+
252
+ Use `.get()` inside `text()`, `computed()`, or `effect()` to subscribe to item changes. Use `.peek()` for one-off reads.
253
+
254
+ ### Non-keyed form (index-based, raw values)
255
+
256
+ The render function receives the raw item value and index number. Items are recreated when the array changes.
257
+
258
+ ```tsx
259
+ {list(todos, (t, i) => <li>{t.text}</li>)}
260
+ ```
261
+
262
+ ### How It Works
263
+
264
+ - New items: rendered and inserted.
265
+ - Removed items: disposed and removed from DOM.
266
+ - Reordered items: moved in the DOM (not re-created).
267
+ - **Keyed: changed items** update the existing item `Signal`, triggering reactive updates inside the node.
268
+ - Each item gets its own dispose scope.
269
+
270
+ ## `Fragment` — Grouping Without a Wrapper
271
+
272
+ ```tsx
273
+ import { createElement, Fragment } from "@blueshed/railroad";
274
+
275
+ function Columns() {
276
+ return (
277
+ <>
278
+ <td>First</td>
279
+ <td>Second</td>
280
+ </>
281
+ );
282
+ }
283
+ ```
284
+
285
+ ## Dispose Scopes
286
+
287
+ The JSX layer manages cleanup automatically via dispose scopes. When `routes()` or `when()` swap content, all effects created during that content's rendering are disposed. You can also manage scopes manually:
288
+
289
+ ```tsx
290
+ import { pushDisposeScope, popDisposeScope } from "@blueshed/railroad";
291
+
292
+ pushDisposeScope();
293
+ // ... create elements, effects ...
294
+ const dispose = popDisposeScope();
295
+
296
+ // later, clean up everything:
297
+ dispose();
298
+ ```
299
+
300
+ This is **required** when an effect creates JSX that may contain child effects, event handlers, or WebSocket listeners. Every effect that clears a container and appends new JSX **must** use this pattern:
301
+
302
+ ```tsx
303
+ let childDispose: (() => void) | null = null;
304
+
305
+ effect(() => {
306
+ const data = mySignal.get();
307
+ if (childDispose) { childDispose(); childDispose = null; }
308
+ container.innerHTML = "";
309
+ pushDisposeScope();
310
+ container.appendChild(<ChildComponent data={data} /> as Node);
311
+ childDispose = popDisposeScope();
312
+ return () => { if (childDispose) { childDispose(); childDispose = null; } };
313
+ });
314
+ ```
315
+
316
+ ### Why the cleanup return matters
317
+
318
+ The effect handles two lifecycle events:
319
+
320
+ 1. **Re-run** — when a tracked signal changes, the effect re-runs. The `if (childDispose)` call at the top cleans up the previous child scope before creating a new one.
321
+ 2. **Dispose** — when the effect itself is stopped (parent scope torn down, route change, `when()` swap), the cleanup function returned from the effect disposes the child scope. **Without this return, child effects leak when the parent is removed.**
322
+
323
+ ### Common mistake
324
+
325
+ The most frequent porting bug is forgetting dispose scopes in effects that rebuild DOM. Any effect that does `container.innerHTML = ""; container.appendChild(<SomeJSX /> as Node)` needs this pattern. Static JSX with no signals, effects, or event listeners inside does not need it, but when in doubt, wrap it.
@@ -0,0 +1,157 @@
1
+ # Routes — Hash-Based Client Router
2
+
3
+ Railroad uses hash-based routing (`#/path`). The router swaps content in a target element and automatically manages dispose scoping — when a route changes, all effects from the previous route are cleaned up.
4
+
5
+ ## `routes(target, table)` — Declarative Router
6
+
7
+ The main entry point. Pass a DOM element and a route table. Returns a dispose function to stop the router.
8
+
9
+ ```tsx
10
+ import { createElement, routes } from "@blueshed/railroad";
11
+
12
+ const app = document.getElementById("app")!;
13
+
14
+ const dispose = routes(app, {
15
+ "/": () => <Home />,
16
+ "/users/:id": ({ id }) => <UserDetail id={id} />,
17
+ "/settings": () => <Settings />,
18
+ "*": () => <NotFound />,
19
+ });
20
+ ```
21
+
22
+ ### Route Table
23
+
24
+ - Keys are URL patterns (without the `#`).
25
+ - Values are handler functions that receive matched params and return a Node.
26
+ - Patterns are matched in declaration order — first match wins.
27
+ - `*` is a catch-all, checked last.
28
+
29
+ ### Pattern Syntax
30
+
31
+ - **Exact match:** `/about` matches only `#/about`
32
+ - **Params:** `/users/:id` matches `#/users/42` with `{ id: "42" }`
33
+ - **Multiple params:** `/users/:id/posts/:pid` matches `#/users/1/posts/99` with `{ id: "1", pid: "99" }`
34
+ - **Catch-all:** `*` matches anything not matched by other patterns
35
+
36
+ Segments must match exactly in count — `/users/:id` does not match `/users/42/extra`.
37
+
38
+ URI components are automatically decoded (`%20` becomes a space, etc.).
39
+
40
+ ### Automatic Cleanup
41
+
42
+ When the hash changes to a new route:
43
+
44
+ 1. The previous route's dispose scope is called (cleaning up all effects).
45
+ 2. The target element is cleared (`replaceChildren()`).
46
+ 3. A new dispose scope is pushed.
47
+ 4. The new handler runs, creating DOM and effects.
48
+ 5. The dispose scope is popped and stored for next cleanup.
49
+
50
+ If the hash changes but the **same pattern** matches (e.g., `/users/1` to `/users/2`), the route does **not** re-render. The component should use signals or `route()` to react to param changes.
51
+
52
+ ## `navigate(path)` — Programmatic Navigation
53
+
54
+ ```ts
55
+ import { navigate } from "@blueshed/railroad";
56
+
57
+ navigate("/users/42"); // sets location.hash = "/users/42"
58
+ navigate("/"); // go home
59
+ ```
60
+
61
+ Use this in event handlers, after form submissions, etc. It triggers a `hashchange` event which the router picks up.
62
+
63
+ ### Navigation Links
64
+
65
+ For `<a>` tags, use hash hrefs directly:
66
+
67
+ ```tsx
68
+ <a href="#/users/42">View User</a>
69
+ <a href="#/settings">Settings</a>
70
+ ```
71
+
72
+ Or use `navigate()` in an onclick:
73
+
74
+ ```tsx
75
+ <button onclick={() => navigate(`/users/${id}`)}>View</button>
76
+ ```
77
+
78
+ ## `route(pattern)` — Reactive Route Signal
79
+
80
+ Returns a `Signal<T | null>` that is non-null when the pattern matches the current hash, null otherwise. Useful for components that need to react to route changes without being inside the router.
81
+
82
+ ```ts
83
+ import { route } from "@blueshed/railroad";
84
+
85
+ const userRoute = route<{ id: string }>("/users/:id");
86
+
87
+ effect(() => {
88
+ const params = userRoute.get();
89
+ if (params) {
90
+ console.log(`Viewing user ${params.id}`);
91
+ }
92
+ });
93
+ ```
94
+
95
+ This is useful for:
96
+ - Highlighting the active nav link.
97
+ - Loading data when params change within the same route pattern.
98
+ - Reacting to route changes from anywhere in the app.
99
+
100
+ ## `matchRoute(pattern, path)` — Pure Pattern Matcher
101
+
102
+ A utility function with no side effects. Returns params object on match, `null` on no match.
103
+
104
+ ```ts
105
+ import { matchRoute } from "@blueshed/railroad";
106
+
107
+ matchRoute("/users/:id", "/users/42"); // { id: "42" }
108
+ matchRoute("/users/:id", "/users/42/extra"); // null (segment count mismatch)
109
+ matchRoute("/about", "/about"); // {}
110
+ matchRoute("/about", "/home"); // null
111
+ ```
112
+
113
+ ## Handling Param Changes Within a Route
114
+
115
+ When navigating from `/users/1` to `/users/2`, the router sees the same pattern (`/users/:id`) and does **not** re-render. The component must handle this itself:
116
+
117
+ ```tsx
118
+ import { createElement, route, when, signal, effect } from "@blueshed/railroad";
119
+
120
+ function UserDetail({ id }: { id: string }) {
121
+ // Option 1: Use route() signal to track param changes
122
+ const userRoute = route<{ id: string }>("/users/:id");
123
+ const user = signal<User | null>(null);
124
+
125
+ effect(() => {
126
+ const params = userRoute.get();
127
+ if (params) {
128
+ fetchUser(params.id).then(u => user.set(u));
129
+ }
130
+ });
131
+
132
+ return when(user, () => <div>{user.get()!.name}</div>);
133
+ }
134
+ ```
135
+
136
+ ## Server-Side Pattern
137
+
138
+ Railroad's route tables mirror Bun.serve's route syntax. A typical app has both:
139
+
140
+ ```ts
141
+ // server.ts — Bun serves the HTML shell
142
+ import home from "./index.html";
143
+ Bun.serve({
144
+ routes: {
145
+ "/": home,
146
+ "/api/users": () => Response.json(users),
147
+ },
148
+ });
149
+
150
+ // app.tsx — client-side routing inside the shell
151
+ routes(document.getElementById("app")!, {
152
+ "/": () => <Home />,
153
+ "/users/:id": ({ id }) => <UserDetail id={id} />,
154
+ });
155
+ ```
156
+
157
+ Resources and routes. Server and client. Same pattern.
@@ -0,0 +1,94 @@
1
+ # Shared — Typed Dependency Injection
2
+
3
+ A minimal provide/inject system for sharing values across modules without prop threading. Uses typed symbol keys for safety.
4
+
5
+ ## Creating a Key
6
+
7
+ ```ts
8
+ import { key } from "@blueshed/railroad";
9
+
10
+ const STORE = key<AppStore>("store");
11
+ const THEME = key<Signal<string>>("theme");
12
+ const API = key<ApiClient>("api");
13
+ ```
14
+
15
+ `key<T>(name)` creates a unique symbol branded with type `T`. The name is for debugging (shows in error messages).
16
+
17
+ ## Providing a Value
18
+
19
+ ```ts
20
+ import { provide } from "@blueshed/railroad";
21
+
22
+ provide(STORE, createStore());
23
+ provide(THEME, signal("light"));
24
+ provide(API, new ApiClient("/api"));
25
+ ```
26
+
27
+ Call `provide()` during initialization, before any component calls `inject()`. Values are stored in a global registry.
28
+
29
+ ## Injecting a Value
30
+
31
+ ```ts
32
+ import { inject } from "@blueshed/railroad";
33
+
34
+ const store = inject(STORE); // typed as AppStore
35
+ const theme = inject(THEME); // typed as Signal<string>
36
+ ```
37
+
38
+ If no value has been provided for the key, `inject()` throws:
39
+
40
+ ```
41
+ Error: No provider for store
42
+ ```
43
+
44
+ ## Typical Pattern
45
+
46
+ Define keys in a shared module, provide at app init, inject anywhere:
47
+
48
+ ```ts
49
+ // keys.ts — shared key definitions
50
+ import { key } from "@blueshed/railroad";
51
+ import type { Signal } from "@blueshed/railroad";
52
+
53
+ export interface AppStore {
54
+ user: Signal<User | null>;
55
+ todos: Signal<Todo[]>;
56
+ }
57
+
58
+ export const STORE = key<AppStore>("store");
59
+ ```
60
+
61
+ ```ts
62
+ // main.ts — provide at startup
63
+ import { provide, signal } from "@blueshed/railroad";
64
+ import { STORE } from "./keys";
65
+
66
+ provide(STORE, {
67
+ user: signal(null),
68
+ todos: signal([]),
69
+ });
70
+ ```
71
+
72
+ ```tsx
73
+ // any-component.tsx — inject where needed
74
+ import { createElement } from "@blueshed/railroad";
75
+ import { inject } from "@blueshed/railroad";
76
+ import { STORE } from "./keys";
77
+
78
+ function TodoList() {
79
+ const { todos } = inject(STORE);
80
+ return list(todos, (t) => t.id, (t) => <li>{t.text}</li>);
81
+ }
82
+ ```
83
+
84
+ ## When to Use Shared vs Props
85
+
86
+ - **Props** — for data that flows parent-to-child and varies per instance.
87
+ - **Shared** — for app-wide services, stores, or configuration that many components need. Avoids threading the same value through every intermediate component.
88
+
89
+ ## Important Notes
90
+
91
+ - The registry is global and module-scoped. There is one registry per JavaScript realm.
92
+ - Keys are symbols, so two calls to `key("store")` produce **different** keys. Export and import a single key instance.
93
+ - `provide()` can be called again with the same key to replace the value.
94
+ - `inject()` is synchronous — the value must already be provided.