'AspNet Core SignalR and Redux Configuration - send and receive data
I'm implametantion a chat feature that use AspNet Core SiginalR and React.js + Redux.
Steps to problem
I can send the message by signalR for back-end server
I can receive the message back on Middleware
But I cound't dispatch for update de state in store and update views.
Question
What I'm doing wrong ?
Maybe I can not access dispatch function from callback connection.on("ReceiveMessage", data => ...
is it ? How to fix it ?
App
import '@fake-db'
import React, {Suspense} from 'react';
import {FuseAuthorization, FuseLayout, FuseTheme} from '@fuse';
import Provider from 'react-redux/es/components/Provider';
import {Router} from 'react-router-dom';
import jssExtend from 'jss-extend';
import history from '@history';
import {Auth} from './auth';
import store from './store';
import AppContext from './AppContext';
import routes from './fuse-configs/routesConfig';
import {create} from 'jss';
import {StylesProvider, jssPreset, createGenerateClassName} from '@material-ui/styles';
import axios from 'axios';
const jss = create({
...jssPreset(),
plugins : [...jssPreset().plugins, jssExtend()],
insertionPoint: document.getElementById('jss-insertion-point'),
});
axios.defaults.baseURL = 'https://localhost:5001/api/v1/';
const generateClassName = createGenerateClassName();
const App = () => {
return (
<Suspense fallback="loading">
<AppContext.Provider value={{routes}}>
<StylesProvider jss={jss} generateClassName={generateClassName}>
<Provider store={store}>
<Auth>
<Router history={history}>
<FuseAuthorization>
<FuseTheme>
<FuseLayout/>
</FuseTheme>
</FuseAuthorization>
</Router>
</Auth>
</Provider>
</StylesProvider>
</AppContext.Provider>
</Suspense>
);
};
export default App;
Create Store
import * as reduxModule from 'redux';
import {applyMiddleware, compose, createStore} from 'redux';
import createReducer from './reducers';
import signalRMiddleware from './middlewares/signalRMiddleware';
import thunk from 'redux-thunk';
/*
Fix for Firefox redux dev tools extension
https://github.com/zalmoxisus/redux-devtools-instrument/pull/19#issuecomment-400637274
*/
reduxModule.__DO_NOT_USE__ActionTypes.REPLACE = '@@redux/INIT';
const composeEnhancers =
process.env.NODE_ENV !== 'production' &&
typeof window === 'object' &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
// Specify extension’s options like name, actionsBlacklist, actionsCreators, serialize...
}) : compose;
const enhancer = composeEnhancers(
applyMiddleware(
thunk,
signalRMiddleware
)
// other store enhancers if any
);
const store = createStore(createReducer(), enhancer);
store.asyncReducers = {};
export const injectReducer = (key, reducer) => {
if ( store.asyncReducers[key] )
{
return;
}
store.asyncReducers[key] = reducer;
store.replaceReducer(createReducer(store.asyncReducers));
return store;
};
export default store;
Middleware File
import { HubConnectionBuilder, LogLevel } from '@microsoft/signalr';
import * as Actions from "../../main/apps/chat/store/actions";
const connection = new HubConnectionBuilder().withUrl("https://localhost:5001/chatHub")
.configureLogging(LogLevel.Information)
.withAutomaticReconnect()
.build();
connection.start();
export default function signalRMiddleware() {
connection.on('ReceiveMessage', data => {
Actions.receiveSocketMessage(data)
})
return next => action => {
switch (action.type) {
case Actions.DIRECT_MESSAGE:
connection.invoke('DirectMessage', action.payload);
}
return next(action);
}
}
Actions of Chat
import axios from 'axios';
import {setselectedContactId} from './contacts.actions';
import {closeMobileChatsSidebar} from './sidebars.actions';
export const GET_CHAT = '[CHAT APP] GET CHAT';
export const REMOVE_CHAT = '[CHAT APP] REMOVE CHAT';
export const SEND_MESSAGE = '[CHAT APP] SEND MESSAGE';
export const DIRECT_MESSAGE = '[CHAT APP] DIRECT MESSAGE';
export const RECEIVE_MESSAGE = '[CHAT APP] RECEIVE MESSAGE';
export function receiveSocketMessage(data)
{
return (dispatch) => {
dispatch({type: RECEIVE_MESSAGE, payload: `test`})
}
}
export function directMessage(data)
{
return {
type : DIRECT_MESSAGE,
payload : data
}
}
Chat Reducers
import * as Actions from '../actions';
const initialState = null;
const chat = function (state = initialState, action) {
switch ( action.type )
{
case Actions.RECEIVE_MESSAGE:
{
return {
...state,
directMessage: action.payload
}
}
default:
{
return state;
}
}
};
export default chat;
Chat Component
import React, {useEffect} from 'react';
import {useTranslation} from 'react-i18next';
import {Drawer, AppBar, Toolbar, Typography, IconButton, Hidden, Avatar, Icon, Paper, Button} from '@material-ui/core';
import {fade} from '@material-ui/core/styles/colorManipulator';
import {useDispatch, useSelector} from 'react-redux';
import clsx from 'clsx';
import withReducer from 'app/store/withReducer';
import * as Actions from "./store/actions";
import Chat from "./Chat";
import ChatsSidebar from "./ChatsSidebar";
import StatusIcon from "./StatusIcon";
import ContactSidebar from './ContactSidebar';
import UserSidebar from './UserSidebar';
import reducer from './store/reducers';
import {makeStyles} from '@material-ui/styles';
const drawerWidth = 400;
const headerHeight = 200;
const useStyles = makeStyles(theme => ({
root : {
display : 'flex',
flexDirection : 'row',
minHeight : '100%',
position : 'relative',
flex : '1 1 auto',
height : 'auto',
backgroundColor: theme.palette.background.default
},
topBg : {
position : 'absolute',
left : 0,
right : 0,
top : 0,
height : headerHeight,
backgroundImage: 'url("../../assets/images/backgrounds/header-bg.png")',
backgroundColor: theme.palette.primary.dark,
backgroundSize : 'cover',
pointerEvents : 'none'
},
contentCardWrapper: {
position : 'relative',
padding : 24,
maxWidth : 1400,
display : 'flex',
flexDirection : 'column',
flex : '1 0 auto',
width : '100%',
minWidth : '0',
maxHeight : '95%',
margin : '0 auto',
[theme.breakpoints.down('sm')]: {
padding: 16
},
[theme.breakpoints.down('xs')]: {
padding: 12
}
},
contentCard : {
display : 'flex',
position : 'relative',
flex : '1 1 100%',
flexDirection : 'row',
backgroundColor: "f7f7f7",
boxShadow : theme.shadows[1],
borderRadius : 8,
minHeight : 0,
overflow : 'hidden'
},
drawerPaper : {
width : drawerWidth,
maxWidth : '100%',
overflow : 'hidden',
height : '100%',
[theme.breakpoints.up('md')]: {
position: 'relative'
}
},
contentWrapper : {
display : 'flex',
flexDirection: 'column',
flex : '1 1 100%',
zIndex : 10,
background : `linear-gradient(to bottom, ${fade(theme.palette.background.paper, 0.8)} 0,${fade(theme.palette.background.paper, 0.6)} 20%,${fade(theme.palette.background.paper, 0.8)})`
},
content : {
display : 'flex',
flex : '1 1 100%',
minHeight: 0
}
}));
function ChatApp(props)
{
const { t } = useTranslation();
const dispatch = useDispatch();
const chat = useSelector(({chatApp}) => chatApp.chat);
const contacts = useSelector(({chatApp}) => chatApp.contacts.entities);
const selectedContactId = useSelector(({chatApp}) => chatApp.contacts.selectedContactId);
const mobileChatsSidebarOpen = useSelector(({chatApp}) => chatApp.sidebars.mobileChatsSidebarOpen);
const userSidebarOpen = useSelector(({chatApp}) => chatApp.sidebars.userSidebarOpen);
const contactSidebarOpen = useSelector(({chatApp}) => chatApp.sidebars.contactSidebarOpen);
const directMessage = useSelector(({chatApp}) => chatApp.directMessage);
const classes = useStyles(props);
const selectedContact = contacts.find(_contact => (_contact.id === selectedContactId));
useEffect(() => {
dispatch(Actions.getUserData());
dispatch(Actions.getContacts());
dispatch(Actions.directMessage({message: "123 testando...", to: "Carlos"}));
}, [dispatch]);
useEffect(() => {
console.log(`Mensagem recebido: ${directMessage} TAMO NO APP`);
}, [directMessage])
return (
<div className={clsx(classes.root)}>
<div className={clsx(classes.contentCardWrapper, 'container')}>
<div className={classes.contentCard}>
<Hidden mdUp>
<Drawer
className="h-full absolute z-20"
variant="temporary"
anchor="left"
open={mobileChatsSidebarOpen}
onClose={() => dispatch(Actions.closeMobileChatsSidebar())}
classes={{
paper: clsx(classes.drawerPaper, "absolute left-0")
}}
style={{position: 'absolute'}}
ModalProps={{
keepMounted : true,
disablePortal: true,
BackdropProps: {
classes: {
root: "absolute"
}
}
}}
>
<ChatsSidebar/>
</Drawer>
</Hidden>
<Hidden smDown>
<Drawer
className="h-full z-20"
variant="permanent"
open
classes={{
paper: classes.drawerPaper
}}
>
<ChatsSidebar/>
</Drawer>
</Hidden>
<Drawer
className="h-full absolute z-30"
variant="temporary"
anchor="left"
open={userSidebarOpen}
onClose={() => dispatch(Actions.closeUserSidebar())}
classes={{
paper: clsx(classes.drawerPaper, "absolute left-0")
}}
style={{position: 'absolute'}}
ModalProps={{
keepMounted : false,
disablePortal: true,
BackdropProps: {
classes: {
root: "absolute"
}
}
}}
>
<UserSidebar/>
</Drawer>
<main className={clsx(classes.contentWrapper, "z-10")}>
{!chat ?
(
<>
<AppBar position="static" elevation={1}>
<Toolbar className="px-16"/>
</AppBar>
<div className="flex flex-col flex-1 items-center justify-center p-24">
<Paper className="rounded-full p-48">
<Icon className="block text-64" color="secondary">chat</Icon>
</Paper>
<Typography variant="h6" className="my-24">{t("Chat")}</Typography>
<Typography className="hidden md:flex px-16 pb-24 mt-24 text-center" color="textSecondary">
{t("Select a contact to start a conversation!")}
</Typography>
<Button variant="outlined" color="primary" className="flex md:hidden normal-case" onClick={() => dispatch(Actions.openMobileChatsSidebar())}>
{t("Select a contact to start a conversation!")}
</Button>
</div>
</>
) : (
<>
<AppBar position="static" elevation={1}>
<Toolbar className="px-16">
<IconButton
color="inherit"
aria-label="Open drawer"
onClick={() => dispatch(Actions.openMobileChatsSidebar())}
className="flex md:hidden"
>
<Icon>chat</Icon>
</IconButton>
<div className="flex items-center cursor-pointer" onClick={() => dispatch(Actions.openContactSidebar())}>
<div className="relative ml-8 mr-12">
<div className="absolute right-0 bottom-0 -m-4 z-10">
<StatusIcon status={selectedContact.status}/>
</div>
<Avatar src={selectedContact.avatar} alt={selectedContact.name}>
{!selectedContact.avatar || selectedContact.avatar === '' ? selectedContact.name[0] : ''}
</Avatar>
</div>
<Typography color="inherit" className="text-18 font-600">{selectedContact.name}</Typography>
</div>
</Toolbar>
</AppBar>
<div className={classes.content}>
<Chat className="flex flex-1 z-10"/>
</div>
</>
)
}
</main>
<Drawer
className="h-full absolute z-30"
variant="temporary"
anchor="right"
open={contactSidebarOpen}
onClose={() => dispatch(Actions.closeContactSidebar())}
classes={{
paper: clsx(classes.drawerPaper, "absolute right-0")
}}
style={{position: 'absolute'}}
ModalProps={{
keepMounted : true,
disablePortal: true,
BackdropProps: {
classes: {
root: "absolute"
}
}
}}
>
<ContactSidebar/>
</Drawer>
</div>
</div>
</div>
);
}
export default withReducer('chatApp', reducer)(ChatApp);
Codesandbox.io
Solution 1:[1]
you are calling the action creator, but you are not actually dispatching the action it generates in your middleware, so it just creates an object that does nothing.
- export default function signalRMiddleware() {
+ export default function signalRMiddleware(api) {
connection.on('ReceiveMessage', data => {
- Actions.receiveSocketMessage(data)
+ api.dispatch(Actions.receiveSocketMessage(data))
})
return next => action => {
switch (action.type) {
case Actions.DIRECT_MESSAGE:
connection.invoke('DirectMessage', action.payload);
}
return next(action);
}
}
Solution 2:[2]
Solution
SignalRMiddleware
import { HubConnectionBuilder, LogLevel } from '@microsoft/signalr';
import * as Actions from '../../main/apps/chat/store/actions';
const connection = new HubConnectionBuilder().withUrl("https://localhost:5001/chatHub")
.configureLogging(LogLevel.Information)
.withAutomaticReconnect()
.build();
connection.start();
export default function signalRMiddleware(api) {
connection.on('ReceiveMessage', data => {
console.log(`cheguei no middleware - msg: ${data}`);
api.dispatch({type: Actions.RECEIVE_MESSAGE, payload: data});
})
return next => action => {
switch (action.type) {
case Actions.DIRECT_MESSAGE:
{
console.log(`enviando msg: ${action.payload.message}`);
connection.invoke('DirectMessage', action.payload);
}
}
return next(action);
}
}
Create Store
import * as reduxModule from 'redux';
import {applyMiddleware, compose, createStore} from 'redux';
import createReducer from './reducers';
import signalRMiddleware from './middlewares/signalRMiddleware';
import thunk from 'redux-thunk';
/*
Fix for Firefox redux dev tools extension
https://github.com/zalmoxisus/redux-devtools-instrument/pull/19#issuecomment-400637274
*/
reduxModule.__DO_NOT_USE__ActionTypes.REPLACE = '@@redux/INIT';
const composeEnhancers =
process.env.NODE_ENV !== 'production' &&
typeof window === 'object' &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
// Specify extension’s options like name, actionsBlacklist, actionsCreators, serialize...
}) : compose;
const enhancer = composeEnhancers(
applyMiddleware(
thunk,
signalRMiddleware
)
// other store enhancers if any
);
const store = createStore(createReducer(), enhancer);
store.asyncReducers = {};
export const injectReducer = (key, reducer) => {
if ( store.asyncReducers[key] )
{
return;
}
store.asyncReducers[key] = reducer;
store.replaceReducer(createReducer(store.asyncReducers));
return store;
};
export default store;
Solution 3:[3]
with @redux/toolkit
signalr-Slice
import {createAction, createSlice} from "@reduxjs/toolkit";
let initialState=[];
const SignalrSlice=createSlice({
name:"sinalr",
initialState,
reducers:{
RESIVEMESSAGE:(signalr,action)=>{
signalr.push({message:action.payload.message,user:action.payload.user})
}
}
})
export const SENDMESSAGE=createAction("SENDMESSAGE")
export const {RESIVEMESSAGE}=SignalrSlice.actions
export default SignalrSlice.reducer
signar-middleware
import { HubConnectionBuilder, LogLevel ,HttpTransportType} from '@aspnet/signalr';
import * as Actions from "./signalr-slice"
const hubConnection = new HubConnectionBuilder()
.withUrl('http://192.168.1.103:4000/chathub', {
skipNegotiation: true,
transport: HttpTransportType.WebSockets,
})
.configureLogging(LogLevel.Information)
.build();
hubConnection.start();
const signalrMiddleWare = ({dispatch,getState})=>next=>async action=> {
hubConnection.on("ResiveMessage", (UserName, MessageText, SendAt) => {
dispatch({type: Actions.RESIVEMESSAGE.type, payload: {user: UserName,message: MessageText}})
});
if(action.type===Actions.SENDMESSAGE.type) {
hubConnection.invoke("SendMessage", action.payload.user, action.payload.message)
}
next(action)
}
export default signalrMiddleWare
reduser
import{combineReducers} from "redux";
import SignalrSlice from "./signalr-slice"
export default combineReducers({
signalr:SignalrSlice,
})
store
import{configureStore,getDefaultMiddleware} from "@reduxjs/toolkit";
import reducer from "./reduser";
import signalrMiddleWare from "./signalr-middleware";
export const store= configureStore({
reducer,
middleware: [...getDefaultMiddleware(),signalrMiddleWare]
})
sendClickHandler
**
const clickHandler = (e) => {
dispatch({type:Actions.SENDMESSAGE.type,payload:{message:fData.messageText, user:fData.user}})
}
**
this code work fine But when send message with clickHandler function received multi times answerenter image description here
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | phry |
| Solution 2 | Community |
| Solution 3 | masoud_ALA |
