Программирование HTML Сравнение useWebSocketStore и useWebSocket Wed, February 11 2026  

Поделиться

Нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.


Сравнение useWebSocketStore и useWebSocket Печать
Добавил(а) microsin   

Давайте сравним эти два подхода для работы с WebSocket в React-приложениях.

[useWebSocket (хук для конкретного соединения)]

Когда использовать:

- Одно соединение на компонент - когда компоненту нужно только одно WebSocket-соединение.
- Простая логика - базовые операции подключения, отправки и приема сообщений.
- Локальное состояние - данные нужны только в пределах одного компонента.
- Быстрый прототип - минимальная настройка.

Пример реализации на TypeScript:

const useWebSocket = (url: string) => {
const [messages, setMessages] = useState< string[]>([]);
const [isConnected, setIsConnected] = useState(false);
const wsRef = useRef< WebSocket | null>(null);

useEffect(() => {
const ws = new WebSocket(url);
wsRef.current = ws;

ws.onopen = () => setIsConnected(true);
ws.onclose = () => setIsConnected(false);
ws.onmessage = (event) => {
setMessages(prev => [...prev, event.data]);
};

return () => ws.close();
}, [url]);

const sendMessage = (message: string) => {
wsRef.current?.send(message);
};

return { messages, isConnected, sendMessage }; };

// Использование в компоненте
const Component = () => {
const { messages, sendMessage } = useWebSocket('ws://api.example.com');

return < div>{/* ... */}< /div>; };

[useWebSocketStore (глобальное состояние)]

Когда использовать:

- Единое соединение на все приложение - когда нужно одно соединение для нескольких компонентов.
- Сложная бизнес-логика - авторизация, повторные подключения, обработка ошибок.
- Глобальный доступ - разные компоненты должны иметь доступ к одному соединению.
- Персистентность - сохранение состояния при переключении между компонентами.

Пример реализации с Zustand на TypeScript:

interface WebSocketStore {
ws: WebSocket | null;
isConnected: boolean;
messages: Message[];
connect: (url: string) => void;
disconnect: () => void;
sendMessage: (message: string) => void;
clearMessages: () => void; }

const useWebSocketStore = create<WebSocketStore>((set, get) => ({
ws: null,
isConnected: false,
messages: [],

connect: (url: string) => {
const ws = new WebSocket(url);

ws.onopen = () => set({ isConnected: true });
ws.onclose = () => set({ isConnected: false });
ws.onmessage = (event) => {
set(state => ({
messages: [...state.messages, JSON.parse(event.data)]
}));
};

set({ ws });
},

disconnect: () => {
get().ws?.close();
set({ ws: null, isConnected: false });
},

sendMessage: (message: string) => {
get().ws?.send(message); },

clearMessages: () => set({ messages: [] }), }));

// Использование в компонентах
const ChatComponent = () => {
const { messages, sendMessage } = useWebSocketStore();
// Те же данные в другом компоненте
const StatusComponent = () => {
const { isConnected } = useWebSocketStore();
}; };

Сравнительная таблица:

Критерий useWebSocket useWebSocketStore
Масштабируемость Низкая (для одного компонента) Высокая (для всего приложения)
Повторное использование Нужно создавать новое соединение Единое соединение переиспользуется
Сложность Просто Сложнее из-за глобального состояния
Производительность Изолированные соединения Одно соединение - меньше ресурсов
Синхронизация Разные данные в разных компонентах Единый источник истины
Тестируемость Легко тестировать изолированно Требуется mock WebStore
Порог входа Низкий Выше (нужно понимание стейт-менеджеров)

Рекомендации по выбору. Выбирайте useWebSocket, если:

1. У вас простое приложение.
2. Каждый компонент работает со своими данными.
3. Не нужно синхронизировать состояние между компонентами.
4. Хотите минимальную зависимость от внешних библиотек.

Выбирайте useWebSocketStore, если:

1. Несколько компонентов должны реагировать на одни сообщения.
2. Нужна единая точка управления соединением.
3. Требуется сложная логика повторных соединений/авторизации.
4. Хотите избежать дублирования соединений.

Гибридный подход (рекомендованный). Можно создать хук-обертку над WebSocketStore для лучшей безопасности типов (TypeScript):

// Базовое хранилище
const useWebSocketStore = create< WebSocketStore>(...);

// Специализированный хук
const useChatWebSocket = () => {
const {
messages,
sendMessage,
isConnected
} = useWebSocketStore();

const sendChatMessage = useCallback((text: string) => {
sendMessage(JSON.stringify({ type: 'chat', text }));
}, [sendMessage]);

return {
chatMessages: messages.filter(m => m.type === 'chat'),
sendChatMessage,
isConnected
}; };

Такой подход дает преимущества глобального состояния с удобством хуков.

[useWebSocketStore и useWebSocket в контексте Vue и JavaScript]

Vue: useWebSocket (Composable). Базовая реализация Composable на JavaScript:

// composables/useWebSocket.js
import { ref, onUnmounted } from 'vue'

export function useWebSocket(url) {
const messages = ref([])
const isConnected = ref(false)
const error = ref(null)
let socket = null

const connect = () => {
socket = new WebSocket(url)

socket.onopen = () => {
isConnected.value = true
error.value = null
}

socket.onmessage = (event) => {
messages.value.push(event.data)
}

socket.onerror = (e) => {
error.value = e
}

socket.onclose = () => {
isConnected.value = false
}
}

const send = (data) => {
if (socket?.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify(data))
}
}

const disconnect = () => {
socket?.close()
}

onUnmounted(() => {
disconnect()
})

connect()

return {
messages,
isConnected,
error,
send,
disconnect,
reconnect: connect
} }

Использование в компоненте Vue:

< script setup>
import { useWebSocket } from '@/composables/useWebSocket'

const { messages, send, isConnected } = useWebSocket('ws://api.example.com') < /script>

Vue: useWebSocketStore (Pinia Store). Реализация через Pinia на JavaScript:

// stores/websocket.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useWebSocketStore = defineStore('websocket', () => {
const socket = ref(null)
const isConnected = ref(false)
const messages = ref([])
const error = ref(null)

// Геттеры
const unreadCount = computed(() =>
messages.value.filter(m => !m.read).length
)

// Действия
const connect = (url) => {
socket.value = new WebSocket(url)

socket.value.onopen = () => {
isConnected.value = true
error.value = null
}

socket.value.onmessage = (event) => {
messages.value.push({
data: JSON.parse(event.data),
timestamp: new Date(),
read: false
})
}

socket.value.onerror = (e) => {
error.value = e.message
}

socket.value.onclose = () => {
isConnected.value = false
}
}

const send = (data) => {
if (socket.value?.readyState === WebSocket.OPEN) {
socket.value.send(JSON.stringify(data))
}
}

const disconnect = () => {
socket.value?.close()
socket.value = null
}

const markAsRead = (index) => {
if (messages.value[index]) {
messages.value[index].read = true
}
}

const clearMessages = () => {
messages.value = []
}

return {
// Состояние
isConnected,
messages,
error,

// Геттеры
unreadCount,

// Действия
connect,
disconnect,
send,
markAsRead,
clearMessages
} })

Использование в компонентах Vue:

< script setup>
import { useWebSocketStore } from '@/stores/websocket'

const wsStore = useWebSocketStore()

// В любом компоненте приложения
const anotherComponent = () => {
const { isConnected, send } = useWebSocketStore() } < /script>

Чистый JavaScript (без фреймворков). useWebSocket как модуль:

// websocketManager.js
export const createWebSocket = (url) => {
let socket = null
const callbacks = {
onMessage: [],
onOpen: [],
onClose: [],
onError: []
}

const connect = () => {
socket = new WebSocket(url)

socket.onopen = (event) => {
callbacks.onOpen.forEach(cb => cb(event))
}

socket.onmessage = (event) => {
callbacks.onMessage.forEach(cb => cb(event.data))
}

socket.onclose = (event) => {
callbacks.onClose.forEach(cb => cb(event))
}

socket.onerror = (event) => {
callbacks.onError.forEach(cb => cb(event))
}
}

const send = (data) => {
if (socket?.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify(data))
}
}

const on = (event, callback) => {
if (callbacks[event]) {
callbacks[event].push(callback)
}
}

const disconnect = () => {
socket?.close()
}

return {
connect,
disconnect,
send,
on
} }

// Использование
const ws = createWebSocket('ws://api.example.com') ws.on('onMessage', (data) => console.log('Получено:', data)) ws.connect()

useWebSocketStore как глобальный менеджер:

// WebSocketStore.js
class WebSocketStore {
constructor() {
this.socket = null
this.subscribers = new Set()
this.messages = []
this.isConnected = false
}

connect(url) {
this.socket = new WebSocket(url)

this.socket.onopen = () => {
this.isConnected = true
this.notifySubscribers('connection', { connected: true })
}

this.socket.onmessage = (event) => {
const message = JSON.parse(event.data)
this.messages.push(message)
this.notifySubscribers('message', message)
}

this.socket.onclose = () => {
this.isConnected = false
this.notifySubscribers('connection', { connected: false })
}
}

send(data) {
if (this.socket?.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify(data))
}
}

subscribe(callback) {
this.subscribers.add(callback)
return () => this.subscribers.delete(callback)
}

notifySubscribers(event, data) {
this.subscribers.forEach(cb => cb(event, data))
}

