27

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?

zurfyx
  • 23,843
  • 15
  • 103
  • 130
Ganbel
  • 427
  • 1
  • 6
  • 11
  • 1
    In general this type of question is not a good fit for stackoverflow, but yes, your approach looks reasonable. Only thing that jumps at me as "weird" is only conditionally calling `next(action)`. Normally you want to call next middleware (dispatcher), e.g. to let it handle logging or anything else that you might have there. – WTK Jun 17 '16 at 13:59
  • 3
    This looks fair enough, but I would really recommend a look into redux-saga middleware, which will let you ```take``` a specific action and spin up a generator for it. From there you could set up more advanced logic, such as multiple channels, unsubscribe, etc. :) – horyd Jun 30 '16 at 17:46
  • @WTK regarding to call next middleware, shall I add an else condition to send to the next middleware? – Ganbel Dec 05 '16 at 10:29
  • @horyd It sounds good that redux-saga, but in my case, I do not need any special advanced logic because all the time I will be sending to one socket the same messages. So in that case, do you have some idea how can I implement better that approach? – Ganbel Dec 05 '16 at 10:38

2 Answers2

47

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.

zurfyx
  • 23,843
  • 15
  • 103
  • 130
  • 1
    Works like a charm! I've been trying to figure out on how to actually get messages back to the client. I'm going to assume that you are suppose to use the on(event, fun)-function for this? – Viktor Sarström Jan 10 '17 at 14:54
  • @ViktorSarström That is the client `on` indeed. You can set it up in an action by using `(client) => client.on('event', fun)` – zurfyx Jan 10 '17 at 15:11
  • Manage to figure it out! Thanks! – Viktor Sarström Jan 10 '17 at 15:14
  • Also, it seems taht only the "SEND" event is triggered. NOT SUCCESS nor FAILURE. Though I am receiving the data on the socket server – Viktor Sarström Jan 10 '17 at 15:27
  • @ViktorSarström make sure your server is using the *optional* callback function on emit. Otherwise, your client will be waiting forever for its response. http://socket.io/docs/#sending-and-getting-data-(acknowledgements) – zurfyx Jan 10 '17 at 16:04
  • Have you managed to implement an on function for listening on "SendMessage" from the server? – Viktor Sarström Jan 11 '17 at 08:57
  • @ViktorSarström it is exactly the same. `socket.on('SendMessage', (data) => { // do something with the data });` http://socket.io/docs/ – zurfyx Jan 11 '17 at 08:59
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/132887/discussion-between-viktor-sarstrom-and-zurfyx). – Viktor Sarström Jan 11 '17 at 09:04
  • I have the same question @zurfyx, when is the connect called and from where? – Rajesh Jain May 11 '17 at 12:48
  • @RajeshJain from an action of your own, whenever you need that connection open. Often, you will just need a socket connection in specific pages of your site, so you should be starting the connection only when you are going to need it (and closing it after). `promise: socket => socket.connect()` – zurfyx May 14 '17 at 14:32
  • Thanks for the fantastic answer. Just one question: How do you handle the jwt token for authorization? Because client side 'local storage' and 'client sessions' are not accessible. Server side, coockies (for what I know) are not really safe. How do you solve the problem? – Pibo Jul 17 '17 at 12:47
  • 1
    @Pibo It really depends on your server-side implementation. Do not choose session storage cookies because of [security](https://stackoverflow.com/questions/3220660) (often not worth it). By using socket.io you can either send information on [connect](https://stackoverflow.com/a/26323687) or later on; by using JavaScript you can access local storage and send this data over socket.io (server should process it later to grant you (or not) access). This change would fit into the `SocketClient.js` as it's constant action. – zurfyx Jul 22 '17 at 08:09
  • @zurfyx awesome solution! It works pretty well, but I'm having trouble on my "on" event listeners. I mean, I do receive an event in the Socket, but sometimes it doesn't trigger anything, except for the sender. To explain better: 1 - User A sends a message (via emit) 2 - Both users A and B receive the event (userMessage) on the socket 3 - Only user A displays the message after triggering .on('userMessage'), but I can see on my reducer that the message is being added to my Messages Array. Did you have any trouble similar to this one? – Armando Freire Feb 19 '18 at 20:07
  • @ArmandoFreire that sounds like an issue very tied to your app specifications. IMO, the server shouldn't be sending anything to a client that it doesn't need to know about. But either way, you can modify the target function to filter out the unwanted: https://github.com/zurfyx/chat/blob/master/frontend/redux/modules/message.js#L102 – zurfyx Feb 20 '18 at 08:54
  • @zurfyx thanks for the reply! Oh, actually the server is not sending something unexpected! After sendind a userMessage I also receive a userMessage (which is replicated to the users on the chatRoom) but the event for displaying the received userMessage just triggers for the one that sent the message! The other person in the chatRoom, receives the message on the socket, but it doesnt show up on the feed! I'll take a look at the github you posted! Thanks again! :D – Armando Freire Feb 20 '18 at 12:59
  • Very useful! So much so that I created an npm package for it [here](https://github.com/pak11273/redux-socketio) – Isaac Pak Feb 02 '19 at 17:17
  • @zurfyx what if we only wanted socket connection on a specific component? Not in the whole app. – Mubbashar Apr 16 '20 at 12:45
  • @zurfyx Thanks for the solution, it seems to be a good approach! I have some questions about it: 1) Why do you check for `typeof action === 'function'` in your 'socketMiddleware.js'. It seems to be an excessive check, since by having this you're adding the functionality of redux-thunk. Or is there some other reason for checking? 2) Why do we need 'SocketClient.js'? It seems to just be a wrapper for defaul socket.io connection, why not just use it instead? – Leopik Apr 25 '21 at 07:39
  • Also, if I'm not mistaken - in `SocketClient.js` in `disconnect` function you do `this.socket.disconnect(() => {...})` with a callback provided. But the socket.io documentation doesn't state that the callback is a parameter of the function and I guess thus it will never be called – Leopik Apr 25 '21 at 10:07
  • How do you setup socketio .on() event listeners with this in Redux? – Hylle May 20 '21 at 14:56
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
      );
    },
  },
});
landorid
  • 440
  • 2
  • 6