Работа с WebSocket в React чаще всего строится вокруг управления
жизненным циклом соединения и подписок через useEffect, а
также хранения состояния сообщений в useState или внешних
сторах. При использовании STOMP поверх WebSocket ключевая задача
заключается в корректной инициализации клиента, контроле переподключений
и изоляции подписок от повторных рендеров компонентов.
Основной принцип архитектуры: STOMP-клиент существует вне рендера, а React-хуки управляют его жизненным циклом.
Типовой сценарий начинается с создания клиента и установки соединения при монтировании компонента.
import { useEffect, useRef, useState } from "react";
import { Client } from "stompjs";
В современных проектах чаще используется пакет
@stomp/stompjs, однако концептуально подход остается
одинаковым: клиент инкапсулируется и хранится в useRef.
const Chat = () => {
const clientRef = useRef(null);
const [messages, setMessages] = useState([]);
};
Использование useRef критично: оно предотвращает
пересоздание STOMP-клиента при каждом рендере, сохраняя единственный
экземпляр соединения.
Подключение выполняется внутри useEffect с пустым
массивом зависимостей, чтобы обеспечить однократное выполнение
логики.
useEffect(() => {
const client = new Client({
brokerURL: "ws://localhost:8080/ws",
reconnectDelay: 5000,
debug: (str) => {
console.log(str);
}
});
client.onConn ect = () => {
console.log("connected");
client.subscribe("/topic/messages", (message) => {
const body = JSON.parse(message.body);
setMessages((prev) => [...prev, body]);
});
};
client.onStompEr ror = (frame) => {
console.error("Broker error:", frame.headers["message"]);
};
client.activate();
clientRef.current = client;
return () => {
client.deactivate();
};
}, []);
Ключевые моменты:
activate() инициирует соединениеdeactivate() гарантирует корректное закрытие
сокетаsubscribe() выполняется только после успешного
подключенияsetMessages использует функциональное обновление для
защиты от гонок состоянияЧастая ошибка при использовании STOMP в React — повторная подписка при каждом рендере или повторном подключении.
Для контроля подписок используется хранение subscription-объекта:
const subscriptionRef = useRef(null);
И дальнейшая регистрация:
client.onConn ect = () => {
if (subscriptionRef.current) {
subscriptionRef.current.unsubscribe();
}
subscriptionRef.current = client.subscribe("/topic/messages", (message) => {
const body = JSON.parse(message.body);
setMessages((prev) => [...prev, body]);
});
};
Такой подход исключает накопление подписок при реконнектах.
Отправка данных реализуется через метод publish (или
send в старых версиях).
const sendMessage = (text) => {
if (!clientRef.current || !clientRef.current.connected) return;
clientRef.current.publish({
destination: "/app/chat",
body: JSON.stringify({
text,
timestamp: Date.now()
})
});
};
Особенности:
connecteddestination на endpoint отправки и топики
подпискиМодель хранения сообщений должна учитывать потоковую природу данных.
const [messages, setMessages] = useState([]);
Обновление состояния через функциональный паттерн:
setMessages((prev) => [...prev, newMessage]);
Этот подход предотвращает потерю данных при конкурентных обновлениях, особенно при высокой частоте входящих сообщений.
Вынос логики в кастомный хук позволяет отделить транспортный слой от UI.
import { useEffect, useRef, useState } from "react";
import { Client } from "stompjs";
export const useStompClient = (url, topic) => {
const clientRef = useRef(null);
const subscriptionRef = useRef(null);
const [messages, setMessages] = useState([]);
useEffect(() => {
const client = new Client({
brokerURL: url,
reconnectDelay: 3000
});
client.onConn ect = () => {
subscriptionRef.current = client.subscribe(topic, (msg) => {
setMessages((prev) => [...prev, JSON.parse(msg.body)]);
});
};
client.activate();
clientRef.current = client;
return () => client.deactivate();
}, [url, topic]);
const send = (destination, payload) => {
if (!clientRef.current?.connected) return;
clientRef.current.publish({
destination,
body: JSON.stringify(payload)
});
};
return { messages, send };
};
STOMP-клиент поддерживает автоматическое переподключение, но логика приложения должна учитывать потерю состояния подписок.
При каждом onConnect необходимо:
client.onWebSocketCl ose = () => {
console.log("socket closed");
};
Дополнительно можно отслеживать:
onWebSocketErroronStompErrorbeforeConnectПри работе с несколькими потоками данных структура расширяется через словарь подписок.
const subscriptions = useRef({});
Регистрация:
subscriptions.current["chat"] = client.subscribe("/topic/chat", handler);
subscriptions.current["notifications"] = client.subscribe("/topic/notifications", handler2);
Очистка:
Object.values(subscriptions.current).forEach((sub) => sub.unsubscribe());
subscriptions.current = {};
Высокочастотные сообщения могут приводить к перегрузке React-рендера. Для оптимизации применяется:
setTimeoutuseReducerПример через буфер:
const bufferRef = useRef([]);
useEffect(() => {
const interval = setInterval(() => {
if (bufferRef.current.length === 0) return;
setMessages((prev) => [...prev, ...bufferRef.current]);
bufferRef.current = [];
}, 200);
return () => clearInterval(interval);
}, []);
При получении сообщения:
bufferRef.current.push(newMessage);
Аутентификация часто передается через headers при подключении:
const client = new Client({
brokerURL: "ws://localhost:8080/ws",
connectHeaders: {
Authorization: "Bearer token"
}
});
При обновлении токена требуется:
Корректная архитектура React-приложения с STOMP подразумевает:
Пример использования context:
const StompContext = createContext(null);
Провайдер управляет жизненным циклом клиента, а компоненты только потребляют данные и методы отправки сообщений.
Основные источники утечек:
deactivate()Корректный cleanup:
return () => {
Object.values(subscriptions.current).forEach((s) => s.unsubscribe());
client.deactivate();
};
Такая модель гарантирует освобождение ресурсов при размонтировании компонентов и смене маршрутов в SPA.