getMessages() {
return [...this.messages]
} }

// Синглтон экземпляр
const websocketStore = new WebSocketStore()

// Использование в разных частях приложения
// Модуль A websocketStore.subscribe((event, data) => {
if (event === 'message') {
updateUI(data)
} })

// Модуль B websocketStore.send({ type: 'chat', text: 'Hello' })

// Модуль C
const messages = websocketStore.getMessages()

[Сравнение для Vue]

 

Аспект useWebSocket (Composable) useWebSocketStore (Pinia)
Область видимости Локальная (компонент) Глобальная (приложение)
Повторное использование Новое соединение на каждый вызов Один экземпляр на все приложение
Reactivity [1] Реактивность через ref() в Composable Реактивность через Pinia
DevTools Обычные Vue DevTools Pinia DevTools с историей изменений
SSR Проблемы с гидрацией Лучшая поддержка SSR
Тестирование Легко делать mock в компоненте Централизованное тестирование useWebSocketStore

Что такое "мокать" (mock). Мокирование (mocking) - это техника тестирования, когда мы заменяем реальную зависимость (как WebSocket) на искусственную (mock-объект), чтобы:

- Изолировать тестируемый компонент.
- Контролировать поведение зависимостей.
- Тестировать различные сценарии (успех, ошибки).
- Ускорить тесты (не создавая реальные соединения).

[Примеры мокирования]

1. Мокирование useWebSocket (Composable). Когда Composable используется локально в компоненте, его легко замокать:

// Компонент Chat.vue
< script setup>
import { useWebSocket } from '@/composables/useWebSocket'

const { messages, send } = useWebSocket('ws://chat.example.com') < /script>

Тест компонента:

import { mount } from '@vue/test-utils'
import Chat from '@/components/Chat.vue'

// Мокаем Composable vi.mock('@/composables/useWebSocket', () => ({
useWebSocket: vi.fn(() => ({
messages: ref(['Тестовое сообщение']),
send: vi.fn(),
isConnected: ref(true)
})) }))
test('отображает сообщения', async () => {
const wrapper = mount(Chat)

// Проверяем, что сообщение отображается
expect(wrapper.text()).toContain('Тестовое сообщение')

// Проверяем вызов send
wrapper.find('button').trigger('click')
expect(send).toHaveBeenCalled() })

2. Мокирование useWebSocketStore (Pinia). С Pinia Store подход немного сложнее:

// Тест компонента с Pinia
import { createTestingPinia } from '@pinia/testing'
import Chat from '@/components/Chat.vue'
test('отображает сообщения из стора', async () => {
const wrapper = mount(Chat, {
global: {
plugins: [
createTestingPinia({
stubActions: false, // не заглушать действия
createSpy: vi.fn,
initialState: {
websocket: {
messages: [{ text: 'Тест из стора' }],
isConnected: true
}
}
})
]
}
})

// Теперь нужен доступ к mock-хранилищу
const store = useWebSocketStore()

// Мокаем метод send
store.send = vi.fn()

// Проверяем
expect(wrapper.text()).toContain('Тест из useWebSocketStore') })

Почему Composable "легче мокать"?

1. Прямой импорт и замена. Composable:

// Прямое мокирование
vi.mock('@/composables/useWebSocket')

Pinia:

// Нужно настраивать тестовое окружение
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'

2. Изоляция тестов. Composable: Каждый тест получает свежий mock. Pinia: Нужно очищать состояние между тестами.

3. Простота контролирования состояния. Composable:

// Легко менять состояние между тестами
useWebSocket.mockReturnValueOnce({ /* состояние 1 */ })
useWebSocket.mockReturnValueOnce({ /* состояние 2 */ })

Pinia: Требуется пересоздание хранилища или сброс состояния.

4. Пример сложного сценария с Composable:

// Тестирование разных состояний соединения
describe('Chat компонент', () => {
it('показывает индикатор загрузки при подключении', () => {
useWebSocket.mockReturnValue({
messages: [],
isConnected: false,
send: vi.fn()
})

const wrapper = mount(Chat)
expect(wrapper.find('.loading').exists()).toBe(true)
})

it('показывает сообщения при подключении', () => {
useWebSocket.mockReturnValue({
messages: ['Привет', 'Как дела?'],
isConnected: true,
send: vi.fn()
})

const wrapper = mount(Chat)
expect(wrapper.findAll('.message')).toHaveLength(2)
}) })

[Полный пример сравнения]

Тест Composable (проще):

// Тест компонента с Composable
import { render, screen } from '@testing-library/vue'
import userEvent from '@testing-library/user-event'
import ChatComponent from './ChatComponent.vue'

