Skip to content

Instantly share code, notes, and snippets.

@markerikson
Created June 28, 2018 00:37
Show Gist options
  • Star 63 You must be signed in to star a gist
  • Fork 10 You must be signed in to fork a gist
  • Save markerikson/3df1cf5abbac57820a20059287b4be58 to your computer and use it in GitHub Desktop.
Save markerikson/3df1cf5abbac57820a20059287b4be58 to your computer and use it in GitHub Desktop.
Redux socket middleware example usage
const createMySocketMiddleware = (url) => {
return storeAPI => {
let socket = createMyWebsocket(url);
socket.on("message", (message) => {
storeAPI.dispatch({
type : "SOCKET_MESSAGE_RECEIVED",
payload : message
});
});
return next => action => {
if(action.type == "SEND_WEBSOCKET_MESSAGE") {
socket.send(action.payload);
return;
}
return next(action);
}
}
}
// later, in your app
function sendSocketMessage(message) {
return {
type : "SEND_WEBSOCKET_MESSAGE",
payload : message
}
}
class MyComponent extends React.Component {
handleClick = () => {
this.props.sendSocketMessage("This goes to the server");
}
}
export default connect(null, {sendSocketMessage})(MyComponent)
@ifeanyidavid
Copy link

Hi Mark, thanks for this. This has worked for me so far but I noticed that the socket instance gets created as soon as the application starts including during Login.

How do I create a socket instance only when a user has logged in?

Thank you.

@JonCognioDigital
Copy link

JonCognioDigital commented Sep 16, 2018

@alaboudi
Copy link

alaboudi commented Dec 3, 2018

Can you explain why you'd rather create a web socket connection as a middleware rather than capturing it in a side-effect handler?

@tienduchust
Copy link

Can you explain why you'd rather create a web socket connection as a middleware rather than capturing it in a side-effect handler?

That avoid socket connection affect by component life cycle

@ulou
Copy link

ulou commented Nov 20, 2020

Hey, thanks for this example, it helped me a lot. However, sometimes you can get error cause message cannot be send during socket connection. To avoid this I add folowing function:

function waitForSocketConnection(socket, callback) {
  setTimeout(
    function () {
      if (socket.readyState === 1) {
        if (callback != null) {
          callback();
        }
      } else {
        console.log("waiting for socket connection...")
        waitForSocketConnection(socket, callback);
      }
    }, 100)
}

and then I replaced line 14 of you code with:

 waitForSocketConnection(socket, () => {
        socket.send(action.payload);
 })

Thanks to this approach we are not blocking redux, however if anyone wants to wait for msg, then we can await waitForSocketConnection(socket, () => {...})

@adarshaacharya
Copy link

@ulou do you have any reference project/ working example for the given approach seems good option for handling socket errors.

@n-ii-ma
Copy link

n-ii-ma commented Nov 1, 2022

@markerikson Would you elaborate more on how this middleware is being applied to the store and used in the app?

I'm trying to implement this in Redux Toolkit and am having a difficult time coming up with an effective solution.

@markerikson
Copy link
Author

@n-ii-ma : not sure I understand the question.

Setup-wise this would be added to the middleware option in configureStore as usual.

Can you clarify what you're asking?

@n-ii-ma
Copy link

n-ii-ma commented Nov 2, 2022

@markerikson: Thank you for your response Mark. Well, I've used your example here and on Stackoverflow as a base to configure the Socket middleware with Socket IO and Laravel Echo in this way and just as the docs recommends I use .concat to append the middleware to the store:

(Each section is in its own file)

// Actions
// Get data
export const getData = createAction('socket/getData', data => {
  return {
    payload: {
      data,
      id: nanoid(),
      createdAt: new Date().toISOString(),
    },
  };
});

// Send data
export const sendData = createAction('socket/sendData', data => {
  return {
    payload: {
      data,
      id: nanoid(),
      createdAt: new Date().toISOString(),
    },
  };
});

// Disconnect
export const disconnect = createAction('socket/disconnect');

// Socket middleware
export const createSocketMiddleware = url => {
  let socket;
  let echo;

  return storeAPI => next => action => {
    switch (action.type) {
      // Connect after user is authenticated
      case 'user/login/fulfilled': {
        socket = socketIOClient(`${url}:3010/socket`);

        // Create a new Echo class instance
        echo = new Echo({
          host: `${url}:6001`,
          broadcaster: 'socket.io',
          client: socketIOClient,
          auth: {
            headers: {
              Authorization: axios.defaults.headers['Authorization'],
            },
          },
        });

        // Connect and listen
        echo.private('transport.1').listen('.order-pending', ev => {
          storeAPI.dispatch(getData(ev));
        });
        break;
      }

      // Send data
      case 'socket/sendData': {
        socket.emit('order-pending', action.payload);
        break;
      }

      // Disconnect
      case 'socket/disconnect': {
        echo.disconnect();
        break;
      }
    }
    return next(action);
  };
};

// Reducer
const socketReducer = createReducer(initialState, builder => {
  builder
    .addCase(getData, (state, action) => {
      state.data = action.payload.data;
    })
    .addCase(disconnect, (state, action) => {
      return initialState;
    });
});

// Store
export const store = configureStore({
  reducer: persistedReducer,
  middleware: getDefaultMiddleware =>
    getDefaultMiddleware({
      serializableCheck: {
        ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
      },
    }).concat(createSocketMiddleware(url)),
  devTools: process.env.NODE_ENV === 'development' && true,
});

Everything is working as expected yet I myself am a bit cautious regarding this method due to the lack of adequate examples and am wondering if it indeed adheres to the Redux standards.

One more question I was wondering about is why is redux requiring the WebSocket connection to be as a middleware and how is RTK Query handling this issue with its Streaming Updates that plain Redux Toolkit is unable to do.

@markerikson
Copy link
Author

@n-ii-ma looks fine at first glance.

You might want to look at the FAQ entry that describes why we normally recommend that websocket-type connections should go into a middleware:

https://redux.js.org/faq/code-structure#where-should-websockets-and-other-persistent-connections-live

@n-ii-ma
Copy link

n-ii-ma commented Nov 10, 2022

@markerikson Many thanks Mike!

Upon further consideration and after a conversation with Lenz, I decided to use the listenerMiddleware, yet I'm still trying to figure out how to stop a particular listener

@markerikson
Copy link
Author

@n-ii-ma probably best to ask over in #redux in Reactiflux, but typically you'd dispatch an action and have the listener do an await take(thatAction) and then cancel itself / return.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment