Search code examples
react-reduxsocket.ioredux-toolkit

How to store socket object of socket io in slice of redux toolkit?


How to store socket object of socket.io in slice of redux toolkit?

I would like to do something like:

const initialState = {
  socket: null
}

const socketSlice = createSlice({
  name: socket,
  initialState,
  reducers:{
    createSocket(state, action){
      state.socket = io("localhost:5000")
    },
    removeSocket(state, action){
      state.socket = null
    }
    // ...
  }
})

However, this gives the following error:

serializableStateInvariantMiddleware.ts:222 A non-serializable value was detected in the state

Help me...


Solution

  • I had the exact same issue and solved it using the following steps:

    • Create a socket client in which I have a single instance of socket which I use to perform all socket related functions:
    import { io } from 'socket.io-client';
    
    class SocketClient {
      socket;
    
      connect() {
        this.socket = io.connect(process.env.SOCKET_HOST, { transports: ['websocket'] });
        return new Promise((resolve, reject) => {
          this.socket.on('connect', () => resolve());
          this.socket.on('connect_error', (error) => reject(error));
        });
      }
    
      disconnect() {
        return new Promise((resolve) => {
          this.socket.disconnect(() => {
            this.socket = null;
            resolve();
          });
        });
      }
    
      emit(event, data) {
        return new Promise((resolve, reject) => {
          if (!this.socket) return reject('No socket connection.');
    
          return this.socket.emit(event, data, (response) => {
            // Response is the optional callback that you can use with socket.io in every request. See 1 above.
            if (response.error) {
              console.error(response.error);
              return reject(response.error);
            }
    
            return resolve();
          });
        });
      }
    
      on(event, fun) {
        // No promise is needed here, but we're expecting one in the middleware.
        return new Promise((resolve, reject) => {
          if (!this.socket) return reject('No socket connection.');
    
          this.socket.on(event, fun);
          resolve();
        });
      }
    }
    
    export default SocketClient;
    
    • Import it into my index.jsx file and initialize it:
    import SocketClient from './js/services/SocketClient';
    
    export const socketClient = new SocketClient();
    
    • Here's the whole code of my index.jsx file:
    import { createRoot } from 'react-dom/client';
    import { Provider } from 'react-redux';
    //import meta image
    import '@/public/assets/images/metaImage.jpg';
    //styles
    import '@/scss/global.scss';
    //store
    import store from '@/js/store/store';
    //app
    import App from './App';
    //socket client
    import SocketClient from './js/services/SocketClient';
    
    export const socketClient = new SocketClient();
    
    const container = document.getElementById('root'),
      root = createRoot(container);
    
    root.render(
      <Provider store={store}>
        <App />
      </Provider>
    );
    
    • I used createAsyncThunk function from @reduxjs/toolkit, because it automatically generates types like pending, fulfilled and rejected.

    • Here's how I structure my reducer slice to connect and disconnect from web socket in redux:

    import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
    import { socketClient } from '../../../../index';
    
    const initialState = {
      connectionStatus: '',
    };
    
    export const connectToSocket = createAsyncThunk('connectToSocket', async function () {
      return await socketClient.connect();
    });
    
    export const disconnectFromSocket = createAsyncThunk('disconnectFromSocket', async function () {
      return await socketClient.disconnect();
    });
    
    const appSlice = createSlice({
      name: 'app',
      initialState,
      reducers: {},
      extraReducers: (builder) => {
        builder.addCase(connectToSocket.pending, (state) => {
          state.connectionStatus = 'connecting';
        });
        builder.addCase(connectToSocket.fulfilled, (state) => {
          state.connectionStatus = 'connected';
        });
        builder.addCase(connectToSocket.rejected, (state) => {
          state.connectionStatus = 'connection failed';
        });
        builder.addCase(disconnectFromSocket.pending, (state) => {
          state.connectionStatus = 'disconnecting';
        });
        builder.addCase(disconnectFromSocket.fulfilled, (state) => {
          state.connectionStatus = 'disconnected';
        });
        builder.addCase(disconnectFromSocket.rejected, (state) => {
          state.connectionStatus = 'disconnection failed';
        });
      },
    });
    export default appSlice.reducer;
    
    • Here how I connect and disconnect in App.jsx file:
    useEffect(() => {
        dispatch(connectToSocket());
    
        return () => {
          if (connectionStatus === 'connected') {
            dispatch(disconnectFromSocket());
          }
        };
        //eslint-disable-next-line
      }, [dispatch]);
    
    • You can do the following if you want to emit to web socket:
    import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
    import { socketClient } from '../../../../index';
    
    const initialState = {
      messageStatus: '', //ideally it should come from the BE
      messages: [],
      typingUsername: '',
    };
    
    export const sendMessage = createAsyncThunk('sendMessage', async function ({ message, username }) {
      return await socketClient.emit('chat', { message, handle: username });
    });
    
    const chatSlice = createSlice({
      name: 'chat',
      initialState,
      reducers: {},
      extraReducers: (builder) => {
        builder.addCase(sendMessage.pending, (state) => {
          state.messageStatus = 'Sending';
        });
        builder.addCase(sendMessage.fulfilled, (state) => {
          state.messageStatus = 'Sent successfully';
        });
        builder.addCase(sendMessage.rejected, (state) => {
          state.messageStatus = 'Send failed';
        });
      },
    });
    export default chatSlice.reducer;
    
    • You can do the following if you want to listen to an event from web socket:
    import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
    import { socketClient } from '../../../../index';
    
    const initialState = {
      messageStatus: '', //ideally it should come from the BE
      messages: [],
      typingUsername: '',
    };
    
    export const fetchMessages = createAsyncThunk(
      'fetchMessages',
      async function (_, { getState, dispatch }) {
        console.log('state ', getState());
        return await socketClient.on('chat', (receivedMessages) =>
          dispatch({ type: 'chat/saveReceivedMessages', payload: { messages: receivedMessages } })
        );
      }
    );
    
    const chatSlice = createSlice({
      name: 'chat',
      initialState,
      reducers: {
        saveReceivedMessages: (state, action) => {
          state.messages.push(action.payload.messages);
          state.typingUsername = '';
        },
      },
      extraReducers: (builder) => {
        builder.addCase(fetchMessages.pending, () => {
          // add a state if you would like to
        });
        builder.addCase(fetchMessages.fulfilled, () => {
          // add a state if you would like to
        });
        builder.addCase(fetchMessages.rejected, () => {
          // add a state if you would like to
        });
      },
    });
    export default chatSlice.reducer;