import {useCallback, useEffect, useReducer, useState} from "react";
import {useDispatch} from "react-redux";
import {setFuturesBinancePriceState, setSpotBinancePriceState} from "../store/binancePriceSlice";

export enum ReadyState {
  UNINSTANTIATED = -1,
  CONNECTING = 0,
  OPEN = 1,
  CLOSING = 2,
  CLOSED = 3,
}

export function useBinancePrice() {
  const stream = "@ticker";
  const dispatch = useDispatch();
  const [futuresWs, setFuturesWs] = useState<WebSocket|null>(null);
  const [futuresReadyState, setFuturesReadyState] = useState(ReadyState.UNINSTANTIATED);
  const [spotWs, setSpotWs] = useState<WebSocket|null>(null);
  const [spotReadyState, setSpotReadyState] = useState(0);
console.log(spotWs);

  const connectToWs = () => {
    if ("undefined" === typeof window) {
      setTimeout("connectToWs", 1000);

      return;
    }

    setFuturesWs(new WebSocket("wss://fstream.binance.com/stream"));
    setSpotWs(new WebSocket("wss://stream.binance.com:9443/stream"));
  };

  const futuresSubscribe = useCallback((symbol: string) => {
    if (null === futuresWs || futuresReadyState !== ReadyState.OPEN) return;

    futuresWs.send(JSON.stringify({
      id: ((new Date()).getTime() + Math.random() * 10000).toFixed(0),
      method: "SUBSCRIBE",
      params: [
        `${symbol}${stream}`,
      ],
    }));
  }, [futuresReadyState]);

  const futuresUnsubscribe = useCallback((symbol: string) => {
    if (null === futuresWs || futuresReadyState !== ReadyState.OPEN) return;

    futuresWs.send(JSON.stringify({
      id: ((new Date()).getTime() + Math.random() * 10000).toFixed(0),
      method: "UNSUBSCRIBE",
      params: [
        `${symbol}${stream}`,
      ],
    }));
  }, [futuresReadyState]);

  useEffect(() => {
    if (null === futuresWs) return;

    futuresWs.onmessage = (message: MessageEvent) => {
      if (!message.data) return;

      const data = JSON.parse(message.data);

      if (!data?.stream) return;
      // console.log(data.stream);

      dispatch(setFuturesBinancePriceState({symbol: data.stream.replace(stream, ""), price: data.data.c}));
    };

    futuresWs.onopen = () => {
      setFuturesReadyState(ReadyState.OPEN);
    };

    futuresWs.onclose = () => {
      setFuturesReadyState(ReadyState.CLOSED);
    };
  }, [futuresWs, futuresWs, stream]);

  const spotSubscribe = useCallback((symbol: string) => {
    if (null === spotWs || spotReadyState !== ReadyState.OPEN) return;

    spotWs.send(JSON.stringify({
      id: ((new Date()).getTime() + Math.random() * 10000).toFixed(0),
      method: "SUBSCRIBE",
      params: [
        `${symbol}${stream}`,
      ],
    }));
  }, [spotReadyState]);

  const spotUnsubscribe = useCallback((symbol: string) => {
    if (null === spotWs || spotReadyState !== ReadyState.OPEN) return;

    spotWs.send(JSON.stringify({
      id: ((new Date()).getTime() + Math.random() * 10000).toFixed(0),
      method: "UNSUBSCRIBE",
      params: [
        `${symbol}${stream}`,
      ],
    }));
  }, [spotReadyState]);

  useEffect(() => {
    if (null === spotWs) return;

    spotWs.onmessage = (message: MessageEvent) => {
      console.log(message);
      if (!message.data) return;

      const data = JSON.parse(message.data);

      if (!data?.stream) return;
      
      dispatch(setSpotBinancePriceState({symbol: data.stream.replace(stream, ""), price: data.data.c}));
    };

    spotWs.onopen = () => {
      setSpotReadyState(ReadyState.OPEN);
    };

    spotWs.onclose = () => {
      setSpotReadyState(ReadyState.CLOSED);
    };
  }, [spotWs]);

  useEffect(() => {
    connectToWs();
  }, []);

  return {
    futuresSubscribe,
    futuresUnsubscribe,
    futuresReadyState,
    spotSubscribe,
    spotUnsubscribe,
    spotReadyState,
  };
}
import {createSlice} from "@reduxjs/toolkit";
import {HYDRATE} from "next-redux-wrapper";

export interface BinancePriceSlice {
  futures: {[key: string]: number};
  spot: {[key: string]: number};
}

const initialState: BinancePriceSlice = {
  futures: {},
  spot: {},
};

export const binancePriceSlice = createSlice({
  name: "binancePrice",
  initialState,
  reducers: {
    setFuturesBinancePriceState(state, action) {
      state.futures[action.payload.symbol] = action.payload.price;
    },
    setSpotBinancePriceState(state, action) {
      console.log(action);
      state.spot[action.payload.symbol] = action.payload.price;
    },
  },
  extraReducers: {
    [HYDRATE]: (state, action) => {
      return {
        ...state,
        ...action.payload,
      };
    },
  },
});

