This post will address the client-side challenges of using SignalR. There’s tons of documentation from Microsoft on how to set up SignalR on the server. Unfortunately, there’s not much out there on how to integrate it with React.
The first challenge in adding SignalR to a React project is how to import the SignalR scripts into your project. Microsoft has an official release of SignalR on Nuget but not on npm, which is absurd. There are several packages on the npm registry which wrap the SignalR scripts and perhaps export them as CommonJS or RequireJS modules. But these packages are not well-maintained and have a fraction of the downloads of Sockets.io, so I would be reluctant to add them as a dependency for my project.
A stupid quirk of SignalR is that it has a dependency on jQuery. SignalR prioritizes communication with the server via sockets, but it needs jQuery for long polling and forever frames as a fallback for browsers that don’t support sockets. Its dependency on jQuery can’t be replaced with another library, although the feature is supposedly in the works. There are also SignalR offshoots that don’t support long polling/forever frames and therefore don’t need jQuery.
In my situation, it was tolerable to simply add the scripts from a cdn, and insert them into my app at the root of my app in the index.html page (click here to see it in my github sample project). As much as I cringe at using SignalR and jQuery as global variables, it made no practical difference in case of my project where push notifications were the primary purpose of the project and would be utilized in every screen.
Client-side Architecture of SignalR
One of the client-side requirements is that SignalR should function as a singleton on the client and maintain a stateful connection; individual React components shouldn’t be responsible for establishing a connection to the SignalR hub on the server. React doesn’t have the concept of a service like Angular has, and the best approximation of a service/singleton is to create a higher-order component (HOC) near the root of my App component. A HOC is a wrapper for another component. It doesn’t necessarily contain any user interface elements, but decorates the wrapped component with some functionality. A HOC might also be called a decorator or a mixin.
The SignalR HOC needs to establish the connection with the hub on the server, and aggregate the subscriptions of the app’s various components. As the user navigates from route to route, the app will need to subscribe and unsubscribe to domains and/or IDs. On a page with a component with a list of widgets, the user needs to be notified of added and deleted widgets. On a widget detail page, they need to be notified of change in the properties for that particular widget. To minimize updates being pushed to each client, the client needs to notify the server exactly what objects and object IDs it is interested in. As you can see in the sample HOC in my github project, the HOC wraps a component and injects it with the methods to communicate with the SignalR hub.
Communication between Components
The question is how a modular React component can communicate its subscriptions to the push notification HOC. One possibility that I considered was to use Redux actions and reducer, which would be the most orthodox solution to implement. In a component’s componentWillMount action, it would invoke an action, and the action would add or remove a subscription from the aggregate list of subscriptions in the Redux state object. The drawback with this solution is that it’s not a good way of event notification. With each change in the Redux store, the HOC would have to compare the current app state with the previous state to find the additions and/or subtractions. Redux isn’t a good message bus when you’re only interested in the latest message.
A more appropriate solution for inter-component messaging is to use the React context object. Context should be used sparingly; it is not a replacement for the Redux store or props, and it might change in future versions of React. (Good tutorials on using Context are here and here.) In my case, I use Context only for giving child components a callback for sending a message to the HOC on what the child component was interested in subscribing to. I could also have used props to pass a callback directly between components, but that would be cumbersome because it would require a callback prop to be passed from parent to child to grandchild to great-grandchild, etc.
The child components use the Context to pass a message describing the type of types of notifications the component is interested in listening to. Let’s assume that our app has two components, Stocks and Weather, which describe stock price and temperature. The state of our Redux store might look like:
When the Stocks component mounts, SignalR should subscribe to changes in stocks, and when the Weather component loads, SignalR should cease its subscription to stocks notifications and switch to listening to changes in weather data. In a real-world application, my Stocks component would probably listen to additions/deletions to the list of stocks, and the Stock component would listen to changes in the particular stock, but in this demo I’m simplifying the structure of the notification in order to focus on integration between SignalR and Redux.
When the stocks component loads, it will send a message to the HOC such as the following:
Then in componentWillUnmount it will send:
I use the subscription’s route attribute as a natural, built-in contract for the client and server to agree on what the client’s dependency is. The route table is already familiar to both the client and server; in a real-world case the route might look like ‘api/stocks/ibm’, which would denote a subscription to changes in the IBM data object.
The HOC, not the component, will be notified when a push is received from the server. Push messages should generally contain very small payloads. They are best used to notify the client that its data cache is stale and needs to be refreshed. Push large amounts of data thru SignalR leads to unstable connections and errors, in my experience. The stateful list of subscriptions can be maintained within either the higher-order component or the Redux app state object. The client needs to keep track of the current list of subscriptions in order to invoke the proper updateMethod when it receives a push notice that the data needs to be refreshed. Therefore each subscription contains the corresponding update action that needs to be invoked to refresh the data. The HOC’s list of subscriptions can most easily be contained in a Map object (not to be confused with the map function). Each Map entry will have a key set to the subscription’s route and the value will references the action that will be invoked to refresh the data. We add a subscription to the Map in the PushService’s
this._subscriptions = new Map();
Variables are normally stored within the component’s state, but in this case I put them in a plain ol’ private variable because 1) my component is a singleton 2) I do not need to re-render the component when the variable changes.
You can view a sample project with this client-side code at my github repo. Keep in mind that the purpose of this github repo is to display code snippets and is not a working project.