SignalR & React example project (2023) – no extra package!

Have you ever needed to communicate from server to the react client? We normally use an api to call the server from client and we learned to live with that, but some time you need the opposite. This article is about how to use Aspnet SignalR to send notifications to the React app. On the other words server to client communication!

Scenario : We are making it easy, send the current date time from one client (by clicking a button) to the SignalR server and send the same message back to all clients (you can open multi browser pages to see the effect)

Get the code here

Aspnet SignalR

SignalR is a open-source library for Microsoft ASP.NET for server to send asynchronous notifications to client-side web applications. It comes with libraries both for server-side as well as client-side JavaScript. The JavaScript library of course can be used in a react web application. There is a good getting started document for the JavaScript library at Microsoft: Use ASP.NET Core SignalR with TypeScript. It is a very good beginning ,but if you want to mix it with react, things can easily go out of control.

Reactjs 

React is a front-end JavaScript library. Although, React is very simple to learn, it take time to be well understood and mastered. Using React you normally don’t have variables but you have constant; so whenever you define a variable with var or let it means that you probably are making a mistake. Instead of variables you have state and your job as the programmer is basically to manage the state of the react application.

So since you don’t have the variable if you have something that changes, you can not just assign a new value to it. This makes things a little bit more interesting while you are introducing some of external libraries.

Redux or not

Redux is a library for managing application state, it mixes very well with React. I personally love Redux, so normally for bigger projects I already have redux or similar things like easy peasy. But here I am making a simple project for you to get the idea, if your project is small you can use this sample as basis, if your project is bigger, then you can get the idea and expand it to redux or whatever you like.

Get the backend ready

So, I am going to make this very short as we are focusing on the react side. If you feel you can’t follow please visit the microsoft getting started (like at the beginning of the article) and get more familiar with donet.

We are doing it in dotnet 7, with a WebApi template. Just make sure you have dotnet 7 installed! Open your favorit terminal and then run this command to make new webapi project. The command creates a dotnet project the name of SignalrProject (or create one in visual studio):

dotnet new webapi -n SignalrProject

Open your project in visual studio and add a class by the name of SignalrHub.cs and paste the content below. It receives the message and the user from the client and send it back to all clients. Put your break point on line 5 if you want to understand it better.

using Microsoft.AspNetCore.SignalR;
public class SignalrHub : Hub
{
    public async Task NewMessage(string user, string message)
    {
        await Clients.All.SendAsync("messageReceived", user, message);
    }
}

Open Program.cs, we need to add a few lines to get SignalR up and running (add highlighted lines ) :

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSignalR();


// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.MapHub<SignalrHub>("/hub");

app.Run();

And that is basically it! For the backend (I did not removed REST API capabilities as in real project you probably going to need REST API too)

Now when you run your hub, should be ready to send and receive message at https://localhost:port/hab

Note : in some environments the casing of the URL letter matters ex /Hub vs /hub

Dealing with CORS ( Cross-Origin Resource Sharing )

In case you run your react app from different address, for example from https://localhost:3000 while dotnet signalR backend served from another port like https://localhost:5001 then you most likely are going to see error

Error: Failed to complete negotiation with the server: TypeError: Failed to fetch

And if you open the browser’s console you are going to see something like:

Access to fetch at /negotiate?negotiateVersion=1 from origin has been blocked by CORS policy: Response to preflight request doesn’t pass access control check: The value of the ‘Access-Control-Allow-Origin’ header in the response must not be the wildcard ‘*’ when the request’s credentials mode is ‘include’.

The error solves from the dotnet side, and not react side! The reason is that you have 2 deferent origins (domain if you want to simplify it). Of course, it is a development setup problem; because for your production setup, you most likely use some kind of application gateway and serve both backend and frontend from the same domain.

To solve CORS error development problem (in easiest way) in dotnet you can allow all origins for your dotnet backend while you are in development mode. Just add highlighted lines to Program.cs in IsDevelopment section.

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
    app.UseCors(x => x
        .AllowAnyMethod()
        .AllowAnyHeader()
        .SetIsOriginAllowed(origin => true) // allow any origin
        .AllowCredentials()); // allow credentials
}

For a more granular setting that gives you a better security please read Enable Cross-Origin Requests

React side with typescript

I make the example with typescript, so if you want javascript, just remove the extra!

To start with react you need NodeJS installed. It comes equipped with npm.

It makes sense that your react app sit in another folder than backend. See the Monorepo architecture, CI/CD and Build pipeline for mode details. Our structure is like this :

-- root folder
  |----SignalrProject
       |- ...
       |- SignalrProject.csproj 
  |-----frontend
       |-- ...
       |-- package.json

To create this setup navigate back to the root folder (cd ..) and run below command:

npx create-react-app frontend --template typescript

We just need to install the client side library to our react. CD to the frontend folder you just created and run command below to add the SignalR client side library to your React project:

npm i @microsoft/signalr @types/node

Open your frontend directory in visual studio code (or what ever editor you like) and create a file named signalRConnection.ts under ./src/signalr-connection.ts:

import * as signalR from "@microsoft/signalr";
const URL = process.env.HUB_ADDRESS ?? "https://localhost:5001/hub"; //or whatever your backend port is
class Connector {
    private connection: signalR.HubConnection;
    public events: (onMessageReceived: (username: string, message: string) => void) => void;
    static instance: Connector;
    constructor() {
        this.connection = new signalR.HubConnectionBuilder()
            .withUrl(URL)
            .withAutomaticReconnect()
            .build();
        this.connection.start().catch(err => document.write(err));
        this.events = (onMessageReceived) => {
            this.connection.on("messageReceived", (username, message) => {
                onMessageReceived(username, message);
            });
        };
    }
    public newMessage = (messages: string) => {
        this.connection.send("newMessage", "foo", messages).then(x => console.log("sent"))
    }
    public static getInstance(): Connector {
        if (!Connector.instance)
            Connector.instance = new Connector();
        return Connector.instance;
    }
}
export default Connector.getInstance;

So Let’s talk about the code above. We need to have a single connection that is why we use Singleton pattern. When you want to write your code make more functions like public newMessage for sending information to the server and also more delegates like onMessageReceived . The functions are straight forward but the delegates might need more elaborations

Receiving message from the server

On line 14 we told our app to listen for an incoming message by the name of messageReceived. So whenever server sends this type of message line 14 is going to call its inner function and with arguments username and message . Here we use delegation (that is very common in react) to call a function that is defined in our components.

If you want to learn more abut Event-Driven Callbacks used to achieve this communications, I explain it on React: Call Child Functions from Parent Functional Component

Usage

To demonstrate how to use this connector I rewrote the App.ts (App.js in case of JavaScript) . If you have more events you can create them as modules and import them. Also I made it with useState/useEffect pattern: useState to keep the state of the message, and useEffect to update the state whenever server sends a new message. By using useEffect, you ensure that the subscription to the event listener is set up only once when the component is mounted. Without useEffect, the subscription could be set up multiple times (e.g., on every render), which could lead to performance issues and bugs such as multiple event listeners being attached.

import React, { useEffect, useState } from 'react';
import './App.css';
import Connector from './signalr-connection'
function App() {
  const { newMessage, events } = Connector();
  const [message, setMessage] = useState("initial value");
  useEffect(() => {
    events((_, message) => setMessage(message));
  });
  return (
    <div className="App">
      <span>message from signalR: <span style={{ color: "green" }}>{message}</span> </span>
      <br />
      <button onClick={() => newMessage((new Date()).toISOString())}>send date </button>
    </div>
  );
}
export default App;

Here I just wanted to keep it simple so instead of adding an input and getting the value, I just sent the current date to the server, then get it back and display it. You can open 2 browsers and and see that this value is updates simultaneously.

To see the magic of the SignalR, navigate to SignalrProject folder run the backend (dotnet run) this run at the server on port 5001 https://localhost:5001. Then navigate the the frontend and run command npm ci to install dependencies and then npm start. Then you can open multiple instance of frontend on the address http://localhost:3000 . When you click the button [send date] all the instances get updated with new date time value (date is pushed from the server to the client) .

Feel free get the code all together from GitHub.

Make more events (server calls):

Essentially we are done here, but if you want to get some idea about how to add more events (how to receive other type of messages).
Back end : add more like line 6 to your SignalrHub
Front end: Let’s modify signalr-connection.ts and add “onSomeOtherServerEventReceived” event. First update the signature definition of your delegate (typescript only )

public events: (
        onMessageReceived: (username: string, message: string) => void,
        onSomeOtherServerEventReceived: (payload: Payload) => void
    ) => void;

Then you add the event “onSomeOtherServerEventReceived”

this.events = (onMessageReceived, onSomeOtherServerEventReceived) => {
            this.connection.on("messageReceived", (username, message) => {
                onMessageReceived(username, message);
            });
            this.connection.on("somethingDefinedOnServer", (payload) => {
                somethingDefinedOnServer(payload);
            });
        };

then use it in your component (App.ts in our case):

useEffect(() => {
    const handleMessageReceived = (_, message) => setMessage(message);
    const handleSomeOtherServerEventReceived = (payload) => { // do something... }
    events(handleMessageReceived ,handleSomeOtherServerEventReceived );
  });

Get the code here

More reading

now that you learned about react and signalR, maybe you wan to learn about :


Posted

in

by

Comments

4 responses to “SignalR & React example project (2023) – no extra package!”

  1. Sabine Avatar
    Sabine

    Thanks for the great post, really helpful and easy to follow! 👍 By the way, you talked about handling CORS issues in development. Any tips for a more secure setup when going live?

    1. Daniel Abrahamberg Avatar

      Thanks for the comment Sabine!

      You might think that using `app.Environment.IsDevelopment()` ensures safety in other environments like testing and production. However, I’ve encountered situations where people accidentally run their app in development mode, compromising security.

      When it comes to setting up a secure live environment, my approach usually involves integrating it into a broader architecture. Typically, I use a reverse proxy as part of the setup. This helps route all the services, both backend and frontend, from the same origin. For example, you could have `localhost/project/frontend` and `localhost/project/signalR`. I find NGINX to be user-friendly for reverse proxying, but my personal favorite is Envoy. Importantly, my entire setup is containerized for better management and security.

  2. Tiendao Avatar
    Tiendao

    why we not have dependencies in useEffect ?

    1. Daniel Abrahamberg Avatar

      You are right Tiendao,
      I wanted to keep it focused on the subject but that is a good feedback, [events] should work for this sample.

      useEffect(() => {
      events((_, message) => setMessage(message));
      },[events]);

Leave a Reply

Your email address will not be published. Required fields are marked *