← Назад к вопросам

Как можно работать с WebSocket в React?

2.3 Middle🔥 271 комментариев
#React

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Работа с WebSocket в React

WebSocket — это протокол для двусторонней коммуникации между клиентом и сервером. В отличие от HTTP, WebSocket позволяет серверу отправлять данные клиенту без явного запроса. Это идеально для real-time приложений: чат, уведомления, игры, live обновления.

Основы WebSocket API

// Подключение к WebSocket серверу
const ws = new WebSocket('ws://localhost:8080');

// События жизненного цикла
ws.addEventListener('open', (event) => {
  console.log('Connected');
  ws.send('Hello Server!');
});

ws.addEventListener('message', (event) => {
  console.log('Message from server:', event.data);
});

ws.addEventListener('error', (event) => {
  console.error('WebSocket error:', event);
});

ws.addEventListener('close', (event) => {
  console.log('Disconnected');
});

// Отправка данных
ws.send('Some message');

// Закрытие соединения
ws.close();

Решение 1: Простой хук useWebSocket

Создаём кастомный хук для управления WebSocket подключением:

import { useState, useEffect, useRef, useCallback } from 'react';

function useWebSocket(url, options = {}) {
  const [state, setState] = useState({
    data: null,
    error: null,
    isConnected: false
  });

  const wsRef = useRef(null);
  const reconnectAttemptsRef = useRef(0);
  const maxReconnectAttempts = options.maxReconnectAttempts || 5;

  const connect = useCallback(() => {
    try {
      const ws = new WebSocket(url);

      ws.onopen = () => {
        setState(prev => ({ ...prev, isConnected: true, error: null }));
        reconnectAttemptsRef.current = 0;
        console.log('WebSocket connected');
      };

      ws.onmessage = (event) => {
        try {
          const message = JSON.parse(event.data);
          setState(prev => ({ ...prev, data: message }));
        } catch {
          setState(prev => ({ ...prev, data: event.data }));
        }
      };

      ws.onerror = (error) => {
        console.error('WebSocket error:', error);
        setState(prev => ({ ...prev, error: error.message }));
      };

      ws.onclose = () => {
        setState(prev => ({ ...prev, isConnected: false }));
        
        // Автоматическое переподключение
        if (reconnectAttemptsRef.current < maxReconnectAttempts) {
          reconnectAttemptsRef.current++;
          const delay = Math.min(1000 * Math.pow(2, reconnectAttemptsRef.current), 30000);
          setTimeout(() => {
            console.log(`Reconnecting... Attempt ${reconnectAttemptsRef.current}`);
            connect();
          }, delay);
        }
      };

      wsRef.current = ws;
    } catch (error) {
      setState(prev => ({ ...prev, error: error.message }));
    }
  }, [url, maxReconnectAttempts]);

  const send = useCallback((message) => {
    if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
      wsRef.current.send(JSON.stringify(message));
    } else {
      console.warn('WebSocket is not connected');
    }
  }, []);

  const disconnect = useCallback(() => {
    if (wsRef.current) {
      wsRef.current.close();
      wsRef.current = null;
    }
  }, []);

  useEffect(() => {
    connect();

    return () => {
      disconnect();
    };
  }, [connect, disconnect]);

  return {
    ...state,
    send,
    disconnect
  };
}

Использование:

function ChatComponent() {
  const { data, isConnected, error, send } = useWebSocket('ws://localhost:8080');
  const [messages, setMessages] = useState([]);
  const [input, setInput] = useState('');

  useEffect(() => {
    if (data) {
      setMessages(prev => [...prev, data]);
    }
  }, [data]);

  const handleSend = (e) => {
    e.preventDefault();
    if (input.trim()) {
      send({ type: 'message', content: input });
      setInput('');
    }
  };

  return (
    <div>
      <div>Status: {isConnected ? 'Connected' : 'Disconnected'}</div>
      {error && <div className="error">{error}</div>}
      
      <div className="messages">
        {messages.map((msg, idx) => (
          <div key={idx}>{msg.content}</div>
        ))}
      </div>

      <form onSubmit={handleSend}>
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          placeholder="Type a message"
          disabled={!isConnected}
        />
        <button type="submit" disabled={!isConnected}>
          Send
        </button>
      </form>
    </div>
  );
}

Решение 2: Использование Context для глобального подключения

Если несколько компонентов нужно подключены к одному WebSocket:

import React, { createContext, useContext, useEffect, useRef, useState } from 'react';

const WebSocketContext = createContext();

function WebSocketProvider({ url, children }) {
  const [isConnected, setIsConnected] = useState(false);
  const wsRef = useRef(null);
  const [listeners, setListeners] = useState({});

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

    ws.onopen = () => setIsConnected(true);
    ws.onmessage = (event) => {
      const data = JSON.parse(event.data);
      const { type } = data;
      
      if (listeners[type]) {
        listeners[type].forEach(cb => cb(data));
      }
    };
    ws.onclose = () => setIsConnected(false);

    wsRef.current = ws;

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

  const send = (message) => {
    if (wsRef.current?.readyState === WebSocket.OPEN) {
      wsRef.current.send(JSON.stringify(message));
    }
  };

  const on = (type, callback) => {
    setListeners(prev => ({
      ...prev,
      [type]: [...(prev[type] || []), callback]
    }));
  };

  const off = (type, callback) => {
    setListeners(prev => ({
      ...prev,
      [type]: (prev[type] || []).filter(cb => cb !== callback)
    }));
  };

  return (
    <WebSocketContext.Provider value={{ isConnected, send, on, off }}>
      {children}
    </WebSocketContext.Provider>
  );
}