export const {
  setFuturesBinancePriceState,
  setSpotBinancePriceState,
} = binancePriceSlice.actions;

export default binancePriceSlice.reducer;

import {BinanceWsProviderProps} from "./BinanceWsProvider.props";
import {ReadyState, useBinancePrice} from "../../hooks/binancePrices";
import {createContext, useContext, useEffect, useState} from "react";

interface BinanceWsProviderContext {
  futuresSubscribe: (symbol: string) => void;
  futuresUnsubscribe: (symbol: string) => void;
  futuresReadyState: number;
  spotSubscribe: (symbol: string) => void;
  spotUnsubscribe: (symbol: string) => void;
  spotReadyState: number;
}

const defaultContextValue: BinanceWsProviderContext = {
  futuresSubscribe: (symbol: string) => {},
  futuresUnsubscribe: (symbol: string) => {},
  futuresReadyState: 0,
  spotSubscribe: (symbol: string) => {},
  spotUnsubscribe: (symbol: string) => {},
  spotReadyState: 0,
};

const Context = createContext(defaultContextValue);

export function useBinanceWsProvider() {
  return useContext(Context);
}

const BinanceWsProvider = ({children}: BinanceWsProviderProps) => {
  const [futuresSymbols, setFuturesSymbols] = useState<Array<string>>([]);
  const [futuresQueue, setFuturesQueue] = useState<Array<string>>([]);
  const [spotSymbols, setSpotSymbols] = useState<Array<string>>([]);
  const [spotQueue, setSpotQueue] = useState<Array<string>>([]);
  const {
    futuresSubscribe,
    futuresUnsubscribe,
    futuresReadyState,
    spotSubscribe,
    spotUnsubscribe,
    spotReadyState,
  } = useBinancePrice();

  const futuresSymbolSubscribe = (symbol: string) => {
    if (futuresSymbols.includes(symbol) || futuresQueue.includes(symbol)) return;

    setFuturesQueue(prev => [...prev, symbol]);
  };

  const futuresSymbolUnsubscribe = (symbol: string) => {
    if (!futuresSymbols.includes(symbol) && !futuresQueue.includes(symbol)) return;

    // TODO: сделать счетчик слушателей, если все отпишутся - отправлять UNSUBSCRIBE
    // futuresUnsubscribe(symbol);
    setFuturesQueue(prev => prev.filter(item => item !== symbol));
  };

  const subscribeFromFuturesQueue = () => {
    futuresQueue.forEach(symbol => {
      futuresSubscribe(symbol);
    });
    setFuturesSymbols([...futuresSymbols, ...futuresQueue]);
    setFuturesQueue([]);
  };

  useEffect(() => {
    if (futuresQueue.length > 0 && futuresReadyState === ReadyState.OPEN) {
      subscribeFromFuturesQueue();
    }
  }, [futuresQueue]);

  useEffect(() => {
    if (futuresReadyState === ReadyState.CLOSED) {
      setFuturesQueue([...futuresSymbols, ...futuresQueue]);
      setFuturesSymbols([]);
    }

    if (futuresQueue.length > 0 && futuresReadyState === ReadyState.OPEN) {
      subscribeFromFuturesQueue();
    }
  }, [futuresReadyState]);

  const spotSymbolSubscribe = (symbol: string) => {
    if (spotSymbols.includes(symbol) || spotQueue.includes(symbol)) return;

    setSpotQueue(prev => [...prev, symbol]);
  };

  const spotSymbolUnsubscribe = (symbol: string) => {
    if (!spotSymbols.includes(symbol) && !spotQueue.includes(symbol)) return;

    // TODO: сделать счетчик слушателей, если все отпишутся - отправлять UNSUBSCRIBE
    // spotUnsubscribe(symbol);
    setSpotQueue(prev => prev.filter(item => item !== symbol));
  };

  const subscribeFromSpotQueue = () => {
    spotQueue.forEach(symbol => {
      spotSubscribe(symbol);
    });
    setSpotSymbols([...spotSymbols, ...spotQueue]);
    setSpotQueue([]);
  };

  useEffect(() => {
    if (spotQueue.length > 0 && spotReadyState === ReadyState.OPEN) {
      subscribeFromSpotQueue();
    }
  }, [spotQueue]);

  useEffect(() => {
    if (spotReadyState === ReadyState.CLOSED) {
      setSpotQueue([...spotSymbols, ...spotQueue]);
      setSpotSymbols([]);
    }

    if (spotQueue.length > 0 && spotReadyState === ReadyState.OPEN) {
      subscribeFromSpotQueue();
    }
  }, [spotReadyState]);

  return <Context.Provider
    value={{
      futuresSubscribe: futuresSymbolSubscribe,
      futuresUnsubscribe: futuresSymbolUnsubscribe,
      futuresReadyState: futuresReadyState,
      spotSubscribe: spotSymbolSubscribe,
      spotUnsubscribe: spotSymbolUnsubscribe,
      spotReadyState: spotReadyState,
    }}
  >
    {children}
  </Context.Provider>;
};

export default BinanceWsProvider;
Надо разобраться с подключением к вебсокетам бинанса на фронте. Они через раз срабатывают