Search code examples
c#react-nativesignalrasp.net-core-6.0

Signal R not real-time updating with .NET Core API, React Native Android App


I'm using the '@microsoft/signalr' react-native npm package to do a SignalR implementation between a .NETcore 6 api and react-native mobile app. The feature I am trying to use signal R for is a user chat instant messaging feature. I have used the same API and same code in a previous xamarin mobile app and everything worked flawlessly. So I know this is not an API issue.

I wrote the API followed several guides and connections seem to be setup correctly. I can even see message updates in my mobile app after a user sends a new message.

The problem I am having is I am not getting real-time updates from signal R for the messages. I have to refresh my page which triggers my original query for messages. I have exhausted all guides and need help.

I tried other signal R npm packages before this, but '@microsoft/signalr' seemed the most straight forward and was the only one I could get to work.

I expect to see real-time message updates between devices when a user sends a new message. I should see these updates regardless of device type. ie: Android Emulator, Desktop, Android Phone, iPhone, or iPhone simulator

I am including some client-side errors that I'm not entirely sure are from the signal R implementation but they have surfaced since I included the code so maybe they can help identify the problem.

error

error_

Here is my my react component code...

export default class UserChatComponent extends React.Component{

state = {
    user: this.props.user,
    sessionItem: this.props.sessionItem,
    messages: [],
    newMessage: ''
}

componentDidMount() {
    const asyncFunc = async ()=>{
        const messages = await new UserChatHandler().GetUserChatSessionMessages(this.state.sessionItem.sessionId);
        this.setState({ messages: messages });
    }
    
    asyncFunc();
    
    const URL = env.api_base_url +'ChatHub';
    let connectionBuilder = new signalR.HubConnectionBuilder()
    .withUrl(URL, {
        //skipNegotiation: true,
        //configureLogging: "warn",
        //withHandshakeResponseTimeout: 3000000,
        //transport: signalR.HttpTransportType.WebSockets 
        //,headers :  {jwtBearer: tokenValue}
    });

    if(env.name === 'DEBUG') connectionBuilder.configureLogging(signalR.LogLevel.Debug);

    this.connection = connectionBuilder.build();

    //this.connection.serverTimeoutInMilliseconds = 1000000;
    
    this.connection.on('SendMessage', (arg1) => {
        //console.log('connection.on',arg1)
    });
    this.connection.start()
    .then(() => {
        //console.log("Connected");
    }).catch((error) => {
        //console.log("Failed: "+ error);
    });
  }

  componentWillUnmount() {
    this.connection.stop()
        .then(() => {
            console.log('My Connection stopped.');
        })
        .catch((error) => {
            console.log("Failed: "+ error);
        });
    // fix Warning: Can't perform a React state update on an unmounted component
    this.setState = (state,callback)=>{
        
        return;
    };
}

  handleNewMessage = (text) => {
    this.setState({ newMessage: text});
 }

 sendMessage(){
    this.connection.invoke("SendMessage", 
    { 
        sessionId: this.state.sessionItem.sessionId, 
        senderUserId: this.state.user.id, 
        message: this.state.newMessage
    }).then(msg => {
        console.log('invoke', msg);
        let arr = [];
        let msgs = this.state.messages.concat(arr)
        msgs.push(msg);
        this.setState({
            
            messages: msgs, 
            newMessage: ''
        });
    }).catch((error) => alert(error));
 }

 onReportClicked(){
    PubSub.publish(PubSubEvents.ShowGlobalModal, 
        { show: true, type: ModalTypes.UgcCompliance, userInfo: this.state.user, data: this.state.sessionItem.sessionMembers[0]});
}

render(){
    return (
        <View style={appStyles.userChatContainer}>
            <View style={localStyles.messagesContainer} >
                <ScrollView ref={scrollView => this.scrollView = scrollView}
                    onContentSizeChange={( contentWidth, contentHeight ) => {
                        this._contentHeight = contentHeight;
                        this.scrollView.scrollTo({ y: this._contentHeight , animated: true})
                    }}>
                    {this.state.messages && 
                        this.state.messages.map((msg, _index) => 
                        this.state.user.id == msg.ownerId ?
                        (<View key={_index} style={localStyles.userMessageViewContainer} >
                            <Text>{msg.ownerFullname}</Text>
                            <View style={localStyles.userTextContainer}>
                                <Text style={localStyles.userText} >{msg.message}</Text>
                            </View>
                        </View>) :
                        (<View key={_index} style={localStyles.otherMessageViewContainer} >
                            <Text>{msg.ownerFullname}</Text>
                            <View style={localStyles.otherUserTextContainer}>
                                <Text style={localStyles.otherUserText} >{msg.message}</Text>
                            </View>
                        </View>)
                        )
                    }
                </ScrollView>
            </View>

            <View style={localStyles.newMessageContainer} >
                <View style={[{ flex: 1 }, appStyles.justifyAlignCenter ]}>
                    <TouchableOpacity
                        onPress = {
                            () => { this.onReportClicked(); }
                        }>
                        <Icon name="more-vert" size={40} color="#7575CF" />
                    </TouchableOpacity>
                </View>
                
                <View style={[{flex: 6 }, appStyles.justifyAlignCenter ]} >
                    <TextInput style = {[appStyles.input, localStyles.input]}
                            underlineColorAndroid = "transparent"
                            placeholder = "Message..."
                            placeholderTextColor = "#5453A6"
                            autoCapitalize = "none"
                            value={this.state.newMessage}
                            onChangeText = {this.handleNewMessage} />
                </View>

                <View style={[{ flex: 1, padding: 10 }, appStyles.justifyAlignCenter ]}>
                    <TouchableOpacity
                        onPress = {
                            () => { this.sendMessage(); }
                        }>
                        <Icon name="send" size={40} color="#7575CF" />
                    </TouchableOpacity>
                </View>
                
            </View>
        </View>
    );
}

}

EDIT: I debugged my api as well and verified there is indeed a connection being established between my emulator and a real device. Below is a screenshot of the connections property of the SignalR client.

API Watch


Solution

  • I finally figured out the problem. This was silly oversight on my part. My code was using the result of the this.connection.invoke() to add the new message to the list which updated the sending device to see the new message.

    The correct way is to use this.connection.on() with the exact same value that the api Clients.All.SendAsync() method replies to. In my case, each chat session is represented by a Guid and the API responsed to that Guid. However, the client was listening using this.connection.on("SendMessage") when it needs to be this.connection.on(GUID_VALUE).

    Below are my code changes. Notice, the result of invoke is irrevelant now, so using send or invoke would work.

    this.connection.on(this.state.sessionItem.sessionId.toString(), (msg) => {
            this.addNewMessageToState(msg);
        });
    
    sendMessage(){
        this.connection.invoke("SendMessage", 
        { 
            sessionId: this.state.sessionItem.sessionId, 
            senderUserId: this.state.user.id, 
            message: this.state.newMessage
        }).catch((error) => console.log('connection invoke error', error));
     }