function useWebSocketContext() {
  return useContext(WebSocketContext);
}

Использование:

function NotificationComponent() {
  const { isConnected, on } = useWebSocketContext();
  const [notifications, setNotifications] = useState([]);

  useEffect(() => {
    const handleNotification = (data) => {
      setNotifications(prev => [...prev, data.message]);
    };

    on('notification', handleNotification);

    // Cleanup
    return () => {
      // Удалить слушателя
    };
  }, [on]);

  return (
    <div>
      {notifications.map((notif, idx) => (
        <div key={idx}>{notif}</div>
      ))}
    </div>
  );
}

function App() {
  return (
    <WebSocketProvider url="ws://localhost:8080">
      <ChatComponent />
      <NotificationComponent />
    </WebSocketProvider>
  );
}

Решение 3: Использование библиотеки socket.io

Socket.IO — популярная библиотека с fallback'ами для старых браузеров:

import io from 'socket.io-client';
import { useEffect, useState, useRef } from 'react';

function useSocket(url) {
  const [isConnected, setIsConnected] = useState(false);
  const [data, setData] = useState(null);
  const socketRef = useRef(null);

  useEffect(() => {
    const socket = io(url, {
      reconnection: true,
      reconnectionDelay: 1000,
      reconnectionDelayMax: 5000,
      reconnectionAttempts: 5
    });

    socket.on('connect', () => setIsConnected(true));
    socket.on('disconnect', () => setIsConnected(false));
    socket.on('message', setData);

    socketRef.current = socket;

    return () => socket.disconnect();
  }, [url]);

  const emit = (event, data) => {
    socketRef.current?.emit(event, data);
  };

  return { isConnected, data, emit };
}

Практический пример: Live Chat

function LiveChat() {
  const { data, isConnected, send } = useWebSocket('ws://localhost:8080');
  const [messages, setMessages] = useState([]);
  const [users, setUsers] = useState([]);
  const [input, setInput] = useState('');
  const messagesEndRef = useRef(null);

  // Обработка входящих сообщений
  useEffect(() => {
    if (!data) return;

    if (data.type === 'message') {
      setMessages(prev => [...prev, data]);
    } else if (data.type === 'user_joined') {
      setUsers(prev => [...prev, data.user]);
    } else if (data.type === 'user_left') {
      setUsers(prev => prev.filter(u => u.id !== data.userId));
    }
  }, [data]);

  // Автоскролл к последнему сообщению
  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [messages]);

  const handleSend = () => {
    if (input.trim() && isConnected) {
      send({
        type: 'message',
        text: input,
        timestamp: new Date().toISOString()
      });
      setInput('');
    }
  };

  return (
    <div className="chat-container">
      <div className="status">
        {isConnected ? 'Connected' : 'Disconnected'}
      </div>

      <div className="messages">
        {messages.map((msg, idx) => (
          <div key={idx} className="message">
            <strong>{msg.user}</strong>: {msg.text}
          </div>
        ))}
        <div ref={messagesEndRef} />
      </div>

      <div className="input-area">
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyPress={(e) => e.key === 'Enter' && handleSend()}
          placeholder="Type a message..."
        />
        <button onClick={handleSend} disabled={!isConnected}>
          Send
        </button>
      </div>

      <div className="users">
        <h3>Users Online ({users.length})</h3>
        {users.map(user => (
          <div key={user.id}>{user.name}</div>
        ))}
      </div>
    </div>
  );
}

Обработка ошибок и восстановления

function useRobustWebSocket(url, options = {}) {
  const [state, setState] = useState({
    isConnected: false,
    lastMessage: null,
    error: null,
    retryCount: 0
  });

  const wsRef = useRef(null);
  const retryTimeoutRef = useRef(null);

  const connect = useCallback(() => {
    try {
      const ws = new WebSocket(url);

      ws.onopen = () => {
        setState(prev => ({
          ...prev,
          isConnected: true,
          error: null,
          retryCount: 0
        }));
      };

      ws.onmessage = (event) => {
        setState(prev => ({
          ...prev,
          lastMessage: JSON.parse(event.data)
        }));
      };

      ws.onerror = () => {
        setState(prev => ({
          ...prev,
          error: 'Connection failed',
          isConnected: false
        }));
      };

      ws.onclose = () => {
        setState(prev => ({
          ...prev,
          isConnected: false
        }));

        // Экспоненциальная задержка перед переподключением
        if (state.retryCount < (options.maxRetries || 5)) {
          const delay = Math.pow(2, state.retryCount) * 1000;
          retryTimeoutRef.current = setTimeout(connect, delay);
          
          setState(prev => ({
            ...prev,
            retryCount: prev.retryCount + 1
          }));
        }
      };

      wsRef.current = ws;
    } catch (error) {
      setState(prev => ({
        ...prev,
        error: error.message
      }));
    }
  }, [url, state.retryCount, options]);

  useEffect(() => {
    connect();

    return () => {
      if (retryTimeoutRef.current) {
        clearTimeout(retryTimeoutRef.current);
      }
      wsRef.current?.close();
    };
  }, [connect]);

  return state;
}

Выводы

  • WebSocket для двусторонней real-time коммуникации
  • Создавай кастомные хуки для управления подключением
  • Обработай переподключение и ошибки
  • Используй Context для общего состояния несколькими компонентами
  • socket.io предоставляет fallback'и для старых браузеров
  • Правильно очищай соединение при размонтировании компонента
  • Не забывай обработку сбоев сети и graceful degradation
Как можно работать с WebSocket в React? | PrepBro