I built a server with an API. It uses Axios for the un-logged calls and Socket.io for the logged calls. Then I have a website connected to it. And that works perfectly. But I have also an application built in react-native which has a strange behavior: it opens connections on every emit without closing them previous connections. As you can see below, I console.log the websocket.engine.clientsCount on the server. Every time I emit from the phone-application it opens a new connection, finding the server with an increasing number of it.

On server, I user following versions:
"connect-mongo": "^1.3.2",
"express": "^4.14.1",
"express-session": "^1.12.1",
"jwt-simple": "^0.5.1",
"mongodb": "^2.2.30",
"mongoose": "^4.11.5",
"passport": "^0.3.2",
"passport-jwt": "^2.2.1",
"passport-local": "^1.0.0",
"socket.io": "^1.7.3",
"socketio-jwt": "^4.5.0"
Here the code of the API. I removed some code for clarity.
const passport = require('passport');
const express = require('express');
const session = require('express-session');
const http = require('http');
const morgan = require('morgan');
const mongoose = require('mongoose');
const socketio = require('socket.io');
const bodyParser = require('body-parser');
const socketioJwt = require("socketio-jwt"); // da commentare
const Users = require('../models/users');
const passportService = require('./services/passport');
const requireAuth = passport.authenticate('jwt', {session: false});
const requireLogin = passport.authenticate('local', {session: false});
const config = require('./config');
const app = express();
const socketRouter = require('./services/socketRouter');
const MongoStore = require('connect-mongo')(session);
const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost/blablabla';
mongoose.connect(mongoUri);
...
const server = http.Server(app);
const websocket = socketio(server);
// add authorization for jwt-passport when first connection -> https://github.com/auth0/socketio-jwt
websocket.use(socketioJwt.authorize({
secret: config.secret,
handshake: true
}));
const sessionMiddleware = session({
store: new MongoStore({ // use MongoDb to store session (re-using previous connection)
mongooseConnection: mongoose.connection,
ttl: (1 * 60 * 60)
}),
secret: config.secretSession,
httpOnly: true,
resave: false,
saveUninitialized: false,
cookie: { maxAge: 86400000 }
});
app.use(sessionMiddleware);
...
websocket.on('connection', (socket) => {
Users.findById(socket.decoded_token.sub, function(err, user) {
if (err) { console.log('the user wasn\'t find in database', err); }
if (user) {
socket.join(user._id);
console.log('Clients connected: ', websocket.engine.clientsCount);
// ------ PROTECTED EVENTS ------ //
...
// ------------------------------ //
}
socket.on('disconnect', ()=> {
socket.leave(user._id);
onsole.log('user disconnected');
});
});
});
...
I won't put the initialisation of the website because it works good.
On mobile application, I user following versions:
"react-native": "^0.41.0",
"react-native-keychain": "^1.1.0",
"socket.io-client": "^1.7.3",
"socketio-jwt": "^4.5.0"
Here is the innitialisation of the react-native application.
import * as Keychain from 'react-native-keychain';
import { BASIC_WS_URL } from '../api';
const io = require('socket.io-client/dist/socket.io');
const socketEvents = require('./events');
exports = module.exports = (store) => {
Keychain.getGenericPassword().then((credentials) => {
if (credentials && credentials !== false) {
const { password } = credentials;
const websocket = io(BASIC_WS_URL, {
jsonp: false,
transports: ['websocket'], // you need to explicitly tell it to use websockets
query: {
token: password
}
});
websocket.connect();
websocket.on('connect', (socket) => {
console.log('Connected');
});
websocket.on('reconnect', (socket) => {
console.log('Re-connected');
});
websocket.on('disconnect', (socket) => {
console.log('Disconnected');
});
// all the events to listen
socketEvents(websocket, store);
}
});
};
What al I doing wrong?
So here I come with an answer. I'll try to leave an answer as the one I'd like to find. A sort of tutorial about how to include Socket.io in React-native. Please, if you know a better solution, write it down. As I wrote, the problem is that socket in React-Native is global and in this way I wrong implementation is just more evident. First of all, I initialized socket in the wrong place. The correct place I found is in App.js, where the router is. I remove some code for clarity.
// important to access the context of React
import PropTypes from 'prop-types';
// Library used to save encrypted token
import * as Keychain from 'react-native-keychain';
// url to address
import { BASIC_WS_URL } from '../../api';// library to encrypt and decrypt data
const io = require('socket.io-client/dist/socket.io');
prepare this function within contructor and componentDidMount:
state = {}
setStateAsync(state) {
return new Promise((resolve) => {
this.setState(state, resolve)
});
}
keichain is a promise, so it won't work in componentDidMount. To make it work, you have to do the following, so every step will wait for the previous to be done:
async componentWillMount() {
const response = await Keychain.getGenericPassword();
const websocket = await io(BASIC_WS_URL, {
jsonp: false,
// forceNew:true,
transports: ['websocket'],
query: {
token: response.password
}
});
await websocket.connect();
await websocket.on('connect', (socket) => {
console.log('Sono -> connesso!');
});
await websocket.on('reconnect', (socket) => {
console.log('Sono riconnesso!');
});
await websocket.on('disconnect', (socket) => {
console.log('Sono disconnesso!');
});
await websocket.on('error', (error) => {
console.log(error);
});
// a function imported containing all the events (passing store retrieved from context declare at the bottom)
await socketEvents(websocket, this.context.store);
// then save the socket in the state, because at this point the component will be already rendered and this.socket would be not effective
await this.setStateAsync({websocket: websocket});
}
Remember to remove the console.logs then. They are there just for verification. Right after this, remeber to disconnect when unmounting:
componentWillUnmount() {
this.state.websocket.disconnect()
}
And right after this, save the socket in the context:
getChildContext() {
return {websocket: this.state.websocket};
}
Remeber to declare the context at the bottom of the component:
App.childContextTypes = {
websocket: PropTypes.object
}
// access context.type to get the store to pass to socket.io initialization
App.contextTypes = {
store: PropTypes.object
}
So, final result is this:
...
// important to access the context of React
import PropTypes from 'prop-types';
// Library used to save encrypted token
import * as Keychain from 'react-native-keychain';
// url to address
import { BASIC_WS_URL } from '../../api';// library to encrypt and decrypt data
const io = require('socket.io-client/dist/socket.io');
...
class App extends Component {
constructor() {
super();
...
}
state = {}
setStateAsync(state) {
return new Promise((resolve) => {
this.setState(state, resolve)
});
}
// set the function as asynchronous
async componentWillMount() {
//retrieve the token to authorize the calls
const response = await Keychain.getGenericPassword();
// initialize the socket connection with the passwordToken (wait for it)
const websocket = await io(BASIC_WS_URL, {
jsonp: false,
// forceNew:true,
transports: ['websocket'], // you need to explicitly tell it to use websockets
query: {
token: response.password
}
});
// connect to socket (ask for waiting for the previous initialization)
await websocket.connect();
await websocket.on('connect', (socket) => {
console.log('Sono -> connesso!');
});
await websocket.on('reconnect', (socket) => {
console.log('Sono riconnesso!');
});
await websocket.on('disconnect', (socket) => {
console.log('Sono disconnesso!');
});
await websocket.on('error', (error) => {
console.log(error);
});
// a function imported containing all the events
await socketEvents(websocket, this.context.store);
await this.setStateAsync({websocket: websocket});
}
componentWillUnmount() {
this.state.websocket.disconnect()
}
getChildContext() {
return {websocket: this.state.websocket};
}
render() {
return (
... // here goes the router
);
}
}
App.childContextTypes = {
websocket: PropTypes.object
}
// access context.type to get the store to pass to socket.io initialization
App.contextTypes = {
store: PropTypes.object
}
export default App;
then, in any page/container, you can do like this. -> declare the context in the bottom of the component:
Main.contextTypes = {
websocket: PropTypes.object
}
And when you dispatch an action, you will be able then to emit:
this.props.dispatch(loadNotif(this.context.websocket));
In the action creator, you will emit like this:
exports.loadNotif = (websocket) => {
return function (dispatch) {
// send request to server
websocket.emit('action', {par: 'blablabla'});
};
};
I hope it will help somebody.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With