'React-Redux and Websockets with socket.io

I'm new with that technology React-Redux and I would like your help with some implementation.

I want to implement one chat application with sockets (socket.io). First, the user has to sign up (I use passport in the server side) and after, if the sign up is successful the user has to connect to the webSocket.

I thought that the best will be to use a middleware like a pipe for all the actions and depending of what kind of action gets the middleware, do different things.

If the action type is AUTH_USER, create client-server connection and set up all the events which will come from the server.

If the action type is MESSAGE send to the server the message.

Code Snippets:

----- socketMiddleware.js ----

import { AUTH_USER,  MESSAGE } from '../actions/types';

import * as actions from 'actions/socket-actions';

import io from 'socket.io-client';

const socket = null;

export default function ({ dispatch }) {

    return next => action => {

        if(action.type == AUTH_USER) {

            socket = io.connect(`${location.host}`);

            socket.on('message', data => {

               store.dispatch(actions.addResponse(action.data));

            });

        }

        else if(action.type == MESSAGE && socket) {

            socket.emit('user-message', action.data);

            return next(action)

        } else {
            return next(action)
        }
    }

}

------ index.js -------

import {createStore, applyMiddleware} from 'redux';

import socketMiddleware from './socketMiddleware';



const createStoreWithMiddleware = applyMiddleware(

  socketMiddleware

)(createStore);

const store = createStoreWithMiddleware(reducer);

<Provider store={store}>

    <App />

</Provider>

What do you think about that practise, is it a better implementation?



Solution 1:[1]

Spoiler: I am currently developing what's going to be an open-source chat application.

You can do that better by separating actions from the middleware, and even the socket client from the middleware. Hence, resulting in something like this:

  • Types -> REQUEST, SUCCESS, FAILURE types for every request (not mandatory).
  • Reducer -> to store different states
  • Actions -> send actions to connect / disconnect / emit / listen.
  • Middleware -> to treat your actions, and pass or not the current action to the socket client
  • Client -> socket client (socket.io).

The code below is taken from the real app which is under development (sometimes slightly edited), and they are enough for the majority of situations, but certain stuff like the SocketClient might not be 100% complete.

Actions

You want actions to be as simple as possible, since they are often repeated work and you'll probably end up having lots of them.

export function send(chatId, content) {
  const message = { chatId, content };
  return {
    type: 'socket',
    types: [SEND, SEND_SUCCESS, SEND_FAIL],
    promise: (socket) => socket.emit('SendMessage', message),
  }
}

Notice that socket is a parametrized function, this way we can share the same socket instance throughout the whole application and we don't have to worry about any import whatsoever (we'll show how to do this later).

Middleware (socketMiddleware.js):

We'll use a similar strategy as erikras/react-redux-universal-hot-example uses, though for socket instead of AJAX.

Our socket middleware will be responsible for processing only socket requests.

Middleware passes the action onto the socket client, and dispatches:

  • REQUEST (action types[0]): is requesting (action.type is sent to reducer).
  • SUCCESS (action types[1]): on request success (action.type and server response as action.result is sent to reducer).
  • FAILURE (action types[2]): on request failure (action.type and server response as action.error are sent to reducer).
export default function socketMiddleware(socket) {
  // Socket param is the client. We'll show how to set this up later.
  return ({dispatch, getState}) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState);
    }

    /*
     * Socket middleware usage.
     * promise: (socket) => socket.emit('MESSAGE', 'hello world!')
     * type: always 'socket'
     * types: [REQUEST, SUCCESS, FAILURE]
     */
    const { promise, type, types, ...rest } = action;

    if (type !== 'socket' || !promise) {
      // Move on! Not a socket request or a badly formed one.
      return next(action);
    }

    const [REQUEST, SUCCESS, FAILURE] = types;
    next({...rest, type: REQUEST});

    return promise(socket)
      .then((result) => {
        return next({...rest, result, type: SUCCESS });
      })
      .catch((error) => {
        return next({...rest, error, type: FAILURE });
      })
  };
}

