Redux middleware for handle socket.io authentication.
Inspiration from redux-promise-middleware.
Demo project: chat with authentication
Middleware are the right place for persistent connections like websockets in a Redux app, for several reasons:
- Middleware exist for the lifetime of the application
- Like with the store itself, you probably only need a single instance of a given connection that the whole app can use
- Middleware can see all dispatched actions and dispatch actions themselves. This means a middleware can take dispatched actions and turn those into messages sent over the websocket, and dispatch new actions when a message is received over the websocket.
Given a single action with a socketIOClient.connect payload, the middleware transforms the action to a separate a pending action and a separate connected/rejected/disconnected action.
import socketIOClient from 'socket.io-client';
export const connectingToServer = () => ({
type: 'SOCKET',
payload: socketIOClient.connect(uri, opts),
});
First, install the middleware:
npm i -S redux-socket-auth-middleware
Import the middleware and include it in applyMiddleware when creating the Redux store:
[TIP] You can also add redux-logger middleware.
import {applyMiddleware, createStore} from 'redux';
import socketAuth from 'redux-socket-auth-middleware';
import logger from 'redux-logger';
// Take our rootReducer/appReducer.
import rootReducer from './reducers';
// Apply middlewares, logger always in the end.
const middleware = applyMiddleware(socketAuth, logger);
// Create store with reducers and middlewares.
const store = createStore(rootReducer, middleware);
// Now we can use store in App
export default store;
Dispatch a socketIOClient.connect as the value of the payload property of the action.
import socketIOClient from 'socket.io-client';
// Type of action:
const SOCKET = 'SOCKET';
// URL where is running a server with socket.io
const url = 'http://localhost:8000';
// Function for connecting to a server
// with that connection we passing query string: user
const connect = user => socketIOClient.connect(url, {query: `user=${user}`});
// const user = JSON.stringify({name: 'Soda', password: 'secret'})
export const connectingToServer = (user = null) => ({
type: SOCKET,
payload: connect(user),
});
/* Now we can use this action in App
* user argument hold name and password as object string
* for adding into connection as query
*/
// That's it for dispatch action!
// Action types suffixes for socket:
const suffix = {
PENDING: '_PENDING',
CONNECTED: '_CONNECTED',
REJECTED: '_REJECTED',
DISCONNECTED: '_DISCONNECTED',
};
// Action type that dispatched connectingToServer action
const SOCKET = 'SOCKET';
// Initialization state:
const initState = {
disconnected: null,
error: null,
fetching: false,
io: null,
};
// Reducer for handle socket.io connection.
export default function socket(state = initState, {type, payload}) {
switch (type) {
// Handle action socket.io connecting to a server:
case SOCKET + suffix.PENDING:
return ({
...state,
fetching: true,
});
// Handle action socket.io connected to a server:
// payload hold connected socket.io.
case SOCKET + suffix.CONNECTED:
return ({
...state,
io: payload,
fetching: false,
});
// Handle action server rejected connection:
// payload hold error from a server.
case SOCKET + suffix.REJECTED:
return ({
...state,
error: payload,
fetching: false,
});
// Handle action server disconnect client-socket:
// payload hold reason disconnected from a server.
case SOCKET + suffix.DISCONNECTED:
return ({
...state,
disconnected: payload,
fetching: false,
});
}
return state;
}
// That's it!
import React, {useState} from 'react';
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
// import redux action function for connecting to server
import {connectingToServer} from '../path to redux action connectingToServer';
function App({socket, connectingToServer}) {
const [name, setName] = useState('');
const [password, setPassword] = useState('');
const handleChangeName = ({target}) => setName(target.value);
const handleChangePassword = ({target}) => setPassword(target.value);
const handleLogin = () => {
const user = JSON.stringify({name, password});
// Dispatch action
connectingToServer(user);
};
const renderStatus = () => (
// If is a socket in redux store and is this socket connected.
socket.io && socket.io.connected
? 'Connected'
: 'Not connected'
);
// If is fetching from a server?
if (socket.fetching) return <h1>Loading...</h1>
return (
<div>
<h1>{renderStatus()}</h1>
<input value={name} onChange={handleChangeName}/>
<input value={password} onChange={handleChangePassword}/>
<button onClick={handleLogin}>Login</button>
</div>
)
}
const mapStateToProps = ({socket}) => ({
socket,
});
const mapDispatchToProps = dispatch => bindActionCreators({
connectingToServer,
}, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(App);
/* When we get a connection with a server:
* redux store will hold socket.io,
* so we can listen or emit events.
* socket.io.on('some event', callback)
* socket.io.emit('some event', arguments)
* just connect redux to component
* and mapStateToProps to get access to socket.io
*/
io.use(function(socket, next) {
var handshakeData = socket.request;
// make sure the handshake data looks good as before
// if error do this:
// next(new Error('not authorized'));
// else just call next
next();
});
For bug reports and feature requests, file an issue on GitHub.
Code licensed with the MIT License (MIT).