'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

https://codesandbox.io/s/signalr-core-and-redux-v7gp4



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

redux devtools screanshot

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