@lankajs/react 0.1.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.
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@lankajs/react",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "module: One hook — `useLankaVM` — and the access tracking core already does.",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/lankajs/lanka.git",
10
+ "directory": "modules/bindings/react"
11
+ },
12
+ "homepage": "https://github.com/lankajs/lanka/tree/main/modules/bindings/react#readme",
13
+ "main": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "default": "./dist/index.js"
19
+ },
20
+ "./testing": {
21
+ "types": "./dist/testing.d.ts",
22
+ "default": "./dist/testing.js"
23
+ }
24
+ },
25
+ "sideEffects": false,
26
+ "files": [
27
+ "dist",
28
+ "LICENSE",
29
+ "README.md",
30
+ "skills"
31
+ ],
32
+ "dependencies": {
33
+ "lanka": "^2.0.0"
34
+ },
35
+ "devDependencies": {
36
+ "@testing-library/react": "^16.3.0",
37
+ "@lankajs/tool-testing": "^2.0.0"
38
+ },
39
+ "peerDependencies": {
40
+ "react": "^19.2.0"
41
+ },
42
+ "peerDependenciesMeta": {
43
+ "@testing-library/react": {
44
+ "optional": true
45
+ }
46
+ },
47
+ "scripts": {
48
+ "build": "tsup",
49
+ "lint": "eslint src _playground --max-warnings=0",
50
+ "test": "vitest run",
51
+ "test:coverage": "vitest run --coverage",
52
+ "test:watch": "vitest",
53
+ "bench": "vitest bench --run",
54
+ "typecheck": "tsc -p tsconfig.json --noEmit"
55
+ }
56
+ }
@@ -0,0 +1,160 @@
1
+ ---
2
+ name: lanka-react
3
+ description: Read a lanka ViewModel from a React component with useLankaVM, keep the useTodoVM() hook spelling with toLankaReactVM, and stop a selector from repainting for changes it did not pick with useLankaShallow. Use when writing or reviewing a React screen in a lanka application, when a component does not repaint after state changed, when a selector repaints a screen for changes it did not select, when deciding what a server component may read, or when reviewing code that imports `@lankajs/react`.
4
+ license: MIT
5
+ metadata:
6
+ author: lankajs
7
+ package: @lankajs/react
8
+ version: "0.1.0"
9
+ ---
10
+
11
+ # @lankajs/react
12
+
13
+ One call to read a ViewModel, two spellings, and one wrapper that makes a
14
+ selector mean something. `reference.md` beside this file is the full guide.
15
+
16
+ > [!NOTE]
17
+ > Only what the framework or a gate refuses is binding. Everything else here is a
18
+ > recommendation you can adapt.
19
+
20
+ ## Pick the call
21
+
22
+ | The situation | Use |
23
+ | ------------------------------------------ | ----------------------------------------------------- |
24
+ | a component reads a ViewModel | `useLankaVM(todoVM)` |
25
+ | it needs one derived value | `useLankaVM(todoVM, (s) => s.todos.length)` |
26
+ | the selector builds an **object or array** | `useLankaVM(todoVM, useLankaShallow((s) => ({ … })))` |
27
+ | the codebase already writes `useTodoVM()` | `toLankaReactVM(todoVM)`, once per file |
28
+ | outside a component — a handler, a module | `todoVM.getState()` |
29
+ | a component test | `renderWithLanka` from `@lankajs/react/testing` |
30
+
31
+ ```tsx
32
+ import { useLankaVM } from "@lankajs/react";
33
+ import { todoVM } from "./todoVM";
34
+
35
+ export const TodoScreen = () => {
36
+ const { todos, isLoading, load } = useLankaVM(todoVM);
37
+
38
+ if (isLoading) return <p>loading</p>;
39
+
40
+ return (
41
+ <ul onClick={() => void load()}>
42
+ {todos.map((t) => (
43
+ <li key={t.id}>{t.title}</li>
44
+ ))}
45
+ </ul>
46
+ );
47
+ };
48
+ ```
49
+
50
+ It answers **the state itself** — React's own idea of reactivity, which is the
51
+ one thing the shelf does not make uniform.
52
+
53
+ ## The selector that repaints for everything
54
+
55
+ `useLankaVM(vm, (s) => ({ a: s.a }))` is the commonest thing a React reader
56
+ writes. It is safe — the binding runs a selector once per state object and holds
57
+ the answer — but on its own it wakes the component for **every** change in the
58
+ ViewModel, including the keys the selector exists to ignore: a fresh object is
59
+ new whenever the state is new.
60
+
61
+ ```tsx
62
+ import { useLankaShallow, useLankaVM } from "@lankajs/react";
63
+
64
+ const { title, status } = useLankaVM(
65
+ missionVM,
66
+ useLankaShallow((s) => ({ title: s.title, status: s.status })),
67
+ );
68
+ ```
69
+
70
+ A selector answering a **primitive** never needed it, which is what makes the
71
+ cost quiet: the shape that is free and the shape that repaints on everything
72
+ look the same on the page. Wrap every selector whose answer is an object or an
73
+ array.
74
+
75
+ Before the binding held the selection, an unwrapped object selector **crashed on
76
+ the first paint** with "Maximum update depth exceeded". If you meet that message,
77
+ you are on a version older than this one.
78
+
79
+ ## The hook spelling, for a codebase that has it
80
+
81
+ ```ts
82
+ import { toLankaReactVM } from "@lankajs/react";
83
+
84
+ export const useFAQViewModel = toLankaReactVM(faqVM);
85
+ ```
86
+
87
+ ```tsx
88
+ const supportLink = useFAQViewModel((state) => state.supportLink);
89
+ useFAQViewModel.getState().trackSupportContacted("faq"); // in a handler
90
+ ```
91
+
92
+ One wrapper per ViewModel file, no call site touched. It works on any ViewModel
93
+ however it was built, wraps ONE store, adds no state, and laziness survives —
94
+ reading `useFAQViewModel.name` constructs nothing.
95
+
96
+ Which spelling is taste, with one thing to weigh: `useLankaVM(todoVM)` is what
97
+ the other four bindings publish, so a screen written that way moves between
98
+ frameworks unedited.
99
+
100
+ ## What re-renders
101
+
102
+ Without a selector the value RECORDS which keys you read, and the next change
103
+ repaints only if one of those moved. With a selector, the selector decides and
104
+ tracking is bypassed.
105
+
106
+ > [!WARNING]
107
+ > **The blind spot.** Tracking sees keys you read DIRECTLY. A key reached only
108
+ > inside a derived getter is invisible to it, so a change to that key repaints
109
+ > nothing and the screen freezes with no error. Set
110
+ > `enableAccessTrackingOptimization: false` on such a ViewModel. Do NOT read the
111
+ > underlying keys in the view "for the side effect": that is dead code, and a
112
+ > refactor or a lint autofix removes it. In development the framework announces
113
+ > the mismatch by ViewModel and key name.
114
+
115
+ ## Server components
116
+
117
+ `lanka/viewmodel` carries no `"use client"`, so a server component may read
118
+ `todoVM.getState()`. This barrel does carry it, because `useLankaVM` is a hook —
119
+ so a server component reads, and only what RENDERS is a client component.
120
+
121
+ ## Testing
122
+
123
+ ```tsx
124
+ import { renderWithLanka } from "@lankajs/react/testing";
125
+
126
+ renderWithLanka(<TodoScreen />, {
127
+ fakes: { gateways: { TodoGateway: { list: () => Promise.resolve([]) } } },
128
+ });
129
+ ```
130
+
131
+ It takes an **element**, not a component — React Testing Library's own shape.
132
+ Every call gets a fresh instance and disposes the previous one.
133
+
134
+ ## Never do these
135
+
136
+ - **Never pass an object-returning selector without `useLankaShallow`.** The
137
+ screen then repaints for every change in the ViewModel, selector or no selector.
138
+ - **Never call `useLankaVM` outside a component.** It is a hook; use
139
+ `todoVM.getState()` in a handler or a module.
140
+ - **Never import this barrel from a server component.** It carries
141
+ `"use client"`; read the ViewModel's state directly instead.
142
+ - **Never subscribe by hand to "fix" the blind spot.** Turn the optimization off
143
+ on the ViewModel — that is the switch built for it.
144
+ - **Never look for a `stop()`.** React releases the subscription on unmount; the
145
+ other bindings publish one because their call can happen outside a scope.
146
+
147
+ ## Symptom → cause
148
+
149
+ | What you see | What it is |
150
+ | ------------------------------------------------- | -------------------------------------------- |
151
+ | a screen repainting for changes it never selected | an object selector without `useLankaShallow` |
152
+ | the screen never updates, no error | the tracking blind spot — a derived getter |
153
+ | "Invalid hook call" | `useLankaVM` outside a component |
154
+ | a build error about `"use client"` | this barrel imported from a server component |
155
+ | a test sees the previous test's state | a render that bypassed `renderWithLanka` |
156
+
157
+ ## More
158
+
159
+ `reference.md` — the full guide: the tracking rules, the callable spelling in
160
+ detail, and what this package deliberately is not.
@@ -0,0 +1,262 @@
1
+ <!-- Generated from modules/bindings/react/GUIDE.md by scripts/skills.mjs. Edit the guide. -->
2
+
3
+ > **`@lankajs/react@0.1.0`** — this document describes that version.
4
+ >
5
+ > Install: `npm install @lankajs/react react zustand` (the peers are not optional; only npm adds a missing one for you).
6
+ >
7
+ > Complete code, compiled and run in CI: [modules/bindings/react/_playground/playground.test.tsx](https://github.com/lankajs/lanka/blob/main/modules/bindings/react/_playground/playground.test.tsx)
8
+
9
+ # @lankajs/react — user guide
10
+
11
+ How a React component reads a lanka ViewModel.
12
+
13
+ ## You will learn
14
+
15
+ - the one call this package publishes, and what it answers
16
+ - how to keep React's familiar `useTodoVM()` spelling, if you had it
17
+ - why a selector that returns an object needs `useLankaShallow`, and what happens without it
18
+ - when a component re-renders and when it deliberately does not
19
+ - what to do about a ViewModel that derives what the screen shows
20
+ - how to test a React component with a live framework behind it
21
+
22
+ ## When to reach for this
23
+
24
+ Reach for it the moment a React component has to read a lanka ViewModel — that
25
+ is the whole job, and there is no other supported way to do it. Install this one
26
+ package and no other binding: one application installs one, and `@lankajs/react`
27
+ serves React Native too.
28
+
29
+ You do NOT need it to reach the rest of the framework. Gateways, scenarios and
30
+ the locator are plain calls with no view in them, and `viewModel.getState()`
31
+ works anywhere, including on a server.
32
+
33
+ > [!NOTE]
34
+ > Everything below is how this package is _meant_ to be used, not how it must
35
+ > be. The framework bends at the seams it publishes — see
36
+ > [ARCHITECTURE.md](https://github.com/lankajs/lanka/blob/main/ARCHITECTURE.md) for what is checked and what is
37
+ > merely advice.
38
+
39
+ ## Install
40
+
41
+ ```bash
42
+ npm install @lankajs/react react zustand
43
+ ```
44
+
45
+ > [!IMPORTANT]
46
+ > `react` is already in your project; `zustand` is `lanka`'s own peer. npm adds
47
+ > a missing peer for you and pnpm does not, so the line names all of them.
48
+
49
+ ## The one call
50
+
51
+ `useLankaVM` is a hook. Every member of `modules/bindings/` publishes that same
52
+ name, so moving a screen from one framework to another rewrites the view and not
53
+ the vocabulary.
54
+
55
+ ```tsx
56
+ import { useLankaVM } from "@lankajs/react";
57
+ import { todoVM } from "./todoVM";
58
+
59
+ export const TodoScreen = () => {
60
+ const { todos, isLoading, load } = useLankaVM(todoVM);
61
+
62
+ if (isLoading) return <p>loading</p>;
63
+
64
+ return (
65
+ <ul onClick={() => void load()}>
66
+ {todos.map((todo) => (
67
+ <li key={todo.id}>{todo.title}</li>
68
+ ))}
69
+ </ul>
70
+ );
71
+ };
72
+ ```
73
+
74
+ It answers **the state itself** — the one thing this shelf does not make uniform,
75
+ because that is React's own idea of reactivity and a binding that hid it
76
+ would be a second reactivity system fighting the first.
77
+
78
+ ## The React spelling, if you prefer it
79
+
80
+ Until 2.0 a ViewModel WAS a hook: `createLankaVM` answered a callable, and every
81
+ screen called it. Core cannot do that any more — it may not know what a hook is —
82
+ but this package may, and it does:
83
+
84
+ ```ts
85
+ import { toLankaReactVM } from "@lankajs/react";
86
+ import { createLazyLankaVM } from "lanka/viewmodel";
87
+
88
+ export const useFAQViewModel = toLankaReactVM(
89
+ createLazyLankaVM<IFAQState, IFAQActions>({ … }),
90
+ );
91
+ ```
92
+
93
+ ```tsx
94
+ const supportLink = useFAQViewModel((state) => state.supportLink);
95
+ const { supportLink, fetchSupportLink } = useFAQViewModel();
96
+ useFAQViewModel.getState().trackSupportContacted("faq"); // in a handler, as always
97
+ ```
98
+
99
+ That is the entire migration for a codebase on 1.x: one wrapper per ViewModel
100
+ file, and not one call site touched.
101
+
102
+ It works on any ViewModel, however it was built — the factory, the lazy factory,
103
+ `ALankaVM`'s `build()`, a shared-store ViewModel — and it wraps ONE store: the
104
+ call forwards to `useLankaVM` and every member forwards to the ViewModel, so
105
+ notification, access tracking and lazy construction are the ones documented
106
+ below. A ViewModel read through this and the same one read in Vue answer
107
+ identically.
108
+
109
+ **Laziness survives.** A lazily declared ViewModel still builds on first use:
110
+ reading `useFAQViewModel.name` answers from the config and constructs nothing,
111
+ and `dispose` is still there.
112
+
113
+ Which spelling to use is taste, with one thing to weigh: `useLankaVM(todoVM)` is
114
+ what the other four bindings publish, so a screen written that way moves between
115
+ frameworks unedited. `toLankaReactVM` is for a React codebase that already has
116
+ hundreds of `useTodoVM()` call sites, and for one that simply prefers them.
117
+
118
+ ## What re-renders, and what does not
119
+
120
+ Without a selector you get a value that RECORDS which keys you read. The next
121
+ change re-renders only if one of those moved:
122
+
123
+ ```ts
124
+ // reads `todos`; a change to `isLoading` alone repaints nothing
125
+ ```
126
+
127
+ With a selector, the selector decides and tracking is bypassed:
128
+
129
+ ```ts
130
+ const count = useLankaVM(todoVM, (state) => state.todos.length);
131
+ ```
132
+
133
+ > [!WARNING]
134
+ > **The blind spot.** Tracking sees keys you read DIRECTLY. A key reached only
135
+ > inside a derived getter — an action calling `get()` — is invisible to it, so a
136
+ > change to that key re-renders nothing and the screen freezes with no error.
137
+ >
138
+ > Set `enableAccessTrackingOptimization: false` on such a ViewModel. Do NOT patch
139
+ > it in the view by reading the underlying keys "for the side effect": that is
140
+ > dead code, and a refactor or a lint autofix removes it.
141
+ >
142
+ > In development the framework announces the mismatch by ViewModel and key name.
143
+
144
+ ## A selector that returns an object
145
+
146
+ `useLankaVM(vm, (state) => ({ … }))` is the commonest thing a React reader
147
+ writes, and it is safe: the binding runs a selector once per state object and
148
+ holds the answer, so the two snapshot reads of one commit see the same
149
+ reference.
150
+
151
+ What it does NOT get on its own is the thing you took a selector for. A fresh
152
+ object is new whenever the state is new, so the component wakes for **every**
153
+ change in the ViewModel — including the keys your selector exists to ignore.
154
+ `useLankaShallow` is the comparison that makes the selection mean something:
155
+
156
+ ```tsx
157
+ import { useLankaShallow, useLankaVM } from "@lankajs/react";
158
+
159
+ const { title, status } = useLankaVM(
160
+ missionVM,
161
+ useLankaShallow((state) => ({ title: state.title, status: state.status })),
162
+ );
163
+ ```
164
+
165
+ It compares the selection one level deep — own keys, same count, `Object.is` on
166
+ each value, arrays included — and hands back the previous object when nothing in
167
+ it moved.
168
+
169
+ The comparison itself is `createLankaShallowHold` in `lanka/viewmodel`, and every
170
+ binding on the shelf can reach it. React gets a hook over it because React needs
171
+ one: a component re-runs `useLankaShallow` on every render, so the holding has to
172
+ survive a render while the selector stays the current one — which is what makes a
173
+ selection computed from props safe here. Use the hook; the core name is what the
174
+ other four bindings write.
175
+
176
+ A selector answering a PRIMITIVE never needed it, which is what makes the cost
177
+ quiet: the shape that is free and the shape that repaints on everything look the
178
+ same on the page. So the rule is simple — **wrap every selector whose answer is
179
+ an object or an array**, and leave the rest alone.
180
+
181
+ > [!NOTE]
182
+ > This used to be worse. Until the binding held the selection, an unwrapped
183
+ > object selector **crashed on the first paint** with "Maximum update depth
184
+ > exceeded" and a stack pointing at React rather than at your selector. If you
185
+ > are reading that message in an older version, this is what it was.
186
+
187
+ <details><summary><b>Deep dive:</b> why a wrapper and not an equality argument</summary>
188
+
189
+ `useLankaVM(vm, selector, isEqual)` was the other option, and it puts the
190
+ comparison in the binding for every caller — including the ones whose selection
191
+ is a string and would pay for a comparison they cannot fail. A wrapper is opt in
192
+ at the call site, which is also where a reader can see it. The shape is React's
193
+ own: a hook returning a selector, so a consumer arriving from zustand has typed
194
+ `useShallow` already.
195
+
196
+ </details>
197
+ ## Releasing the subscription
198
+
199
+ React releases the subscription when the component unmounts, and there is
200
+ nothing for you to call: a hook cannot be used outside a component, so the case
201
+ the other bindings publish a `stop` for cannot arise here.
202
+
203
+ ## Server components
204
+
205
+ `lanka/viewmodel` carries no `"use client"` — a ViewModel is a store, and a
206
+ server component may read its state with `todoVM.getState()`. This package's
207
+ barrel does carry the directive, because `useLankaVM` is a hook. So the split is
208
+ the useful one: a server component reads, and only what RENDERS is a client
209
+ component.
210
+
211
+ ```tsx
212
+ // app/page.tsx — a server component
213
+ import { todoVM } from "./todoVM";
214
+
215
+ export default function Page() {
216
+ return <TodoScreen initial={todoVM.getState().todos} />;
217
+ }
218
+ ```
219
+
220
+ ## Testing
221
+
222
+ `@lankajs/react/testing` renders a component with a bootstrapped framework, so a component
223
+ test needs no bootstrap preamble of its own:
224
+
225
+ ```tsx
226
+ import { renderWithLanka } from "@lankajs/react/testing";
227
+
228
+ renderWithLanka(<TodoScreen />, {
229
+ fakes: { gateways: { TodoGateway: { list: () => Promise.resolve([]) } } },
230
+ });
231
+ ```
232
+
233
+ It takes an ELEMENT and not a component, which is React Testing Library's own
234
+ shape — the other four bindings take the component, because theirs do.
235
+
236
+ Every call gets a FRESH instance and disposes the previous one, so a test never
237
+ inherits its neighbour's subscriptions.
238
+
239
+ ## What this package is not
240
+
241
+ It is a subscription and a render trigger, and nothing else. The recording of
242
+ which keys you read, the comparison that decides whether a change is worth a
243
+ render, and the blind-spot warning are all in `lanka` itself — which is why the
244
+ behaviour you see is the framework's rather than this package's reading of it,
245
+ and why `lankaViewBindingConformance` can hold every binding to one list.
246
+
247
+ If this package ever needs more than the ViewModel port gives it, the port has
248
+ the defect and the fix belongs in `lanka`, for every framework at once.
249
+
250
+ ## Recap
251
+
252
+ - `useLankaVM(todoVM)` is the one call, and every binding publishes that name — a screen written with it moves between frameworks unedited.
253
+ - Without a selector you get a value that records which keys you read, and only those keys re-render you.
254
+ - A key reached only through a derived getter is invisible to tracking: set `enableAccessTrackingOptimization: false` on that ViewModel rather than patching the view.
255
+ - Wrap every selector whose answer is an object or an array in `useLankaShallow`; a primitive selector needs nothing.
256
+ - `toLankaReactVM` hands back the `useTodoVM()` spelling for a 1.x codebase, over the same store and with no behaviour of its own.
257
+ - The barrel is `"use client"` and `lanka/viewmodel` is not, so a server component may read state and only what renders it is a client component.
258
+
259
+ ---
260
+
261
+ Maintaining this package: [SKILL.md](https://github.com/lankajs/lanka/blob/main/modules/bindings/react/SKILL.md) · What it is:
262
+ [README.md](https://github.com/lankajs/lanka/blob/main/modules/bindings/react/README.md) · Repository map: [../../../README.md](https://github.com/lankajs/lanka/blob/main/README.md)