Search code examples
reactjssignalr

SignalR with React not very Realtime


I followed this this tutorial for signalR with vanilla javascript and all went well. I opened two browsers and both get updated instantly once I send a message from any of them. PROBLEM is located at the bottom.

Then I started another project in VS2017, this time a React template using aspnet core 2.1. Added signalr in Startup.cs

services.AddMvc().SetCompa.....
services.AddSignalR(); // added this
services.AddSpaStaticFiles......

app.UseSpaStaticFiles();
// then added below code
app.UseSignalR(routes =>
{
    routes.MapHub<ChatHub>("/chathub");
});
app.UseMvc(.....

Used this chathub class

public class ChatHub : Hub
{
    public async Task SendMessage(string user, string message)
    {
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }
}

and tried to convert the vanilla JS code to this react one

....
this.state = { name: "", message: "", messages: [], hubConnection: null }
...
componentDidMount() {
    const connection = new SignalR.HubConnectionBuilder()
        .withUrl("/chathub")
        .build();
    connection.on("ReceiveMessage", (user, message) => this.receiveMessage(user, message))
    connection.start().catch(err => console.error(err.toString())); 
    this.setState({ hubConnection: connection });
}

receiveMessage = (user, message) => {
    let msgs = this.state.messages;
    const msg = message
      .replace("/&/g", "&amp;")
      .replace("/</g", "&lt;")
      .replace("/>/g", "&gt;");
    const encodedMsg = user + " says " + msg;
    msgs.push(encodedMsg);
}

sendMessage = async (event) => {
    event.preventDefault();
    let user = this.state.name;
    let message = this.state.message;
    let connection = this.state.hubConnection;
    await connection
      .invoke("SendMessage", user, message)
      .catch(err => console.error(err.toString()));
    this.setState({ name: "", message: "" });
}; 

then added the following in render

<p>User: </p><input type="text" id="userInput" value={this.state.name} onChange={event => { this.setState({ name: event.target.value }); }} />
<p>Message: </p><input type="text" id="messageInput" value={this.state.message} onChange={event => { this.setState({ message: event.target.value }); }} />
<input type="button" id="sendButton" onClick={this.sendMessage} value="Send Message"/>

<ul>
    {this.state.messages.map(message => (
        <li key={message}>{message.toString()}</li>
    ))}
</ul>

then bound the methods to the class. Issue, other browser does not update instantly when the other browser send message. I need to perform some action which will cause it to rerender, like typing something in the input field. So how do I make it listen to incoming messages and rerender when it receive one? The problem which i think I have is that my client code does not know how to listen to the messages unless it's the one who called explicitly the SendMessage method


Solution

  • silly me. msgs.push does not trigger a re-render and just read that it's not good to mutate state. So I just performed a concat on the messages state to return new value, push the new message to the new array then setState

    let arr = []
    let msgs = this.state.messages.concat(arr);
    ......
    msgs.push(encodedMsg);
    this.setState({ messages: msgs });