JamesG
JamesG

Reputation: 164

TypeScript SignalR base implementation

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

Answers (2)

JamesG
JamesG

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

JamesG
JamesG

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

Related Questions