@vira-ui/react 1.0.0 → 1.0.2
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 +746 -0
- package/package.json +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,746 @@
|
|
|
1
|
+
# @vira-ui/react
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
**Vira Framework - React hooks for Vira Reactive Protocol (VRP)**
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/@vira-ui/react)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
[](https://www.typescriptlang.org/)
|
|
10
|
+
|
|
11
|
+
**React hooks для работы с Vira Reactive Protocol - синхронизация состояния в реальном времени через WebSocket.**
|
|
12
|
+
|
|
13
|
+
[Установка](#-установка) • [Быстрый старт](#-быстрый-старт) • [API](#-api-reference) • [Примеры](#-примеры)
|
|
14
|
+
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 🎯 Что это?
|
|
20
|
+
|
|
21
|
+
**@vira-ui/react** — это пакет React хуков для работы с **Vira Reactive Protocol (VRP)**. Он предоставляет простой способ синхронизации состояния между клиентом и сервером в реальном времени через WebSocket.
|
|
22
|
+
|
|
23
|
+
### Основные возможности
|
|
24
|
+
|
|
25
|
+
- ✅ **useViraState** — универсальный хук для работы с VRP каналами
|
|
26
|
+
- ✅ **Автоматическая синхронизация** — состояние обновляется автоматически при изменениях на сервере
|
|
27
|
+
- ✅ **Типизация** — полная поддержка TypeScript
|
|
28
|
+
- ✅ **Idempotency** — поддержка msgId для предотвращения дублирования сообщений
|
|
29
|
+
- ✅ **Deep merge** — умное слияние частичных обновлений
|
|
30
|
+
- ✅ **Reconnection** — автоматическое переподключение при разрыве связи
|
|
31
|
+
- ✅ **Session management** — управление сессиями для восстановления состояния
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 📦 Установка
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm install @vira-ui/react @vira-ui/core react
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Требования:**
|
|
42
|
+
- React 18.2.0+
|
|
43
|
+
- @vira-ui/core 1.0.1+
|
|
44
|
+
- TypeScript 5.3+ (рекомендуется)
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 🚀 Быстрый старт
|
|
49
|
+
|
|
50
|
+
### 1. Базовое использование
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
import { useViraState } from "@vira-ui/react";
|
|
54
|
+
|
|
55
|
+
function MyComponent() {
|
|
56
|
+
const { data, sendUpdate, sendDiff, sendEvent } = useViraState<User>(
|
|
57
|
+
"user:123"
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
if (!data) {
|
|
61
|
+
return <div>Loading...</div>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div>
|
|
66
|
+
<h1>{data.name}</h1>
|
|
67
|
+
<button onClick={() => sendDiff({ name: "New Name" })}>
|
|
68
|
+
Обновить имя
|
|
69
|
+
</button>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 2. С опциями
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
import { useViraState } from "@vira-ui/react";
|
|
79
|
+
|
|
80
|
+
function MyComponent() {
|
|
81
|
+
const { data, sendUpdate, sendDiff, isConnected, error } = useViraState<User>(
|
|
82
|
+
"user:123",
|
|
83
|
+
{
|
|
84
|
+
initial: { name: "Guest", email: "" },
|
|
85
|
+
enableMsgId: true,
|
|
86
|
+
deepMerge: true,
|
|
87
|
+
onOpen: () => console.log("Connected"),
|
|
88
|
+
onClose: () => console.log("Disconnected"),
|
|
89
|
+
onError: (err) => console.error("Error:", err),
|
|
90
|
+
apiUrl: "http://localhost:8080",
|
|
91
|
+
authToken: "your-token",
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
if (error) {
|
|
96
|
+
return <div>Error: {error.message}</div>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!isConnected) {
|
|
100
|
+
return <div>Connecting...</div>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return <div>{data?.name}</div>;
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### 3. С несколькими каналами
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
import { useViraState } from "@vira-ui/react";
|
|
111
|
+
|
|
112
|
+
function Dashboard() {
|
|
113
|
+
const user = useViraState<User>("user:123");
|
|
114
|
+
const notifications = useViraState<Notification[]>("notifications:123");
|
|
115
|
+
const tasks = useViraState<Task[]>("tasks:123");
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<div>
|
|
119
|
+
<UserProfile data={user.data} />
|
|
120
|
+
<NotificationsList data={notifications.data} />
|
|
121
|
+
<TasksList data={tasks.data} />
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## 📚 API Reference
|
|
130
|
+
|
|
131
|
+
### `useViraState<T>(channel, options?)`
|
|
132
|
+
|
|
133
|
+
Универсальный хук для работы с VRP каналами.
|
|
134
|
+
|
|
135
|
+
**Параметры:**
|
|
136
|
+
|
|
137
|
+
- `channel` (string) — имя канала (например, `"user:123"`, `"tasks:456"`)
|
|
138
|
+
- `options` (UseViraStateOptions, опционально) — опции хука
|
|
139
|
+
|
|
140
|
+
**Возвращает:**
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
{
|
|
144
|
+
// Текущее состояние данных
|
|
145
|
+
data: T | null;
|
|
146
|
+
|
|
147
|
+
// Отправить событие на сервер
|
|
148
|
+
sendEvent: (name: string, payload: any, msgId?: string) => void;
|
|
149
|
+
|
|
150
|
+
// Отправить полное обновление (заменяет состояние)
|
|
151
|
+
sendUpdate: (payload: T, msgId?: string) => void;
|
|
152
|
+
|
|
153
|
+
// Отправить частичное обновление (сливается с текущим состоянием)
|
|
154
|
+
sendDiff: (patch: Partial<T>, msgId?: string) => void;
|
|
155
|
+
|
|
156
|
+
// Статус подключения
|
|
157
|
+
isConnected: boolean;
|
|
158
|
+
|
|
159
|
+
// Ошибка подключения, если есть
|
|
160
|
+
error: Error | null;
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**Опции:**
|
|
165
|
+
|
|
166
|
+
```tsx
|
|
167
|
+
interface UseViraStateOptions<T> {
|
|
168
|
+
// Начальное значение состояния
|
|
169
|
+
initial?: T | null;
|
|
170
|
+
|
|
171
|
+
// Включить поддержку msgId для идемпотентности
|
|
172
|
+
enableMsgId?: boolean;
|
|
173
|
+
|
|
174
|
+
// Callback при открытии соединения
|
|
175
|
+
onOpen?: () => void;
|
|
176
|
+
|
|
177
|
+
// Callback при закрытии соединения
|
|
178
|
+
onClose?: (event: CloseEvent) => void;
|
|
179
|
+
|
|
180
|
+
// Callback при ошибке соединения
|
|
181
|
+
onError?: (error: Error) => void;
|
|
182
|
+
|
|
183
|
+
// Использовать deep merge для diff патчей (по умолчанию: true)
|
|
184
|
+
deepMerge?: boolean;
|
|
185
|
+
|
|
186
|
+
// URL API (по умолчанию: VITE_API_URL или 'http://localhost:8080')
|
|
187
|
+
apiUrl?: string;
|
|
188
|
+
|
|
189
|
+
// Auth token для handshake
|
|
190
|
+
authToken?: string;
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## 🎯 Примеры использования
|
|
197
|
+
|
|
198
|
+
### Пример 1: Простое обновление состояния
|
|
199
|
+
|
|
200
|
+
```tsx
|
|
201
|
+
import { useViraState } from "@vira-ui/react";
|
|
202
|
+
|
|
203
|
+
interface Counter {
|
|
204
|
+
count: number;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function CounterComponent() {
|
|
208
|
+
const { data, sendDiff } = useViraState<Counter>("counter:demo");
|
|
209
|
+
|
|
210
|
+
const increment = () => {
|
|
211
|
+
sendDiff({ count: (data?.count || 0) + 1 });
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
<div>
|
|
216
|
+
<p>Count: {data?.count || 0}</p>
|
|
217
|
+
<button onClick={increment}>Increment</button>
|
|
218
|
+
</div>
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Пример 2: Работа с пользователем
|
|
224
|
+
|
|
225
|
+
```tsx
|
|
226
|
+
import { useViraState } from "@vira-ui/react";
|
|
227
|
+
|
|
228
|
+
interface User {
|
|
229
|
+
id: string;
|
|
230
|
+
name: string;
|
|
231
|
+
email: string;
|
|
232
|
+
avatar?: string;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function UserProfile({ userId }: { userId: string }) {
|
|
236
|
+
const { data, sendDiff, sendEvent, isConnected } = useViraState<User>(
|
|
237
|
+
`user:${userId}`,
|
|
238
|
+
{
|
|
239
|
+
initial: { id: userId, name: "", email: "" },
|
|
240
|
+
enableMsgId: true,
|
|
241
|
+
}
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
const updateName = (newName: string) => {
|
|
245
|
+
sendDiff({ name: newName });
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const updateEmail = (newEmail: string) => {
|
|
249
|
+
sendDiff({ email: newEmail });
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const uploadAvatar = (avatarUrl: string) => {
|
|
253
|
+
sendEvent("user.avatar.upload", { avatarUrl });
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
if (!isConnected) {
|
|
257
|
+
return <div>Connecting...</div>;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return (
|
|
261
|
+
<div>
|
|
262
|
+
<img src={data?.avatar} alt={data?.name} />
|
|
263
|
+
<input
|
|
264
|
+
value={data?.name || ""}
|
|
265
|
+
onChange={(e) => updateName(e.target.value)}
|
|
266
|
+
/>
|
|
267
|
+
<input
|
|
268
|
+
value={data?.email || ""}
|
|
269
|
+
onChange={(e) => updateEmail(e.target.value)}
|
|
270
|
+
/>
|
|
271
|
+
</div>
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Пример 3: Kanban доска
|
|
277
|
+
|
|
278
|
+
```tsx
|
|
279
|
+
import { useViraState } from "@vira-ui/react";
|
|
280
|
+
|
|
281
|
+
interface KanbanBoard {
|
|
282
|
+
id: string;
|
|
283
|
+
title: string;
|
|
284
|
+
columns: Column[];
|
|
285
|
+
cards: Record<string, Card>;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function KanbanBoard({ boardId }: { boardId: string }) {
|
|
289
|
+
const { data, sendEvent, sendDiff } = useViraState<KanbanBoard>(
|
|
290
|
+
`kanban:${boardId}`,
|
|
291
|
+
{
|
|
292
|
+
enableMsgId: true,
|
|
293
|
+
deepMerge: true,
|
|
294
|
+
}
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
const createCard = (columnId: string, title: string) => {
|
|
298
|
+
sendEvent("kanban.card.create", {
|
|
299
|
+
boardId,
|
|
300
|
+
columnId,
|
|
301
|
+
title,
|
|
302
|
+
at: Date.now(),
|
|
303
|
+
});
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const moveCard = (
|
|
307
|
+
cardId: string,
|
|
308
|
+
fromColumnId: string,
|
|
309
|
+
toColumnId: string,
|
|
310
|
+
newOrder: number
|
|
311
|
+
) => {
|
|
312
|
+
sendEvent("kanban.card.move", {
|
|
313
|
+
boardId,
|
|
314
|
+
cardId,
|
|
315
|
+
fromColumnId,
|
|
316
|
+
toColumnId,
|
|
317
|
+
newOrder,
|
|
318
|
+
at: Date.now(),
|
|
319
|
+
});
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const updateCard = (cardId: string, updates: Partial<Card>) => {
|
|
323
|
+
sendDiff({
|
|
324
|
+
cards: {
|
|
325
|
+
[cardId]: updates,
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
if (!data) {
|
|
331
|
+
return <div>Loading board...</div>;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return (
|
|
335
|
+
<div>
|
|
336
|
+
<h1>{data.title}</h1>
|
|
337
|
+
{data.columns.map((column) => (
|
|
338
|
+
<Column
|
|
339
|
+
key={column.id}
|
|
340
|
+
column={column}
|
|
341
|
+
cards={getColumnCards(data, column.id)}
|
|
342
|
+
onCreateCard={(title) => createCard(column.id, title)}
|
|
343
|
+
onMoveCard={moveCard}
|
|
344
|
+
onUpdateCard={updateCard}
|
|
345
|
+
/>
|
|
346
|
+
))}
|
|
347
|
+
</div>
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### Пример 4: Чат приложение
|
|
353
|
+
|
|
354
|
+
```tsx
|
|
355
|
+
import { useViraState } from "@vira-ui/react";
|
|
356
|
+
|
|
357
|
+
interface Chat {
|
|
358
|
+
messages: Message[];
|
|
359
|
+
participants: User[];
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function ChatRoom({ roomId }: { roomId: string }) {
|
|
363
|
+
const { data, sendEvent, sendDiff } = useViraState<Chat>(`chat:${roomId}`, {
|
|
364
|
+
initial: { messages: [], participants: [] },
|
|
365
|
+
enableMsgId: true,
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
const sendMessage = (text: string) => {
|
|
369
|
+
sendEvent("chat.message.send", {
|
|
370
|
+
roomId,
|
|
371
|
+
text,
|
|
372
|
+
timestamp: Date.now(),
|
|
373
|
+
});
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const addParticipant = (user: User) => {
|
|
377
|
+
sendDiff({
|
|
378
|
+
participants: [...(data?.participants || []), user],
|
|
379
|
+
});
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
return (
|
|
383
|
+
<div>
|
|
384
|
+
<div>
|
|
385
|
+
{data?.messages.map((msg) => (
|
|
386
|
+
<MessageBubble key={msg.id} message={msg} />
|
|
387
|
+
))}
|
|
388
|
+
</div>
|
|
389
|
+
<MessageInput onSend={sendMessage} />
|
|
390
|
+
</div>
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### Пример 5: Редактирование документа
|
|
396
|
+
|
|
397
|
+
```tsx
|
|
398
|
+
import { useViraState } from "@vira-ui/react";
|
|
399
|
+
|
|
400
|
+
interface Document {
|
|
401
|
+
id: string;
|
|
402
|
+
title: string;
|
|
403
|
+
content: string;
|
|
404
|
+
version: number;
|
|
405
|
+
updatedAt: number;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function DocumentEditor({ docId }: { docId: string }) {
|
|
409
|
+
const { data, sendDiff, isConnected } = useViraState<Document>(
|
|
410
|
+
`document:${docId}`,
|
|
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
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vira-ui/react",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Vira Framework - React hooks for Vira Reactive Protocol",
|
|
5
5
|
"author": "Vira Team",
|
|
6
6
|
"license": "MIT",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"prepublishOnly": "npm run build"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@vira-ui/core": "
|
|
35
|
+
"@vira-ui/core": "^1.0.1",
|
|
36
36
|
"react": "^18.2.0"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|