react-presenter 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/LICENSE +21 -0
- package/README.md +491 -0
- package/dist/index.cjs +268 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +164 -0
- package/dist/index.d.ts +164 -0
- package/dist/index.js +261 -0
- package/dist/index.js.map +1 -0
- package/dist/react.cjs +30 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +142 -0
- package/dist/react.d.ts +142 -0
- package/dist/react.js +27 -0
- package/dist/react.js.map +1 -0
- package/package.json +70 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 react-presenter contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
# react-presenter
|
|
2
|
+
|
|
3
|
+
Clean presentation logic for React and Next.js applications with TypeScript presenters. The core is framework-agnostic and also works with NestJS, Angular, Vue.js, and Node.js.
|
|
4
|
+
|
|
5
|
+
Keep formatting, derived booleans, i18n, and authorization-aware fields
|
|
6
|
+
out of your JSX and off your raw API/DB entities, put them in a small
|
|
7
|
+
class next to the component that uses them.
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
class ProductPresenter extends Presenter<Product> {
|
|
11
|
+
get isNew() {
|
|
12
|
+
const days =
|
|
13
|
+
(Date.now() - new Date(this.data.created_at).getTime()) / 86_400_000;
|
|
14
|
+
return days <= 7;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get formattedPrice() {
|
|
18
|
+
return this.format.currency(this.data.price, this.data.currency);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function ProductCard({ product }: { product: Product }) {
|
|
23
|
+
const p = ProductPresenter.present(product);
|
|
24
|
+
return (
|
|
25
|
+
<article>
|
|
26
|
+
<h2>{p.title}</h2> {/* passed through from `product` automatically */}
|
|
27
|
+
<span>{p.formattedPrice}</span>
|
|
28
|
+
{p.isNew && <Badge>NEW</Badge>}
|
|
29
|
+
</article>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Table of contents
|
|
37
|
+
|
|
38
|
+
- [Install](#install)
|
|
39
|
+
- [Quick start](#quick-start)
|
|
40
|
+
- [Core concepts](#core-concepts)
|
|
41
|
+
- [Use cases](#use-cases)
|
|
42
|
+
1. [Generic `Presenter<T>` base + automatic attribute mapping](#1-generic-presentert-base--automatic-attribute-mapping)
|
|
43
|
+
2. [Collections](#2-collections)
|
|
44
|
+
3. [Server-action / RSC friendly output](#3-server-action--rsc-friendly-output)
|
|
45
|
+
4. [Formatting & localization](#4-formatting--localization)
|
|
46
|
+
5. [Serialization: `toJSON`, `only`, `except`](#5-serialization-tojson-only-except)
|
|
47
|
+
6. [Context](#6-context)
|
|
48
|
+
7. [Authorization-aware presentation](#7-authorization-aware-presentation)
|
|
49
|
+
8. [Async computed properties](#8-async-computed-properties)
|
|
50
|
+
9. [Functional `decorate()`](#9-functional-decorate)
|
|
51
|
+
10. [Automatic attribute mapping (detail)](#10-automatic-attribute-mapping-detail)
|
|
52
|
+
- [React integration](#react-integration)
|
|
53
|
+
- [Next.js: Server Components & Server Actions](#nextjs-server-components--server-actions)
|
|
54
|
+
- [API reference](#api-reference)
|
|
55
|
+
- [A note on `async get`](#a-note-on-async-get)
|
|
56
|
+
- [Testing](#testing)
|
|
57
|
+
- [Project layout](#project-layout)
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Install
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npm install react-presenter
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
React is an **optional peer dependency**, the core `Presenter` class has
|
|
68
|
+
no React dependency at all and works in plain Node, NestJs, Next.js Server
|
|
69
|
+
Components, API routes, or anywhere else. Only `react-presenter/react`
|
|
70
|
+
(the `usePresenter` hook) needs React installed.
|
|
71
|
+
|
|
72
|
+
## Quick start
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
// presenters/user-presenter.ts
|
|
76
|
+
import { Presenter } from "react-presenter";
|
|
77
|
+
|
|
78
|
+
interface User {
|
|
79
|
+
id: string;
|
|
80
|
+
email: string;
|
|
81
|
+
firstName: string;
|
|
82
|
+
lastName: string;
|
|
83
|
+
createdAt: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export class UserPresenter extends Presenter<User> {
|
|
87
|
+
get fullName() {
|
|
88
|
+
return `${this.data.firstName} ${this.data.lastName}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
get formattedCreatedAt() {
|
|
92
|
+
return this.format.date(this.data.createdAt, { dateStyle: "medium" });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
// components/user-card.tsx
|
|
99
|
+
import { UserPresenter } from "../presenters/user-presenter";
|
|
100
|
+
|
|
101
|
+
function UserCard({ user }: { user: User }) {
|
|
102
|
+
const p = UserPresenter.present(user);
|
|
103
|
+
return (
|
|
104
|
+
<div>
|
|
105
|
+
<h3>{p.fullName}</h3> {/* presenter getter */}
|
|
106
|
+
<p>{p.email}</p> {/* passed through from `user` */}
|
|
107
|
+
<time>{p.formattedCreatedAt}</time>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Core concepts
|
|
114
|
+
|
|
115
|
+
- **`Presenter<T, C>`** — abstract base class. `T` is your raw data
|
|
116
|
+
shape, `C` is an optional context type (locale, current user, etc).
|
|
117
|
+
- **`.present(data, context?, options?)`** — static factory. Instantiates
|
|
118
|
+
your presenter and wraps it in a `Proxy` so any attribute of `data` not
|
|
119
|
+
shadowed by a getter is available directly.
|
|
120
|
+
- **`.presentMany(dataList, context?, options?)`** — same, for arrays.
|
|
121
|
+
- **`.toJSON()` / `.only()` / `.except()`** — serialize _only_ the
|
|
122
|
+
presenter's own computed getters, never the raw entity. Safe to send
|
|
123
|
+
across a server boundary.
|
|
124
|
+
- **`.resolve()`** — await every async computed property and merge the
|
|
125
|
+
result with `toJSON()`.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Use cases
|
|
130
|
+
|
|
131
|
+
### 1. Generic `Presenter<T>` base + automatic attribute mapping
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
class ProductPresenter extends Presenter<Product> {
|
|
135
|
+
get formattedPrice() {
|
|
136
|
+
return this.format.currency(this.data.price, this.data.currency);
|
|
137
|
+
}
|
|
138
|
+
get isNew() {
|
|
139
|
+
const days =
|
|
140
|
+
(Date.now() - new Date(this.data.created_at).getTime()) / 86_400_000;
|
|
141
|
+
return days <= 7;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const product = ProductPresenter.present(rawProduct);
|
|
146
|
+
product.price; // ← raw pass-through, no getter needed
|
|
147
|
+
product.formattedPrice; // ← presenter getter
|
|
148
|
+
product.location; // ← raw pass-through
|
|
149
|
+
product.isNew; // ← presenter getter
|
|
150
|
+
product.seller; // ← raw pass-through (the whole nested object)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### 2. Collections
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
const user = UserPresenter.present(rawUser);
|
|
157
|
+
const users = UserPresenter.presentMany(rawUsers);
|
|
158
|
+
|
|
159
|
+
users.map((u) => u.fullName);
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Context is shared across every item:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
const users = UserPresenter.presentMany(rawUsers, { currentUserId: "456" });
|
|
166
|
+
users[0].canEdit; // evaluated per-item against the same context
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### 3. Server-action / RSC friendly output
|
|
170
|
+
|
|
171
|
+
Presenter instances use `Proxies`, so they can't be passed directly between Next.js Server and Client Components but directly works in server component. Call `.toJSON()`, `.only()`, or `.except()` first to convert the presenter into a serializable object when you pass to client component.
|
|
172
|
+
— see the [Next.js section](#nextjs-server-components--server-actions) below for
|
|
173
|
+
the full pattern.
|
|
174
|
+
|
|
175
|
+
### 4. Formatting & localization
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
class UserPresenter extends Presenter<User, { locale?: string }> {
|
|
179
|
+
get statusLabel() {
|
|
180
|
+
return this.t(`users.status.${this.data.status}`);
|
|
181
|
+
}
|
|
182
|
+
get formattedJoinDate() {
|
|
183
|
+
return this.format.date(this.data.createdAt, { dateStyle: "long" });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
`this.format` is an Intl-backed adapter (`.date`, `.number`, `.currency`,
|
|
189
|
+
`.relativeTime`) that automatically uses `context.locale`. `this.t(key,
|
|
190
|
+
params?)` calls whatever translate function you've wired up globally:
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
import { configurePresenter } from "react-presenter";
|
|
194
|
+
import i18next from "i18next";
|
|
195
|
+
|
|
196
|
+
configurePresenter({
|
|
197
|
+
translate: (key, params, locale) =>
|
|
198
|
+
i18next.t(key, { ...params, lng: locale }),
|
|
199
|
+
locale: "en-US",
|
|
200
|
+
});
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Call `configurePresenter` once, near your app's entry point. Without it,
|
|
204
|
+
`this.t(key)` just returns `key` unchanged, so it's always safe to call.
|
|
205
|
+
|
|
206
|
+
### 5. Serialization: `toJSON`, `only`, `except`
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
const presenter = UserPresenter.present(user);
|
|
210
|
+
|
|
211
|
+
presenter.toJSON(); // every presenter getter
|
|
212
|
+
presenter.only("id", "fullName", "avatarUrl"); // just these, in this order
|
|
213
|
+
presenter.except("internalNotes"); // everything except these
|
|
214
|
+
presenter.toJSON({ only: ["id", "fullName"] }); // equivalent to only()
|
|
215
|
+
presenter.toJSON({ except: ["internalNotes"] }); // equivalent to except()
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`toJSON()` **only ever includes getters you defined on the presenter** —
|
|
219
|
+
never `data`, never `context`, never a raw attribute that's only visible
|
|
220
|
+
via the automatic pass-through. This is deliberate: it's what makes it
|
|
221
|
+
safe to return a presenter's `toJSON()` from an API route or Server
|
|
222
|
+
Action without worrying you've leaked the whole underlying entity.
|
|
223
|
+
|
|
224
|
+
### 6. Context
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
const presenter = UserPresenter.present(user, {
|
|
228
|
+
locale: "ja-JP",
|
|
229
|
+
currentUserId: "456",
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
class UserPresenter extends Presenter<
|
|
233
|
+
User,
|
|
234
|
+
{ locale: string; currentUserId: string }
|
|
235
|
+
> {
|
|
236
|
+
get canEdit() {
|
|
237
|
+
return this.context.currentUserId === this.data.id;
|
|
238
|
+
}
|
|
239
|
+
get formattedDate() {
|
|
240
|
+
return this.format.date(this.data.createdAt);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Context is just a second constructor argument — put whatever your
|
|
246
|
+
presentation logic depends on in there (current user, feature flags,
|
|
247
|
+
locale, request-scoped data, etc).
|
|
248
|
+
|
|
249
|
+
### 7. Authorization-aware presentation
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
class UserPresenter extends Presenter<
|
|
253
|
+
User,
|
|
254
|
+
{ currentUser?: { can(p: string): boolean } }
|
|
255
|
+
> {
|
|
256
|
+
get showEmail() {
|
|
257
|
+
return this.context.currentUser?.can("users.read_email") === true;
|
|
258
|
+
}
|
|
259
|
+
get email() {
|
|
260
|
+
if (!this.showEmail) return undefined;
|
|
261
|
+
return this.data.email;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
If a getter **throws** instead of returning `undefined` (e.g. you prefer
|
|
267
|
+
to `throw new Error("not authorized")`), `toJSON()` / `only()` /
|
|
268
|
+
`except()` catch it and omit the key rather than failing the whole
|
|
269
|
+
serialization — so either style works.
|
|
270
|
+
|
|
271
|
+
### 8. Async computed properties
|
|
272
|
+
|
|
273
|
+
```ts
|
|
274
|
+
class UserPresenter extends Presenter<User> {
|
|
275
|
+
async profileScore() {
|
|
276
|
+
return calculateScore(this.data);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const presenter = UserPresenter.present(user);
|
|
281
|
+
const resolved = await presenter.resolve();
|
|
282
|
+
resolved.profileScore; // number
|
|
283
|
+
resolved.fullName; // sync getters are included too
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
See [the note below](#a-note-on-async-get) on why this is a plain
|
|
287
|
+
`async` method rather than `async get profileScore()`.
|
|
288
|
+
|
|
289
|
+
### 9. Functional `decorate()`
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
import { decorate, decorateMany } from "react-presenter";
|
|
293
|
+
|
|
294
|
+
const user = decorate(rawUser, UserPresenter);
|
|
295
|
+
const users = decorateMany(rawUsers, UserPresenter, context);
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Identical to `UserPresenter.present(...)` / `.presentMany(...)` — pick
|
|
299
|
+
whichever reads better at the call site.
|
|
300
|
+
|
|
301
|
+
### 10. Automatic attribute mapping (detail)
|
|
302
|
+
|
|
303
|
+
Given:
|
|
304
|
+
|
|
305
|
+
```ts
|
|
306
|
+
type User = {
|
|
307
|
+
id: string;
|
|
308
|
+
email: string;
|
|
309
|
+
active: boolean;
|
|
310
|
+
firstName: string;
|
|
311
|
+
lastName: string;
|
|
312
|
+
};
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
you do **not** need to write pass-through getters for every field:
|
|
316
|
+
|
|
317
|
+
```ts
|
|
318
|
+
class UserPresenter extends Presenter<User> {
|
|
319
|
+
// nothing here yet — id/email/active/firstName/lastName all work already
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const user = UserPresenter.present(rawUser);
|
|
323
|
+
user.email; // works — proxied straight through to rawUser.email
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
`.present()` wraps the presenter instance in a `Proxy`: a property read
|
|
327
|
+
checks the presenter itself first (so a getter you _do_ define always
|
|
328
|
+
wins), then falls back to the same key on the raw data object. This is
|
|
329
|
+
implemented with `Proxy` `get`/`has` traps, not code generation, so it
|
|
330
|
+
works for any object shape without configuration.
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## React integration
|
|
335
|
+
|
|
336
|
+
The core package has zero React dependency — call `.present()` directly
|
|
337
|
+
in a Server Component. For Client Components, `react-presenter/react`
|
|
338
|
+
provides hooks that memoize presenter creation across re-renders:
|
|
339
|
+
|
|
340
|
+
```tsx
|
|
341
|
+
"use client";
|
|
342
|
+
import { usePresenter } from "react-presenter/react";
|
|
343
|
+
|
|
344
|
+
function ProductCard({ product }: { product: Product }) {
|
|
345
|
+
const p = usePresenter(ProductPresenter, product);
|
|
346
|
+
return <h2>{p.title}</h2>;
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
`usePresenterMany` is the collection equivalent. Both recompute only
|
|
351
|
+
when `data`/`context` (or your own custom `deps` array, passed as a 5th
|
|
352
|
+
argument) change.
|
|
353
|
+
|
|
354
|
+
## Next.js: Server Components & Server Actions
|
|
355
|
+
|
|
356
|
+
Presenters work directly in Server Components, no client boundary
|
|
357
|
+
needed if you're just rendering:
|
|
358
|
+
|
|
359
|
+
```tsx
|
|
360
|
+
// app/products/[id]/page.tsx (Server Component)
|
|
361
|
+
import { ProductPresenter } from "@/presenters/product-presenter";
|
|
362
|
+
|
|
363
|
+
export default async function ProductPage({
|
|
364
|
+
params,
|
|
365
|
+
}: {
|
|
366
|
+
params: { id: string };
|
|
367
|
+
}) {
|
|
368
|
+
const product = await getProduct(params.id);
|
|
369
|
+
const p = ProductPresenter.present(product);
|
|
370
|
+
return (
|
|
371
|
+
<h1>
|
|
372
|
+
{p.title} — {p.formattedPrice}
|
|
373
|
+
</h1>
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
If you need to **pass presented data across a server/client boundary**
|
|
379
|
+
(props into a Client Component, or a Server Action's return value),
|
|
380
|
+
serialize first — the Proxy-wrapped instance itself is not
|
|
381
|
+
serializable:
|
|
382
|
+
|
|
383
|
+
```tsx
|
|
384
|
+
// app/products/[id]/page.tsx
|
|
385
|
+
const p = ProductPresenter.present(product);
|
|
386
|
+
return <ProductCardClient product={p.toJSON()} />; // plain object, safe
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
```ts
|
|
390
|
+
// actions/update-user.ts
|
|
391
|
+
"use server";
|
|
392
|
+
import { UserPresenter } from "@/presenters/user-presenter";
|
|
393
|
+
|
|
394
|
+
export async function updateUser(id: string, data: FormData) {
|
|
395
|
+
const user = await db.user.update({
|
|
396
|
+
where: { id },
|
|
397
|
+
data: { name: data.get("name") as string },
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
// Return only what the client needs, never the raw entity:
|
|
401
|
+
return UserPresenter.present(user).only("id", "fullName", "avatarUrl");
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
This is also what makes `toJSON()`/`only()`/`except()` valuable even
|
|
406
|
+
outside Next.js: they're a deliberate allowlist between your database
|
|
407
|
+
entity and anything that leaves the server.
|
|
408
|
+
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
## API reference
|
|
412
|
+
|
|
413
|
+
### `class Presenter<T, C = Record<string, unknown>>`
|
|
414
|
+
|
|
415
|
+
| Member | Description |
|
|
416
|
+
| ------------------------------------ | ------------------------------------------------------------------------------------------ |
|
|
417
|
+
| `constructor(data: T, context?: C)` | Usually called for you via `.present()`. |
|
|
418
|
+
| `this.data: T` | The raw entity. |
|
|
419
|
+
| `this.context: C` | Whatever you passed as context (`{}` if omitted). |
|
|
420
|
+
| `this.format` _(protected)_ | Intl-backed `{ date, number, currency, relativeTime }`, locale-aware via `context.locale`. |
|
|
421
|
+
| `this.t(key, params?)` _(protected)_ | Calls the globally configured `translate` function. |
|
|
422
|
+
| `.toJSON(options?)` | Plain object of presenter-defined getters only. |
|
|
423
|
+
| `.only(...keys)` | Shorthand for `toJSON({ only: keys })`. |
|
|
424
|
+
| `.except(...keys)` | Shorthand for `toJSON({ except: keys })`. |
|
|
425
|
+
| `.resolve(options?)` | `Promise<object>` — awaits every `async` method and merges with `toJSON(options)`. |
|
|
426
|
+
|
|
427
|
+
### Static methods
|
|
428
|
+
|
|
429
|
+
| Method | Description |
|
|
430
|
+
| ----------------------------------------------------- | -------------------------------------------------------------- |
|
|
431
|
+
| `Presenter.present(data, context?, options?)` | Instantiate + wrap in the pass-through Proxy. Returns `P & T`. |
|
|
432
|
+
| `Presenter.presentMany(dataList, context?, options?)` | Array version of `present`. |
|
|
433
|
+
|
|
434
|
+
`options: { memoize?: boolean }` — when `true`, each getter's value is
|
|
435
|
+
cached the first time it's read on a given instance (per-property, via
|
|
436
|
+
an internal `WeakMap`). Off by default.
|
|
437
|
+
|
|
438
|
+
### Top-level exports (`react-presenter`)
|
|
439
|
+
|
|
440
|
+
- `Presenter` — the base class.
|
|
441
|
+
- `decorate(data, PresenterClass, context?, options?)` / `decorateMany(...)`
|
|
442
|
+
- `configurePresenter({ translate?, locale?, formatters? })`
|
|
443
|
+
- `getPresenterConfig()`
|
|
444
|
+
- `createFormatAdapter(locale?)` — build a standalone formatter, e.g. for use outside a presenter.
|
|
445
|
+
- Types: `PresentOptions`, `ToJSONOptions`, `PresenterConstructor`, `FormatAdapter`, `TranslateFn`, `PresenterGlobalConfig`, `AnyRecord`.
|
|
446
|
+
|
|
447
|
+
### `react-presenter/react`
|
|
448
|
+
|
|
449
|
+
- `usePresenter(PresenterClass, data, context?, options?, deps?)`
|
|
450
|
+
- `usePresenterMany(PresenterClass, dataList, context?, options?, deps?)`
|
|
451
|
+
|
|
452
|
+
---
|
|
453
|
+
|
|
454
|
+
## A note on `async`
|
|
455
|
+
|
|
456
|
+
The implemented API keeps the spirit of "async computed
|
|
457
|
+
property" but as a plain `async` method (no `get`):
|
|
458
|
+
|
|
459
|
+
```ts
|
|
460
|
+
class UserPresenter extends Presenter<User> {
|
|
461
|
+
async profileScore() {
|
|
462
|
+
return calculateScore(this.data);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
`.resolve()` auto-detects every `async` method anywhere on the
|
|
468
|
+
presenter's prototype chain (no extra registration/decorator needed),
|
|
469
|
+
calls each with no arguments, and merges the resolved values with
|
|
470
|
+
`toJSON()`. If one rejects, it resolves to `undefined` in the output
|
|
471
|
+
instead of failing the whole call.
|
|
472
|
+
|
|
473
|
+
---
|
|
474
|
+
|
|
475
|
+
## Testing
|
|
476
|
+
|
|
477
|
+
The package ships with a full Vitest suite covering every use case
|
|
478
|
+
above: automatic attribute pass-through & override, memoization,
|
|
479
|
+
collections & shared context, `toJSON`/`only`/`except`, `resolve()`,
|
|
480
|
+
and context-driven authorization/i18n/formatting.
|
|
481
|
+
|
|
482
|
+
```bash
|
|
483
|
+
npm install
|
|
484
|
+
npm run typecheck # tsc --noEmit
|
|
485
|
+
npm run build # tsup → dist/{index,react}.{js,cjs,d.ts}
|
|
486
|
+
npm test # vitest run
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
## Contributing OR Fix a bug?
|
|
490
|
+
|
|
491
|
+
Please read the [Contributing Guidelines](https://github.com/salmanx/react-presenter/blob/main/CONTRIBUTING.md).
|