How to store socket object of socket.io
in slice of redux toolkit
?
I would like to do something like:
const initialState = {
socket: null
}
const socketSlice = createSlice({
name: socket,
initialState,
reducers:{
createSocket(state, action){
state.socket = io("localhost:5000")
},
removeSocket(state, action){
state.socket = null
}
// ...
}
})
However, this gives the following error:
serializableStateInvariantMiddleware.ts:222 A non-serializable value was detected in the state
Help me...
I had the exact same issue and solved it using the following steps:
import { io } from 'socket.io-client';
class SocketClient {
socket;
connect() {
this.socket = io.connect(process.env.SOCKET_HOST, { transports: ['websocket'] });
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();
});
}
}
export default SocketClient;
import SocketClient from './js/services/SocketClient';
export const socketClient = new SocketClient();
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
//import meta image
import '@/public/assets/images/metaImage.jpg';
//styles
import '@/scss/global.scss';
//store
import store from '@/js/store/store';
//app
import App from './App';
//socket client
import SocketClient from './js/services/SocketClient';
export const socketClient = new SocketClient();
const container = document.getElementById('root'),
root = createRoot(container);
root.render(
<Provider store={store}>
<App />
</Provider>
);
I used createAsyncThunk function from @reduxjs/toolkit, because it automatically generates types like pending
, fulfilled
and rejected
.
Here's how I structure my reducer slice to connect and disconnect from web socket in redux:
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { socketClient } from '../../../../index';
const initialState = {
connectionStatus: '',
};
export const connectToSocket = createAsyncThunk('connectToSocket', async function () {
return await socketClient.connect();
});
export const disconnectFromSocket = createAsyncThunk('disconnectFromSocket', async function () {
return await socketClient.disconnect();
});
const appSlice = createSlice({
name: 'app',
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(connectToSocket.pending, (state) => {
state.connectionStatus = 'connecting';
});
builder.addCase(connectToSocket.fulfilled, (state) => {
state.connectionStatus = 'connected';
});
builder.addCase(connectToSocket.rejected, (state) => {
state.connectionStatus = 'connection failed';
});
builder.addCase(disconnectFromSocket.pending, (state) => {
state.connectionStatus = 'disconnecting';
});
builder.addCase(disconnectFromSocket.fulfilled, (state) => {
state.connectionStatus = 'disconnected';
});
builder.addCase(disconnectFromSocket.rejected, (state) => {
state.connectionStatus = 'disconnection failed';
});
},
});
export default appSlice.reducer;
useEffect(() => {
dispatch(connectToSocket());
return () => {
if (connectionStatus === 'connected') {
dispatch(disconnectFromSocket());
}
};
//eslint-disable-next-line
}, [dispatch]);
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { socketClient } from '../../../../index';
const initialState = {
messageStatus: '', //ideally it should come from the BE
messages: [],
typingUsername: '',
};
export const sendMessage = createAsyncThunk('sendMessage', async function ({ message, username }) {
return await socketClient.emit('chat', { message, handle: username });
});
const chatSlice = createSlice({
name: 'chat',
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(sendMessage.pending, (state) => {
state.messageStatus = 'Sending';
});
builder.addCase(sendMessage.fulfilled, (state) => {
state.messageStatus = 'Sent successfully';
});
builder.addCase(sendMessage.rejected, (state) => {
state.messageStatus = 'Send failed';
});
},
});
export default chatSlice.reducer;
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { socketClient } from '../../../../index';
const initialState = {
messageStatus: '', //ideally it should come from the BE
messages: [],
typingUsername: '',
};
export const fetchMessages = createAsyncThunk(
'fetchMessages',
async function (_, { getState, dispatch }) {
console.log('state ', getState());
return await socketClient.on('chat', (receivedMessages) =>
dispatch({ type: 'chat/saveReceivedMessages', payload: { messages: receivedMessages } })
);
}
);
const chatSlice = createSlice({
name: 'chat',
initialState,
reducers: {
saveReceivedMessages: (state, action) => {
state.messages.push(action.payload.messages);
state.typingUsername = '';
},
},
extraReducers: (builder) => {
builder.addCase(fetchMessages.pending, () => {
// add a state if you would like to
});
builder.addCase(fetchMessages.fulfilled, () => {
// add a state if you would like to
});
builder.addCase(fetchMessages.rejected, () => {
// add a state if you would like to
});
},
});
export default chatSlice.reducer;