Reputation: 164
I'm new with using SignalR and have currently hooked up my application to C#-backend with a successful connection.
But is there any good tutorials/repos/helper library that have set up a nice base implementation?
It would be nice to get rid of this when it will be used in many places, and possibly handling events in a nice discrete and controllable way.
const connection = new HubConnectionBuilder()
.withUrl(url, options)
.withAutomaticReconnect()
.withHubProtocol(new JsonHubProtocol())
.configureLogging(LogLevel.Information)
.build()
useEffect(() => {
const setUpSignalR = () => {
await connection.start()
.....
}
setUpSignalR()
return () => {
connection.stop()
}
}, [])
Upvotes: 1
Views: 1404
Reputation: 164
I've continued my implementation now with an event handler in front end to get rid of a lot of boiler plate code and reuse the events in multiple different places in the application depending on which view you are on. It would've been a pain to check "what is the name that triggers signalr and what does it actually pass as argument?"
The hook
import { useEffect } from 'react'
import { HubConnection } from '@microsoft/signalr'
interface Event {
name: string
function: (...args: any[]) => void
}
interface useClientEventsProps {
hubConnection: HubConnection | undefined
events: Event[]
}
/**
* Registers a handler that will be invoked when the hub function with the specified function name is invoked.
* @param {HubConnection} hubConnection The signalR hub connection.
* @param {Object[]} The events to register.
* @param {string} Events.name The name of the hub function to define.
* @param {function} Events.function The handler that will be raised when the hub function is invoked.
*/
export function useClientEvents({ hubConnection, events }: useClientEventsProps) {
useEffect(() => {
if (!hubConnection) {
return
}
events.forEach((event) => {
hubConnection.on(event.name, event.function)
})
return () => {
events.forEach((event) => hubConnection.off(event.name))
}
}, [hubConnection, events])
}
Some example event handlers with a delegate to another function.
export function reportProject(func: (date: string) => void) {
return {
name: 'reportProject',
function: func,
}
}
export function reportWorkOrder(func: (date: string) => void) {
return {
name: 'reportWorkOrder',
function: func,
}
}
export function lockReporting(func: (reported: boolean, date: string) => void) {
return {
name: 'lockReporting',
function: func,
}
}
How to use:
useClientEvents({
hubConnection: connection,
events: [
reportProject(handleReportProject),
reportWorkOrder(handleReportWorkOrder),
lockReporting(handleLockReporting),
],
})
Upvotes: 0
Reputation: 164
For anyone interested this is how all ended up.
Feel free to give advice on improvement or use by yourself if it is not all to bad.
import { HubConnection } from '@microsoft/signalr'
export type ConnectionState = {
error?: Error
loading: boolean
isConnected: boolean
accessToken?: string
connection?: HubConnection
}
export const initialConnectionState = {
error: undefined,
loading: true,
isConnected: false,
accessToken: undefined,
connection: undefined,
}
import { createContext, useContext } from 'react'
import { ConnectionState, initialConnectionState } from './state'
export const SignalRContext = createContext<ConnectionState>(initialConnectionState)
const useSignalRContext = () => {
const context = useContext(SignalRContext)
if (!context) throw new Error('There is no context values for signalr')
return context
}
export default useSignalRContext
import { SignalRContext } from './SignalRContext'
import { useConnection } from './useConnection'
const SignalRWrapper = ({ children }) => {
const connection = useConnection()
return <SignalRContext.Provider value={connection}>{children}</SignalRContext.Provider>
}
export default SignalRWrapper
import { useEffect, useReducer, useRef } from 'react'
import { useConfig } from '@griegconnect/krakentools-react-kraken-app'
import { HubConnectionBuilder, HubConnectionState, JsonHubProtocol, LogLevel } from '@microsoft/signalr'
import { useTenantServices } from '../../api-services/plan/TenantServices/TenantServices'
import { ConnectionState, initialConnectionState } from './state'
import { log } from './utils'
const startSignalRConnection = async (connection) => {
try {
await connection.start()
log('SignalR connection established')
} catch (err) {
log('SignalR Connection Error: ', err)
setTimeout(() => startSignalRConnection(connection), 5000)
}
}
export const useConnection = (): ConnectionState => {
const config = useConfig()
const { enlistClient, delistClient } = useTenantServices()
const reducer = (state: ConnectionState, newState: ConnectionState): ConnectionState => ({ ...state, ...newState })
const [state, setState] = useReducer(reducer, initialConnectionState)
const componentMounted = useRef(true)
useEffect(() => {
return () => {
componentMounted.current = false
}
}, [])
useEffect(() => {
const connection = new HubConnectionBuilder()
.withUrl(`${config.api.planApiUrl}/planhub`)
.withAutomaticReconnect()
.withHubProtocol(new JsonHubProtocol())
.configureLogging(LogLevel.Information)
.build()
startSignalRConnection(connection).then(() => {
if (componentMounted.current) setState({ loading: false, isConnected: true, connection })
enlistClient(connection.connectionId)
})
connection.onclose(() => {
log('SignalR connection closed')
delistClient(connection.connectionId)
})
connection.onreconnected(() => {
log('SignalR connection reconnecting')
enlistClient(connection.connectionId)
})
return () => {
connection.stop()
}
}, [config.api.planApiUrl, delistClient, enlistClient])
return state
}
And here is backend implementation
{
public class PlanHub : Hub, IPlanHub
{
private readonly IHubContext<PlanHub> hubContext;
public PlanHub(IHubContext<PlanHub> hubContext)
{
this.hubContext = hubContext;
}
public async Task AddToGroupAsync(string companyTenantId, string connectionId, CancellationToken cancel)
{
await hubContext.Groups.AddToGroupAsync(connectionId, companyTenantId, cancel);
}
public async Task RemoveFromGroupAsync(string companyTenantId, string connectionId, CancellationToken cancel)
{
await hubContext.Groups.AddToGroupAsync(connectionId, companyTenantId, cancel);
}
}
}
Together with a bunch of different event handlers.
public class TasksEventHandler : ITasksEventHandler
{
private readonly IHubContext<PlanHub> hubContext;
public TasksEventHandler(IHubContext<PlanHub> hubContext)
{
this.hubContext = hubContext;
}
public async Task HandleSaveTask(string companyTenantId, IList<TaskDetailDto> tasks, CancellationToken cancel)
{
await hubContext.Clients.Group(companyTenantId).SendAsync("saveTask", tasks, cancel);
}
.......
}
}
Upvotes: 2