// Мок Composable
const mockWebSocket = {
messages: ['Hello', 'World'],
isConnected: true, send: vi.fn() }
vi.mock('./useWebSocket', () => ({
useWebSocket: () => mockWebSocket }))
test('отправляет сообщение', async () => {
render(ChatComponent)

await userEvent.type(screen.getByRole('textbox'), 'New message')
await userEvent.click(screen.getByRole('button', { name: 'Send' }))

expect(mockWebSocket.send).toHaveBeenCalledWith('New message') })

Тест с Pinia (сложнее):

// Тест компонента с Pinia
import { createTestingPinia } from '@pinia/testing'
import { render, screen } from '@testing-library/vue'
import ChatComponent from './ChatComponent.vue'
import { useWebSocketStore } from './stores/websocket'

let store
beforeEach(() => {
const { container } = render(ChatComponent, {
global: {
plugins: [createTestingPinia()]
}
})

store = useWebSocketStore()
store.send = vi.fn() // Мокаем метод })
test('отправляет сообщение через стор', async () => {
await userEvent.type(screen.getByRole('textbox'), 'New message')
await userEvent.click(screen.getByRole('button', { name: 'Send' }))

expect(store.send).toHaveBeenCalledWith('New message') })

Практические рекомендации. Когда мокирование композабла действительно проще:

1. Небольшие компоненты - тестируете один компонент изолированно.
2. Быстрые юнит-тесты - нужно быстро проверить логику компонента.
3. Разные состояния для разных тестов - легко менять возвращаемые данные.
4. Тестирование хуков жизненного цикла - можно мокать side effects

Когда Pinia все равно может быть лучше для тестирования:

1. Интеграционные тесты - тестируете взаимодействие нескольких компонентов.
2. Тестирование бизнес-логики в сторе - логика централизована в одном месте.
3. Сложные вычисляемые свойства - тестируются в изоляции хранилища.
4. Тестирование actions с side effects - Pinia предоставляет инструменты для этого

Вывод: "Легко делать mock в компоненте" означает, что при использовании Composable:

- Меньше шаблонного кода для настройки тестов.
- Более прямолинейное и понятное мокирование.
- Лучшая изоляция тестов.
- Проще тестировать edge-cases.

Однако это не означает, что Pinia Store плох для тестирования. Просто он требует немного больше настройки, но взамен дает более мощные инструменты для тестирования сложной бизнес-логики и интеграционных сценариев.

Vue: гибридный подход

// Composable-обертка над хранилищем
export function useWebSocket() {
const store = useWebSocketStore()

// Дополнительная бизнес-логика
const sendChatMessage = (text) => {
store.send({
type: 'chat',
text,
timestamp: Date.now()
})
}

const unreadMessages = computed(() =>
store.messages.filter(m => m.type === 'chat' && !m.read)
)

return {
...storeToRefs(store), // Делаем реактивными
sendChatMessage,
unreadMessages
} }

// Использование - выглядит как обычный Composable
const { messages, sendChatMessage, isConnected } = useWebSocket()

Рекомендации для Vue. Используйте useWebSocket (Composable), когда:

1. Компонент работает с изолированными данными.
2. Нужно быстро добавить WebSocket в один компонент.
3. Работаете над малым проектом или фичей.
4. Не хотите настраивать Pinia.

Используйте useWebSocketStore (Pinia), когда:

1. WebSocket-данные нужны в нескольких компонентах.
2. Требуется централизованное управление соединением.
3. Нужны сложные вычисляемые свойства на основе сообщений.
4. Требуется персистентность состояния.
5. Работаете над средним/крупным проектом.

Чистый JavaScript, рекомендации. Паттерн EventEmitter для масштабируемости:

class WebSocketService extends EventTarget {
constructor(url) {
super()
this.url = url
this.ws = null
this.queue = []
}

connect() {
this.ws = new WebSocket(this.url)

this.ws.onmessage = (event) => {
this.dispatchEvent(new CustomEvent('message', {
detail: JSON.parse(event.data)
}))
}

this.ws.onopen = () => {
this.dispatchEvent(new Event('connected'))
this.flushQueue()
}
}

send(data) {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data))
} else {
this.queue.push(data)
}
}

flushQueue() {
while (this.queue.length > 0) {
this.send(this.queue.shift())
}
} }

// Использование
const wsService = new WebSocketService('ws://api.example.com') wsService.addEventListener('message', (e) => {
console.log('Сообщение:', e.detail) }) wsService.connect()

Для Vue-приложений Pinia Store предпочтительнее для production-проектов, так как предоставляет лучшую архитектуру, тестируемость и масштабируемость. Для прототипов или небольших компонентов достаточно Composable.

[Ссылки]

1. Vue: реактивность.

 

Добавить комментарий


Защитный код
Обновить

Top of Page