@vira-ui/react 1.0.2 → 1.1.1
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/README.md +411 -746
- package/dist/index.d.mts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +149 -13
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +154 -14
- package/dist/index.mjs.map +1 -1
- package/package.json +51 -50
package/README.md
CHANGED
|
@@ -1,746 +1,411 @@
|
|
|
1
|
-
# @vira-ui/react
|
|
2
|
-
|
|
3
|
-
<div align="center">
|
|
4
|
-
|
|
5
|
-
**
|
|
6
|
-
|
|
7
|
-
[](https://www.npmjs.com/package/@vira-ui/react)
|
|
8
|
-
[](LICENSE)
|
|
9
|
-
[](https://www.typescriptlang.org/)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
[Установка](#-установка) • [Быстрый старт](#-быстрый-старт) • [
|
|
14
|
-
|
|
15
|
-
</div>
|
|
16
|
-
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
## 🎯 Что это?
|
|
20
|
-
|
|
21
|
-
**@vira-ui/react**
|
|
22
|
-
|
|
23
|
-
### Основные возможности
|
|
24
|
-
|
|
25
|
-
- ✅
|
|
26
|
-
- ✅
|
|
27
|
-
- ✅
|
|
28
|
-
- ✅
|
|
29
|
-
- ✅ **
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
```tsx
|
|
53
|
-
import { useViraState } from
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
function
|
|
208
|
-
const { data,
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
);
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
function ChatRoom({ roomId }: { roomId: string }) {
|
|
363
|
-
const { data, sendEvent
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
const sendMessage = (text: string) => {
|
|
369
|
-
sendEvent(
|
|
370
|
-
|
|
371
|
-
text,
|
|
372
|
-
timestamp: Date.now()
|
|
373
|
-
});
|
|
374
|
-
};
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
enableMsgId: true,
|
|
413
|
-
deepMerge: true,
|
|
414
|
-
onOpen: () => console.log("Document loaded"),
|
|
415
|
-
}
|
|
416
|
-
);
|
|
417
|
-
|
|
418
|
-
const updateTitle = (title: string) => {
|
|
419
|
-
sendDiff({
|
|
420
|
-
title,
|
|
421
|
-
updatedAt: Date.now(),
|
|
422
|
-
});
|
|
423
|
-
};
|
|
424
|
-
|
|
425
|
-
const updateContent = (content: string) => {
|
|
426
|
-
sendDiff({
|
|
427
|
-
content,
|
|
428
|
-
updatedAt: Date.now(),
|
|
429
|
-
});
|
|
430
|
-
};
|
|
431
|
-
|
|
432
|
-
if (!isConnected || !data) {
|
|
433
|
-
return <div>Loading document...</div>;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
return (
|
|
437
|
-
<div>
|
|
438
|
-
<input
|
|
439
|
-
value={data.title}
|
|
440
|
-
onChange={(e) => updateTitle(e.target.value)}
|
|
441
|
-
/>
|
|
442
|
-
<textarea
|
|
443
|
-
value={data.content}
|
|
444
|
-
onChange={(e) => updateContent(e.target.value)}
|
|
445
|
-
/>
|
|
446
|
-
<div>Version: {data.version}</div>
|
|
447
|
-
</div>
|
|
448
|
-
);
|
|
449
|
-
}
|
|
450
|
-
```
|
|
451
|
-
|
|
452
|
-
### Пример 6: С обработкой ошибок
|
|
453
|
-
|
|
454
|
-
```tsx
|
|
455
|
-
import { useViraState } from "@vira-ui/react";
|
|
456
|
-
|
|
457
|
-
function RobustComponent() {
|
|
458
|
-
const { data, sendDiff, isConnected, error } = useViraState<MyData>(
|
|
459
|
-
"my:channel",
|
|
460
|
-
{
|
|
461
|
-
onError: (err) => {
|
|
462
|
-
console.error("Connection error:", err);
|
|
463
|
-
// Можно отправить в систему мониторинга
|
|
464
|
-
// sendToMonitoring(err);
|
|
465
|
-
},
|
|
466
|
-
onClose: (event) => {
|
|
467
|
-
console.log("Connection closed:", event.code, event.reason);
|
|
468
|
-
// Можно показать уведомление пользователю
|
|
469
|
-
},
|
|
470
|
-
}
|
|
471
|
-
);
|
|
472
|
-
|
|
473
|
-
if (error) {
|
|
474
|
-
return (
|
|
475
|
-
<div>
|
|
476
|
-
<p>Ошибка подключения: {error.message}</p>
|
|
477
|
-
<button onClick={() => window.location.reload()}>
|
|
478
|
-
Перезагрузить
|
|
479
|
-
</button>
|
|
480
|
-
</div>
|
|
481
|
-
);
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
if (!isConnected) {
|
|
485
|
-
return <div>Подключение...</div>;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
return <div>{/* Ваш контент */}</div>;
|
|
489
|
-
}
|
|
490
|
-
```
|
|
491
|
-
|
|
492
|
-
---
|
|
493
|
-
|
|
494
|
-
## 🔧 Конфигурация
|
|
495
|
-
|
|
496
|
-
### Переменные окружения
|
|
497
|
-
|
|
498
|
-
```env
|
|
499
|
-
# URL API сервера
|
|
500
|
-
VITE_API_URL=http://localhost:8080
|
|
501
|
-
|
|
502
|
-
# Auth token для подключения
|
|
503
|
-
VITE_AUTH_TOKEN=your-secret-token
|
|
504
|
-
```
|
|
505
|
-
|
|
506
|
-
### Программная конфигурация
|
|
507
|
-
|
|
508
|
-
```tsx
|
|
509
|
-
import { useViraState } from "@vira-ui/react";
|
|
510
|
-
|
|
511
|
-
function MyComponent() {
|
|
512
|
-
const { data } = useViraState("my:channel", {
|
|
513
|
-
apiUrl: "https://api.example.com",
|
|
514
|
-
authToken: "your-token",
|
|
515
|
-
});
|
|
516
|
-
}
|
|
517
|
-
```
|
|
518
|
-
|
|
519
|
-
---
|
|
520
|
-
|
|
521
|
-
## 🎯 Vira Reactive Protocol (VRP)
|
|
522
|
-
|
|
523
|
-
### Типы сообщений
|
|
524
|
-
|
|
525
|
-
VRP поддерживает следующие типы сообщений:
|
|
526
|
-
|
|
527
|
-
#### 1. `update` — полное обновление состояния
|
|
528
|
-
|
|
529
|
-
```tsx
|
|
530
|
-
sendUpdate({ name: "John", age: 30 });
|
|
531
|
-
```
|
|
532
|
-
|
|
533
|
-
#### 2. `diff` — частичное обновление
|
|
534
|
-
|
|
535
|
-
```tsx
|
|
536
|
-
sendDiff({ name: "Jane" }); // Обновит только name
|
|
537
|
-
```
|
|
538
|
-
|
|
539
|
-
#### 3. `event` — событие
|
|
540
|
-
|
|
541
|
-
```tsx
|
|
542
|
-
sendEvent("user.created", { userId: "123" });
|
|
543
|
-
```
|
|
544
|
-
|
|
545
|
-
### Каналы
|
|
546
|
-
|
|
547
|
-
Каналы используются для изоляции данных:
|
|
548
|
-
|
|
549
|
-
```tsx
|
|
550
|
-
// Пользователь
|
|
551
|
-
"user:123"
|
|
552
|
-
|
|
553
|
-
// Задачи
|
|
554
|
-
"tasks:456"
|
|
555
|
-
|
|
556
|
-
// Уведомления
|
|
557
|
-
"notifications:123"
|
|
558
|
-
|
|
559
|
-
// Демо канал
|
|
560
|
-
"demo"
|
|
561
|
-
```
|
|
562
|
-
|
|
563
|
-
### Idempotency (msgId)
|
|
564
|
-
|
|
565
|
-
Для предотвращения дублирования сообщений используйте `enableMsgId`:
|
|
566
|
-
|
|
567
|
-
```tsx
|
|
568
|
-
const { sendUpdate } = useViraState("my:channel", {
|
|
569
|
-
enableMsgId: true, // Автоматически генерирует msgId
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
// Или вручную
|
|
573
|
-
sendUpdate(data, "custom-msg-id");
|
|
574
|
-
```
|
|
575
|
-
|
|
576
|
-
### Deep Merge
|
|
577
|
-
|
|
578
|
-
По умолчанию используется deep merge для diff обновлений:
|
|
579
|
-
|
|
580
|
-
```tsx
|
|
581
|
-
// Текущее состояние
|
|
582
|
-
{ user: { name: "John", age: 30 } }
|
|
583
|
-
|
|
584
|
-
// Отправляем diff
|
|
585
|
-
sendDiff({ user: { name: "Jane" } });
|
|
586
|
-
|
|
587
|
-
// Результат (deep merge)
|
|
588
|
-
{ user: { name: "Jane", age: 30 } }
|
|
589
|
-
```
|
|
590
|
-
|
|
591
|
-
Для shallow merge:
|
|
592
|
-
|
|
593
|
-
```tsx
|
|
594
|
-
useViraState("my:channel", {
|
|
595
|
-
deepMerge: false, // Shallow merge
|
|
596
|
-
});
|
|
597
|
-
```
|
|
598
|
-
|
|
599
|
-
---
|
|
600
|
-
|
|
601
|
-
## 🔥 Best Practices
|
|
602
|
-
|
|
603
|
-
### 1. Используйте типизацию
|
|
604
|
-
|
|
605
|
-
```tsx
|
|
606
|
-
// ✅ Хорошо
|
|
607
|
-
interface User {
|
|
608
|
-
id: string;
|
|
609
|
-
name: string;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
const { data } = useViraState<User>("user:123");
|
|
613
|
-
|
|
614
|
-
// ❌ Плохо
|
|
615
|
-
const { data } = useViraState("user:123");
|
|
616
|
-
```
|
|
617
|
-
|
|
618
|
-
### 2. Используйте enableMsgId для критичных операций
|
|
619
|
-
|
|
620
|
-
```tsx
|
|
621
|
-
// ✅ Хорошо
|
|
622
|
-
useViraState("payment:123", {
|
|
623
|
-
enableMsgId: true, // Предотвращает дублирование платежей
|
|
624
|
-
});
|
|
625
|
-
|
|
626
|
-
// ❌ Плохо
|
|
627
|
-
useViraState("payment:123"); // Может привести к дублированию
|
|
628
|
-
```
|
|
629
|
-
|
|
630
|
-
### 3. Обрабатывайте ошибки
|
|
631
|
-
|
|
632
|
-
```tsx
|
|
633
|
-
// ✅ Хорошо
|
|
634
|
-
const { data, error, isConnected } = useViraState("my:channel", {
|
|
635
|
-
onError: (err) => console.error(err),
|
|
636
|
-
});
|
|
637
|
-
|
|
638
|
-
if (error) {
|
|
639
|
-
return <ErrorComponent error={error} />;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
// ❌ Плохо
|
|
643
|
-
const { data } = useViraState("my:channel");
|
|
644
|
-
// Нет обработки ошибок
|
|
645
|
-
```
|
|
646
|
-
|
|
647
|
-
### 4. Используйте initial для лучшего UX
|
|
648
|
-
|
|
649
|
-
```tsx
|
|
650
|
-
// ✅ Хорошо
|
|
651
|
-
useViraState("user:123", {
|
|
652
|
-
initial: { name: "Loading...", email: "" },
|
|
653
|
-
});
|
|
654
|
-
|
|
655
|
-
// ❌ Плохо
|
|
656
|
-
useViraState("user:123");
|
|
657
|
-
// Пользователь видит пустой экран
|
|
658
|
-
```
|
|
659
|
-
|
|
660
|
-
### 5. Используйте sendDiff для частичных обновлений
|
|
661
|
-
|
|
662
|
-
```tsx
|
|
663
|
-
// ✅ Хорошо
|
|
664
|
-
sendDiff({ name: "New Name" }); // Обновляет только name
|
|
665
|
-
|
|
666
|
-
// ❌ Плохо
|
|
667
|
-
sendUpdate({ ...data, name: "New Name" }); // Отправляет весь объект
|
|
668
|
-
```
|
|
669
|
-
|
|
670
|
-
---
|
|
671
|
-
|
|
672
|
-
## ❓ FAQ
|
|
673
|
-
|
|
674
|
-
**Q: В чём разница между `sendUpdate` и `sendDiff`?**
|
|
675
|
-
|
|
676
|
-
A: `sendUpdate` заменяет всё состояние целиком, а `sendDiff` сливает изменения с текущим состоянием. Используйте `sendDiff` для частичных обновлений.
|
|
677
|
-
|
|
678
|
-
**Q: Что такое msgId и зачем он нужен?**
|
|
679
|
-
|
|
680
|
-
A: `msgId` — это уникальный идентификатор сообщения для предотвращения дублирования. Включите `enableMsgId: true` для критичных операций.
|
|
681
|
-
|
|
682
|
-
**Q: Как работает deep merge?**
|
|
683
|
-
|
|
684
|
-
A: Deep merge рекурсивно сливает объекты, сохраняя вложенные структуры. Например, `{ user: { name: "Jane" } }` обновит только `name`, не затрагивая другие поля `user`.
|
|
685
|
-
|
|
686
|
-
**Q: Можно ли использовать несколько каналов одновременно?**
|
|
687
|
-
|
|
688
|
-
A: Да! Просто вызовите `useViraState` несколько раз с разными каналами.
|
|
689
|
-
|
|
690
|
-
**Q: Что происходит при разрыве соединения?**
|
|
691
|
-
|
|
692
|
-
A: Клиент автоматически пытается переподключиться. Вы можете отслеживать статус через `isConnected` и обрабатывать через `onClose` callback.
|
|
693
|
-
|
|
694
|
-
**Q: Как работает сессия?**
|
|
695
|
-
|
|
696
|
-
A: Сессия сохраняется между переподключениями, что позволяет восстановить подписки и состояние.
|
|
697
|
-
|
|
698
|
-
**Q: Можно ли использовать без TypeScript?**
|
|
699
|
-
|
|
700
|
-
A: Да, но TypeScript рекомендуется для лучшего DX и безопасности типов.
|
|
701
|
-
|
|
702
|
-
---
|
|
703
|
-
|
|
704
|
-
## 🛣️ Roadmap
|
|
705
|
-
|
|
706
|
-
### v1.1 (В разработке)
|
|
707
|
-
- [ ] Поддержка batch операций
|
|
708
|
-
- [ ] Оптимистичные обновления
|
|
709
|
-
- [ ] Offline queue
|
|
710
|
-
- [ ] Compression для больших payload
|
|
711
|
-
|
|
712
|
-
### v1.2 (Планируется)
|
|
713
|
-
- [ ] DevTools интеграция
|
|
714
|
-
- [ ] Метрики и мониторинг
|
|
715
|
-
- [ ] Поддержка подписок на несколько каналов
|
|
716
|
-
- [ ] WebRTC fallback
|
|
717
|
-
|
|
718
|
-
---
|
|
719
|
-
|
|
720
|
-
## 📄 License
|
|
721
|
-
|
|
722
|
-
MIT
|
|
723
|
-
|
|
724
|
-
---
|
|
725
|
-
|
|
726
|
-
## 🤝 Contributing
|
|
727
|
-
|
|
728
|
-
Мы приветствуем вклад! Пожалуйста, прочитайте [CONTRIBUTING.md](../../CONTRIBUTING.md) для деталей.
|
|
729
|
-
|
|
730
|
-
---
|
|
731
|
-
|
|
732
|
-
## 📞 Support
|
|
733
|
-
|
|
734
|
-
- **GitHub Issues**: [Создать issue](https://github.com/skrolikov/vira-core/issues)
|
|
735
|
-
- **Discussions**: [Обсуждения](https://github.com/skrolikov/vira-core/discussions)
|
|
736
|
-
|
|
737
|
-
---
|
|
738
|
-
|
|
739
|
-
<div align="center">
|
|
740
|
-
|
|
741
|
-
**Сделано с ❤️ командой Vira**
|
|
742
|
-
|
|
743
|
-
[GitHub](https://github.com/skrolikov/vira-core) • [Документация](https://vira.dev/react) • [Примеры](https://vira.dev/examples)
|
|
744
|
-
|
|
745
|
-
</div>
|
|
746
|
-
|
|
1
|
+
# @vira-ui/react
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
**React хуки для Vira Reactive Protocol (VRP)**
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/@vira-ui/react)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
[](https://www.typescriptlang.org/)
|
|
10
|
+
|
|
11
|
+
**Синхронизация состояния между клиентом и сервером через WebSocket**
|
|
12
|
+
|
|
13
|
+
[Установка](#-установка) • [Быстрый старт](#-быстрый-старт) • [Документация](#-документация)
|
|
14
|
+
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 🎯 Что это?
|
|
20
|
+
|
|
21
|
+
**@vira-ui/react** предоставляет React хуки для работы с Vira Reactive Protocol (VRP) — протоколом для real-time синхронизации состояния между клиентом и сервером через WebSocket.
|
|
22
|
+
|
|
23
|
+
### Основные возможности
|
|
24
|
+
|
|
25
|
+
- ✅ **Автоматическая синхронизация** — состояние обновляется при изменениях на сервере
|
|
26
|
+
- ✅ **Двусторонняя связь** — можно отправлять события и обновления на сервер
|
|
27
|
+
- ✅ **Diff-патчи** — обновляются только изменённые части данных
|
|
28
|
+
- ✅ **Переподключение** — автоматическое восстановление соединения
|
|
29
|
+
- ✅ **TypeScript** — полная типизация
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 📦 Установка
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install @vira-ui/react @vira-ui/core react
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Требования:**
|
|
40
|
+
- React 18.2.0+
|
|
41
|
+
- `@vira-ui/core` ^1.0.0
|
|
42
|
+
- Сервер с поддержкой Vira Reactive Protocol
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 🚀 Быстрый старт
|
|
47
|
+
|
|
48
|
+
### useViraState
|
|
49
|
+
|
|
50
|
+
Основной хук для синхронизации состояния:
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
import { useViraState } from '@vira-ui/react';
|
|
54
|
+
|
|
55
|
+
interface User {
|
|
56
|
+
id: string;
|
|
57
|
+
name: string;
|
|
58
|
+
email: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function UserProfile({ userId }: { userId: string }) {
|
|
62
|
+
const { data, sendUpdate, sendDiff, isConnected } = useViraState<User>(
|
|
63
|
+
`user:${userId}`,
|
|
64
|
+
{
|
|
65
|
+
initial: { id: userId, name: 'Guest', email: '' },
|
|
66
|
+
onOpen: () => console.log('Connected'),
|
|
67
|
+
deepMerge: true
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
if (!isConnected) return <div>Connecting...</div>;
|
|
72
|
+
if (!data) return <div>Loading...</div>;
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div>
|
|
76
|
+
<h1>{data.name}</h1>
|
|
77
|
+
<p>{data.email}</p>
|
|
78
|
+
<button onClick={() => sendDiff({ name: 'New Name' })}>
|
|
79
|
+
Update Name
|
|
80
|
+
</button>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## 📚 Документация
|
|
89
|
+
|
|
90
|
+
### useViraState
|
|
91
|
+
|
|
92
|
+
Подключается к VRP каналу и синхронизирует состояние.
|
|
93
|
+
|
|
94
|
+
**Параметры:**
|
|
95
|
+
- `channel` — имя канала (например: `"user:123"`, `"tasks"`)
|
|
96
|
+
- `options` — опции конфигурации
|
|
97
|
+
|
|
98
|
+
**Возвращает:**
|
|
99
|
+
- `data` — текущее состояние
|
|
100
|
+
- `sendUpdate(payload)` — полная замена состояния
|
|
101
|
+
- `sendDiff(patch)` — частичное обновление (merge)
|
|
102
|
+
- `sendEvent(name, payload)` — отправка события
|
|
103
|
+
- `isConnected` — статус соединения
|
|
104
|
+
- `isLoading` — статус загрузки
|
|
105
|
+
|
|
106
|
+
**Опции:**
|
|
107
|
+
- `initial` — начальное значение
|
|
108
|
+
- `apiUrl` — URL сервера (по умолчанию из `VITE_API_URL`)
|
|
109
|
+
- `authToken` — токен авторизации
|
|
110
|
+
- `deepMerge` — глубокое слияние для diff-патчей
|
|
111
|
+
- `enableMsgId` — поддержка idempotency
|
|
112
|
+
- `onOpen`, `onClose`, `onError` — колбэки событий соединения
|
|
113
|
+
|
|
114
|
+
### Примеры использования
|
|
115
|
+
|
|
116
|
+
#### Список элементов
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
import { useViraState } from '@vira-ui/react';
|
|
120
|
+
|
|
121
|
+
interface Task {
|
|
122
|
+
id: string;
|
|
123
|
+
title: string;
|
|
124
|
+
completed: boolean;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function TasksList() {
|
|
128
|
+
const { data, sendEvent, sendDiff } = useViraState<Task[]>(
|
|
129
|
+
'tasks',
|
|
130
|
+
{ initial: [] }
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const toggleTask = (taskId: string) => {
|
|
134
|
+
const task = data?.find(t => t.id === taskId);
|
|
135
|
+
if (task) {
|
|
136
|
+
sendDiff({ [taskId]: { completed: !task.completed } });
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const createTask = (title: string) => {
|
|
141
|
+
sendEvent('task.created', {
|
|
142
|
+
id: crypto.randomUUID(),
|
|
143
|
+
title,
|
|
144
|
+
completed: false
|
|
145
|
+
});
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<div>
|
|
150
|
+
{data?.map(task => (
|
|
151
|
+
<div key={task.id}>
|
|
152
|
+
<input
|
|
153
|
+
type="checkbox"
|
|
154
|
+
checked={task.completed}
|
|
155
|
+
onChange={() => toggleTask(task.id)}
|
|
156
|
+
/>
|
|
157
|
+
<span>{task.title}</span>
|
|
158
|
+
</div>
|
|
159
|
+
))}
|
|
160
|
+
<button onClick={() => createTask('New Task')}>
|
|
161
|
+
Add Task
|
|
162
|
+
</button>
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
#### Одиночный элемент
|
|
169
|
+
|
|
170
|
+
```tsx
|
|
171
|
+
function UserDetails({ userId }: { userId: string }) {
|
|
172
|
+
const { data, sendDiff, isConnected } = useViraState<User>(
|
|
173
|
+
`user:${userId}`,
|
|
174
|
+
{
|
|
175
|
+
initial: null,
|
|
176
|
+
deepMerge: true
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const updateName = (name: string) => {
|
|
181
|
+
sendDiff({ name });
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
if (!isConnected) {
|
|
185
|
+
return <div>Connecting to server...</div>;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!data) {
|
|
189
|
+
return <div>Loading user...</div>;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
<div>
|
|
194
|
+
<input
|
|
195
|
+
value={data.name}
|
|
196
|
+
onChange={(e) => updateName(e.target.value)}
|
|
197
|
+
/>
|
|
198
|
+
<p>Email: {data.email}</p>
|
|
199
|
+
</div>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
#### С обработкой ошибок
|
|
205
|
+
|
|
206
|
+
```tsx
|
|
207
|
+
function DataComponent({ channel }: { channel: string }) {
|
|
208
|
+
const { data, sendEvent, isConnected, error } = useViraState(
|
|
209
|
+
channel,
|
|
210
|
+
{
|
|
211
|
+
initial: null,
|
|
212
|
+
onError: (err) => {
|
|
213
|
+
console.error('VRP Error:', err);
|
|
214
|
+
// Можно показать уведомление пользователю
|
|
215
|
+
},
|
|
216
|
+
onClose: () => {
|
|
217
|
+
console.log('Connection closed, reconnecting...');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
if (error) {
|
|
223
|
+
return <div>Error: {error.message}</div>;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (!isConnected) {
|
|
227
|
+
return <div>Reconnecting...</div>;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return <div>{/* ... */}</div>;
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
#### С авторизацией
|
|
235
|
+
|
|
236
|
+
```tsx
|
|
237
|
+
function AuthenticatedComponent() {
|
|
238
|
+
const authToken = useAuthToken(); // Ваш хук для получения токена
|
|
239
|
+
|
|
240
|
+
const { data } = useViraState('protected:data', {
|
|
241
|
+
authToken,
|
|
242
|
+
onError: (err) => {
|
|
243
|
+
if (err.message.includes('unauthorized')) {
|
|
244
|
+
// Перенаправить на страницу входа
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
return <div>{/* ... */}</div>;
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## 🔄 Паттерны использования
|
|
256
|
+
|
|
257
|
+
### Real-time обновления
|
|
258
|
+
|
|
259
|
+
```tsx
|
|
260
|
+
function LiveDashboard() {
|
|
261
|
+
const { data } = useViraState('dashboard:stats', {
|
|
262
|
+
initial: { users: 0, orders: 0 }
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Данные автоматически обновляются при изменениях на сервере
|
|
266
|
+
return (
|
|
267
|
+
<div>
|
|
268
|
+
<div>Users: {data?.users}</div>
|
|
269
|
+
<div>Orders: {data?.orders}</div>
|
|
270
|
+
</div>
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Оптимистичные обновления
|
|
276
|
+
|
|
277
|
+
```tsx
|
|
278
|
+
function OptimisticUpdate() {
|
|
279
|
+
const { data, sendDiff } = useViraState('user:123', {
|
|
280
|
+
initial: { name: 'John' }
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
const updateName = (newName: string) => {
|
|
284
|
+
// Сразу обновляем локально (оптимистично)
|
|
285
|
+
sendDiff({ name: newName });
|
|
286
|
+
|
|
287
|
+
// Сервер подтвердит или откатит изменение
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
return (
|
|
291
|
+
<input
|
|
292
|
+
value={data?.name}
|
|
293
|
+
onChange={(e) => updateName(e.target.value)}
|
|
294
|
+
/>
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### События вместо обновлений
|
|
300
|
+
|
|
301
|
+
```tsx
|
|
302
|
+
function EventDriven() {
|
|
303
|
+
const { sendEvent } = useViraState('tasks', { initial: [] });
|
|
304
|
+
|
|
305
|
+
const handleComplete = (taskId: string) => {
|
|
306
|
+
// Отправляем событие вместо прямого обновления
|
|
307
|
+
sendEvent('task.completed', { taskId });
|
|
308
|
+
|
|
309
|
+
// Сервер обработает событие и обновит состояние
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
return <button onClick={() => handleComplete('123')}>Complete</button>;
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## 🔗 Интеграция
|
|
319
|
+
|
|
320
|
+
Обычно используется вместе с:
|
|
321
|
+
|
|
322
|
+
- **@vira-ui/core** — базовый фреймворк
|
|
323
|
+
- **@vira-ui/bindings-react** — компоненты с автоматическим связыванием
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
## 📖 Примеры
|
|
328
|
+
|
|
329
|
+
### Kanban доска
|
|
330
|
+
|
|
331
|
+
```tsx
|
|
332
|
+
function KanbanBoard() {
|
|
333
|
+
const { data, sendEvent } = useViraState<Column[]>('kanban:board', {
|
|
334
|
+
initial: []
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const moveCard = (cardId: string, fromColumn: string, toColumn: string) => {
|
|
338
|
+
sendEvent('card.moved', {
|
|
339
|
+
cardId,
|
|
340
|
+
fromColumn,
|
|
341
|
+
toColumn
|
|
342
|
+
});
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
return (
|
|
346
|
+
<div className="kanban-board">
|
|
347
|
+
{data?.map(column => (
|
|
348
|
+
<Column
|
|
349
|
+
key={column.id}
|
|
350
|
+
column={column}
|
|
351
|
+
onMoveCard={moveCard}
|
|
352
|
+
/>
|
|
353
|
+
))}
|
|
354
|
+
</div>
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Чат
|
|
360
|
+
|
|
361
|
+
```tsx
|
|
362
|
+
function ChatRoom({ roomId }: { roomId: string }) {
|
|
363
|
+
const { data, sendEvent } = useViraState<Message[]>(
|
|
364
|
+
`chat:${roomId}`,
|
|
365
|
+
{ initial: [] }
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
const sendMessage = (text: string) => {
|
|
369
|
+
sendEvent('message.sent', {
|
|
370
|
+
id: crypto.randomUUID(),
|
|
371
|
+
text,
|
|
372
|
+
timestamp: Date.now()
|
|
373
|
+
});
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
return (
|
|
377
|
+
<div>
|
|
378
|
+
<div className="messages">
|
|
379
|
+
{data?.map(msg => (
|
|
380
|
+
<Message key={msg.id} message={msg} />
|
|
381
|
+
))}
|
|
382
|
+
</div>
|
|
383
|
+
<MessageInput onSend={sendMessage} />
|
|
384
|
+
</div>
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
---
|
|
390
|
+
|
|
391
|
+
## 🔥 Best Practices
|
|
392
|
+
|
|
393
|
+
1. **Используйте типизацию** — всегда указывайте тип для `useViraState<T>`
|
|
394
|
+
2. **Обрабатывайте ошибки** — используйте `onError` для обработки ошибок соединения
|
|
395
|
+
3. **Оптимистичные обновления** — используйте `sendDiff` для мгновенного обновления UI
|
|
396
|
+
4. **События для действий** — используйте `sendEvent` для действий, которые должны обрабатываться на сервере
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## 📄 License
|
|
401
|
+
|
|
402
|
+
MIT
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
406
|
+
## 🔗 Связанные пакеты
|
|
407
|
+
|
|
408
|
+
- [`@vira-ui/core`](../core/README.md) - Базовый фреймворк с VRP клиентом
|
|
409
|
+
- [`@vira-ui/bindings-react`](../bindings-react/README.md) - Компоненты с auto-binding
|
|
410
|
+
- [`@vira-ui/ui`](../ui/README.md) - UI компоненты
|
|
411
|
+
|