SocketClient.js

The only one that will ever load and manage the socket.io-client.

[optional] (see 1 below in the code). One very interesting feature about socket.io is the fact that you can have message acknowledgements, which would be the typical replies when doing an HTTP request. We can use them to verify that each request was correct. Note that in order to make use of this feature server socket.io commands do also have to have this latest acknowledgement parameter.

import io from 'socket.io-client';

// Example conf. You can move this to your config file.
const host = 'http://localhost:3000';
const socketPath = '/api/socket.io';

export default class socketAPI {
  socket;

  connect() {
    this.socket = io.connect(host, { path: socketPath });
    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();
    });
  }
}

app.js

On our app start-up, we initialize the SocketClient and pass it to the store configuration.

const socketClient = new SocketClient();
const store = configureStore(initialState, socketClient, apiClient);

configureStore.js

We add the socketMiddleware with our newly initialized SocketClient to the store middlewares (remember that parameter which we told you we would explain later?).

export default function configureStore(initialState, socketClient, apiClient) {
const loggerMiddleware = createLogger();
const middleware = [
  ...
  socketMiddleware(socketClient),
  ...
];

[Nothing special] Action types constants

Nothing special = what you would normally do.

const SEND = 'redux/message/SEND';
const SEND_SUCCESS = 'redux/message/SEND_SUCCESS';
const SEND_FAIL = 'redux/message/SEND_FAIL';

[Nothing special] Reducer

export default function reducer(state = {}, action = {}) {
  switch(action.type) {
    case SEND: {
      return {
        ...state,
        isSending: true,
      };
    }
    default: {
      return state;
    }
  }
}

It might look like a lot of work, but once you have set it up it is worth it. Your relevant code will be easier to read, debug and you will be less prone to make mistakes.

PS: You can follow this strategy with AJAX API calls as well.

Solution 2:[2]

For this purpose I used createAsyncThunk function from @reduxjs/toolkit. It automatically generates types like pending, fulfilled and rejected.

I used kind of the same socketService as @zurfyx in his answer.

The action looks like this:

const sendMessage = createAsyncThunk(
  'game/send-message',
  async function (text, { getState }) {
    const roomToken = selectRoomToken(getState());
    return await socketService.emit('send-message', { text, roomToken });
  }
);

And the reducer looks like this:

const gameSlice = createSlice({
  name: 'game',
  initialState: { },
  reducers: {},
  extraReducers: {
    [sendMessage.pending]: (state, action) => {
      state.messages.push({
        id: action.meta.requestId,
        text: action.meta.arg,
        my: true,
      });
    },
    [sendMessage.rejected]: (state, action) => {
      state.messages = state.messages.filter(
        ms => ms.id !== action.meta.requestId
      );
    },
  },
});

Solution 3:[3]

Working solution with middleware:

import { Middleware } from 'redux';
import { io, Socket } from 'socket.io-client';

import { RootState } from '../index';
import { SERVER_URL } from '../../api/consts';
import { actions } from '../features/chat/slice';

export const socketMiddleware: Middleware = (store) => {
    let socket: Socket;

    return (next) => (action) => {
        const state = store.getState() as RootState;
        const needInitSocket = state.auth.isAuth && !socket;

        if (needInitSocket) {
            socket = io(SERVER_URL, { transports: ['websocket', 'polling'], withCredentials: true });

            socket.on('connect', () => {
                store.dispatch(actions.connectionEstablished());
            });

            socket.on('YOUR_EVENT', (data) => {
               store.dispatch(actions.doSomething(data));
            })
        }

        // to emit data to server
        if (actions.sendMessage.match(action) && socket) {
            socket.emit('ON_ROOM_MESSAGE', action.payload);
        }

        next(action);
    };
};

I worked with redux-toolkit

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 zurfyx
Solution 2 landorid
Solution 3 zemil