@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 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
+ ![yoltra logo](../../assets/yoltra-logo.png)
2
+
3
+ # @yoltra/react
4
+
5
+ > 👉 🇲🇽 Versión en Español&nbsp; |
6
+ > &nbsp;[ 🇺🇸 English Version](https://github.com/yoltra/yoltra/blob/main/packages/react/README.md)&nbsp;
7
+
8
+ ![npm downloads](https://badgen.net/npm/dm/@yoltra/react)
9
+ ![License](https://badgen.net/npm/license/@yoltra/react)
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.