@yoltra/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/LICENSE +21 -0
- package/README.es.md +411 -0
- package/README.md +411 -0
- package/dist/index.cjs +13 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.mjs +620 -0
- package/dist/index.mjs.map +1 -0
- package/dist/types/context/StoreContext.d.ts +26 -0
- package/dist/types/context/StoreProvider.d.ts +54 -0
- package/dist/types/hooks/createHooks.d.ts +182 -0
- package/dist/types/hooks/hooks.d.ts +248 -0
- package/dist/types/hooks/suspense.d.ts +178 -0
- package/dist/types/index.d.ts +11 -0
- package/dist/types/utils/path.d.ts +1 -0
- package/dist/types/utils/warnOnce.d.ts +4 -0
- package/package.json +110 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Yoltra — Copyright (c) 2026 Manu Ramirez <@pixerael>
|
|
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.es.md
ADDED
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
# @yoltra/react
|
|
4
|
+
|
|
5
|
+
> 👉 🇲🇽 Versión en Español |
|
|
6
|
+
> [ 🇺🇸 English Version](https://github.com/yoltra/yoltra/blob/main/packages/react/README.md)
|
|
7
|
+
|
|
8
|
+

|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
**Hooks de React para [yoltra](https://github.com/yoltra/yoltra/blob/main/README.md) con
|
|
12
|
+
suscripciones de grano fino por ruta.**
|
|
13
|
+
|
|
14
|
+
Suscribete a `"items.0.title"` o `"items.*.done"` -- el componente se re-renderiza solo cuando
|
|
15
|
+
esa ruta exacta cambia. Sin selectores, sin memoizacion, sin optimizacion manual.
|
|
16
|
+
|
|
17
|
+
[Ver la comparacion de flamegraph (Redux vs yoltra).](https://github.com/yoltra/yoltra/blob/main/examples/v0/yoltra-in-react/redux-yoltra-profiler.md)
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Instalacion
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install @yoltra/core @yoltra/react
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**Dependencias peer:** React 18+
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Configuracion con `createHooks` (recomendado)
|
|
32
|
+
|
|
33
|
+
`createHooks` vincula hooks completamente tipados al contexto de tu store. Todos los parametros
|
|
34
|
+
de tipo se infieren -- no se necesitan generics explicitos en los componentes.
|
|
35
|
+
|
|
36
|
+
### 1. Definir tipos y store
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
// store.ts
|
|
40
|
+
import { createStore, eventKeys } from "@yoltra/core";
|
|
41
|
+
|
|
42
|
+
export type AppEM = {
|
|
43
|
+
counter: { increment: number; decrement: number; reset: null };
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type AppState = { counter: { value: number } };
|
|
47
|
+
|
|
48
|
+
export const store = createStore<AppState, AppEM>({
|
|
49
|
+
name: "App",
|
|
50
|
+
reducer: {
|
|
51
|
+
counter: {
|
|
52
|
+
state: { value: 0 },
|
|
53
|
+
when: {
|
|
54
|
+
keys: eventKeys<AppEM>()([
|
|
55
|
+
["counter", "increment"],
|
|
56
|
+
["counter", "decrement"],
|
|
57
|
+
["counter", "reset"],
|
|
58
|
+
]),
|
|
59
|
+
},
|
|
60
|
+
reducer: (state, event) => {
|
|
61
|
+
switch (event.type) {
|
|
62
|
+
case "increment":
|
|
63
|
+
return { value: state.value + event.payload };
|
|
64
|
+
case "decrement":
|
|
65
|
+
return { value: state.value - event.payload };
|
|
66
|
+
case "reset":
|
|
67
|
+
return { value: 0 };
|
|
68
|
+
default:
|
|
69
|
+
return state;
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 2. Crear hooks tipados
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
// hooks.ts
|
|
81
|
+
import { createContext } from "react";
|
|
82
|
+
import { createHooks } from "@yoltra/react";
|
|
83
|
+
import type { StoreInstance } from "@yoltra/core";
|
|
84
|
+
import type { AppState, AppEM } from "./store";
|
|
85
|
+
|
|
86
|
+
export const AppStoreContext = createContext<StoreInstance<"counter", AppState, AppEM> | null>(
|
|
87
|
+
null,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
export const {
|
|
91
|
+
useStore,
|
|
92
|
+
useEmit,
|
|
93
|
+
useSelector,
|
|
94
|
+
useAtomicProp,
|
|
95
|
+
useAtomicProps,
|
|
96
|
+
useEvent,
|
|
97
|
+
shallowEqual,
|
|
98
|
+
} = createHooks(AppStoreContext);
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### 3. Proveer y usar
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
// App.tsx
|
|
105
|
+
import { store } from "./store";
|
|
106
|
+
import { AppStoreContext, useAtomicProp, useEmit } from "./hooks";
|
|
107
|
+
|
|
108
|
+
function Counter() {
|
|
109
|
+
const value = useAtomicProp({ reducer: "counter", property: "value" });
|
|
110
|
+
const emit = useEmit();
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<div>
|
|
114
|
+
<h1>Count: {value}</h1>
|
|
115
|
+
<button onClick={() => emit("counter", "increment", 1)}>+</button>
|
|
116
|
+
<button onClick={() => emit("counter", "decrement", 1)}>-</button>
|
|
117
|
+
<button onClick={() => emit("counter", "reset", null)}>Reset</button>
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function App() {
|
|
123
|
+
return (
|
|
124
|
+
<AppStoreContext.Provider value={store}>
|
|
125
|
+
<Counter />
|
|
126
|
+
</AppStoreContext.Provider>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## API de Hooks
|
|
134
|
+
|
|
135
|
+
### `useAtomicProp({ reducer, property }, map?, isEqual?)`
|
|
136
|
+
|
|
137
|
+
Selector de ruta unica con grano fino. Se re-renderiza solo cuando la ruta especificada cambia.
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
// Ruta exacta — se re-renderiza cuando items[0].title cambia
|
|
141
|
+
const title = useAtomicProp({
|
|
142
|
+
reducer: "todos",
|
|
143
|
+
property: "items.0.title",
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Con mapper — derivar un valor de la ruta
|
|
147
|
+
const count = useAtomicProp({ reducer: "todos", property: "items" }, (items) => items.length);
|
|
148
|
+
|
|
149
|
+
// Patron wildcard — se re-renderiza cuando cualquier item cambia
|
|
150
|
+
const allTitles = useAtomicProp(
|
|
151
|
+
{ reducer: "todos", property: "items.**" },
|
|
152
|
+
(state) => state.items.map((t) => t.title),
|
|
153
|
+
shallowEqual,
|
|
154
|
+
);
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Patrones soportados:**
|
|
158
|
+
|
|
159
|
+
- `"items.0.title"` -- ruta exacta (incluyendo indices numericos de array)
|
|
160
|
+
- `"items.*.title"` -- `*` coincide con un segmento
|
|
161
|
+
- `"items.**"` -- `**` coincide con cero o mas segmentos
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
### `useAtomicProps(specs, selector, isEqual?)`
|
|
166
|
+
|
|
167
|
+
Selector de multiples rutas. Se suscribe a varias rutas y recalcula cuando alguna cambia.
|
|
168
|
+
|
|
169
|
+
```tsx
|
|
170
|
+
const filtered = useAtomicProps(
|
|
171
|
+
[
|
|
172
|
+
{ reducer: "todos", property: "items.**" },
|
|
173
|
+
{ reducer: "filter", property: "q" },
|
|
174
|
+
],
|
|
175
|
+
(state) => state.todos.items.filter((item) => item.title.includes(state.filter.q)),
|
|
176
|
+
shallowEqual,
|
|
177
|
+
);
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
### `useEvent(channel, type, handler, phase?)`
|
|
183
|
+
|
|
184
|
+
Suscribete a eventos del store desde un componente. No afecta el flujo de eventos --
|
|
185
|
+
fire-and-forget.
|
|
186
|
+
|
|
187
|
+
```tsx
|
|
188
|
+
// Eventos confirmados (por defecto) — eventos que pasaron el middleware
|
|
189
|
+
useEvent("ui", "save", (event) => {
|
|
190
|
+
showToast("Saved!");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Eventos no confirmados — eventos rechazados por el middleware
|
|
194
|
+
useEvent(
|
|
195
|
+
"ui",
|
|
196
|
+
"delete",
|
|
197
|
+
(event) => {
|
|
198
|
+
showToast("Delete was blocked by permissions");
|
|
199
|
+
},
|
|
200
|
+
"uncommitted",
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// Todos los eventos — distinguir por fase
|
|
204
|
+
useEvent(
|
|
205
|
+
"ui",
|
|
206
|
+
"action",
|
|
207
|
+
(event, getState, emit, phase) => {
|
|
208
|
+
console.log(`Action ${phase}:`, event.type);
|
|
209
|
+
},
|
|
210
|
+
"all",
|
|
211
|
+
);
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**Fases:**
|
|
215
|
+
|
|
216
|
+
- `'committed'` (por defecto) -- eventos que pasaron el middleware y llegaron a los reducers
|
|
217
|
+
- `'uncommitted'` -- eventos rechazados por el middleware
|
|
218
|
+
- `'all'` -- ambos, con parametro `phase` para distinguir
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
### `useEmit()`
|
|
223
|
+
|
|
224
|
+
Retorna la funcion `emit` tipada del store (referencia estable).
|
|
225
|
+
|
|
226
|
+
```tsx
|
|
227
|
+
const emit = useEmit();
|
|
228
|
+
await emit("counter", "increment", 1);
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
### `useSelector(selector, isEqual?)`
|
|
234
|
+
|
|
235
|
+
Selector de grano grueso via `useSyncExternalStore`. Se re-renderiza cuando el valor
|
|
236
|
+
seleccionado cambia.
|
|
237
|
+
|
|
238
|
+
```tsx
|
|
239
|
+
const count = useSelector((state) => state.counter.value);
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
### `useStore()`
|
|
245
|
+
|
|
246
|
+
Retorna la instancia del store. Lanza error si se llama fuera de un provider.
|
|
247
|
+
|
|
248
|
+
```tsx
|
|
249
|
+
const store = useStore();
|
|
250
|
+
const state = store.getState();
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Hooks de Suspense
|
|
256
|
+
|
|
257
|
+
### `useSuspenseAtomicProp(spec, options)`
|
|
258
|
+
|
|
259
|
+
Version compatible con Suspense de `useAtomicProp`. Lanza una promesa mientras carga, capturada
|
|
260
|
+
por el boundary `<Suspense>` mas cercano.
|
|
261
|
+
|
|
262
|
+
```tsx
|
|
263
|
+
function UserName({ userId }: { userId: string }) {
|
|
264
|
+
const name = useSuspenseAtomicProp(
|
|
265
|
+
{ reducer: "users", property: `byId.${userId}.name` },
|
|
266
|
+
{
|
|
267
|
+
load: async (name, slice) => name ?? (await fetchUser(userId)).name,
|
|
268
|
+
staleTime: 30_000,
|
|
269
|
+
},
|
|
270
|
+
);
|
|
271
|
+
return <span>{name}</span>;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Uso
|
|
275
|
+
<Suspense fallback={<Spinner />}>
|
|
276
|
+
<UserName userId='123' />
|
|
277
|
+
</Suspense>;
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### `useSuspenseAtomicProps(specs, options)`
|
|
281
|
+
|
|
282
|
+
Selector Suspense de multiples rutas.
|
|
283
|
+
|
|
284
|
+
```tsx
|
|
285
|
+
const stats = useSuspenseAtomicProps(
|
|
286
|
+
[
|
|
287
|
+
{ reducer: "orders", property: "items.**" },
|
|
288
|
+
{ reducer: "users", property: "active" },
|
|
289
|
+
],
|
|
290
|
+
{ load: async (state) => computeDashboardStats(state) },
|
|
291
|
+
);
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Utilidades de cache
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
import {
|
|
298
|
+
invalidateAtomicProp,
|
|
299
|
+
invalidateAtomicPropsByReducer,
|
|
300
|
+
clearSuspenseCache,
|
|
301
|
+
} from "@yoltra/react";
|
|
302
|
+
|
|
303
|
+
// Invalidar cache de una ruta especifica
|
|
304
|
+
invalidateAtomicProp("users", "byId.123.name");
|
|
305
|
+
|
|
306
|
+
// Invalidar todas las entradas de cache de un reducer
|
|
307
|
+
invalidateAtomicPropsByReducer("users");
|
|
308
|
+
|
|
309
|
+
// Limpiar todo
|
|
310
|
+
clearSuspenseCache();
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## `shallowEqual`
|
|
316
|
+
|
|
317
|
+
Comparador de igualdad superficial de objetos. Usalo como argumento `isEqual` cuando tu valor
|
|
318
|
+
derivado es un objeto plano:
|
|
319
|
+
|
|
320
|
+
```tsx
|
|
321
|
+
const todos = useAtomicProp(
|
|
322
|
+
{ reducer: "todos", property: "items.**" },
|
|
323
|
+
(state) => state.items.map((t) => ({ id: t.id, title: t.title })),
|
|
324
|
+
shallowEqual,
|
|
325
|
+
);
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
## Rendimiento: Antes y Despues
|
|
331
|
+
|
|
332
|
+
### Antes (grano grueso)
|
|
333
|
+
|
|
334
|
+
```tsx
|
|
335
|
+
// Cada TodoItem se re-renderiza cuando CUALQUIER tarea cambia
|
|
336
|
+
function TodoList() {
|
|
337
|
+
const todos = useSelector((state) => state.todos.items);
|
|
338
|
+
return todos.map((todo) => <TodoItem key={todo.id} todo={todo} />);
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Despues (grano fino con yoltra)
|
|
343
|
+
|
|
344
|
+
```tsx
|
|
345
|
+
// Cada TodoItem se re-renderiza SOLO cuando sus propios datos cambian
|
|
346
|
+
function TodoItem({ index }: { index: number }) {
|
|
347
|
+
const title = useAtomicProp({
|
|
348
|
+
reducer: "todos",
|
|
349
|
+
property: `items.${index}.title`,
|
|
350
|
+
});
|
|
351
|
+
const done = useAtomicProp({
|
|
352
|
+
reducer: "todos",
|
|
353
|
+
property: `items.${index}.done`,
|
|
354
|
+
});
|
|
355
|
+
return <div className={done ? "done" : ""}>{title}</div>;
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
[Ver la comparacion completa de flamegraph.](https://github.com/yoltra/yoltra/blob/main/examples/v0/yoltra-in-react/redux-yoltra-profiler.md)
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
## Compatibilidad con React 18+
|
|
364
|
+
|
|
365
|
+
- **Concurrent Mode:** Totalmente compatible. Todos los hooks usan `useSyncExternalStore`.
|
|
366
|
+
- **Strict Mode:** La deduplicacion de eventos previene el doble procesamiento.
|
|
367
|
+
- **Suspense:** `useSuspenseAtomicProp` y `useSuspenseAtomicProps` lanzan promesas para
|
|
368
|
+
boundaries `<Suspense>`.
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
## Ejemplos
|
|
373
|
+
|
|
374
|
+
- **[App de Tareas con Profiler](../../examples/v0/yoltra-in-react)** -- CRUD completo con
|
|
375
|
+
comparacion de flamegraph
|
|
376
|
+
- **[Logo Cinetico (3000 particulas)](../../examples/v0/yoltra-kinetic-logo)** -- Suscripciones
|
|
377
|
+
independientes por circulo SVG
|
|
378
|
+
- **[Next.js 15 App Router](../../examples/v0/yoltra-in-nextjs)** -- SSR + cambio de tema
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
## Documentacion
|
|
383
|
+
|
|
384
|
+
- **[README raiz de yoltra](https://github.com/yoltra/yoltra/blob/main/README.md)** --
|
|
385
|
+
Descripcion general y configuracion rapida
|
|
386
|
+
- **[API de @yoltra/core](https://github.com/yoltra/yoltra/blob/main/packages/core/README.md)**
|
|
387
|
+
-- Store, middleware, efectos, matchers `When`
|
|
388
|
+
- **[Guia de Inicio Rapido](https://github.com/yoltra/yoltra/blob/main/docs/en/QUICK_START_GUIDE.md)**
|
|
389
|
+
-- Cinco pasos hacia una app funcional
|
|
390
|
+
- **[Comparacion de Bibliotecas](https://github.com/yoltra/yoltra/blob/main/docs/en/design/state-management-library-comparison.md)**
|
|
391
|
+
-- Comparacion arquitectonica
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
395
|
+
## Contribuir
|
|
396
|
+
|
|
397
|
+
- [Raiz del Monorepo](../../)
|
|
398
|
+
- [Guia de Contribucion](../../CONTRIBUTING.md)
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
402
|
+
## Estado
|
|
403
|
+
|
|
404
|
+
**Release Candidate** -- Las APIs son estables, usadas en produccion, cambios menores posibles
|
|
405
|
+
antes de v1.0.0.
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
## Licencia
|
|
410
|
+
|
|
411
|
+
**MIT** -- Libre para usar en proyectos comerciales y de codigo abierto.
|