Skip to content

Instantly share code, notes, and snippets.

@jdnichollsc
Last active August 24, 2023 23:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jdnichollsc/4d0c1a7fffafe9a932033d60d8a532d7 to your computer and use it in GitHub Desktop.
Save jdnichollsc/4d0c1a7fffafe9a932033d60d8a532d7 to your computer and use it in GitHub Desktop.
Hooks with React Native for realtime connection using SignalR with NetInfo and AppState for automatic reconnection
import React, { useEffect } from 'react'
import {
Text
} from 'react-native'
import useSignalR from './useSignalR'
import { getCounterAndToken } from '../services/api'
import { hideError, showErrorAndRetryAction } from '../services/common'
/**
* A hook for getting realtime updates of a counter
*/
export const useRealTimeCounter = function () {
const [state, setState] = useSignalR('UpdateCounter', {
initialData: 0,
newMethod: function (newMessage) {
setState({
error: null,
data: newMessage
})
}
})
return [state, setState]
}
function CounterDemo () {
const [state, setState] = useRealTimeCounter()
const counter = state.data
const verifyConnection = async () => {
hideError()
if (state.isConnected) {
const { counterUpdated, accessToken } = await getCounterAndToken()
setState({
data: counterUpdated,
accessToken,
loading: true
})
} else throw Error('Network Error')
}
useEffect(() => {
verifyConnection().catch((err) => setState({ error: err }))
}, [state.isConnected])
useEffect(() => {
hideError()
if (state.error) showErrorAndRetryAction(state.error, verifyConnection)
}, [state.error])
return (
<Text>{counter}</Text>
)
}
export default CounterDemo
import { useEffect, useState } from 'react'
import { AppState } from 'react-native'
/**
* A React Hook which updates when the app state changes.
*
* @returns The app state.
*/
export default () => {
const [appState, setAppState] = useState(AppState.currentState)
function onChange (newState) {
setAppState(newState)
}
useEffect(() => {
AppState.addEventListener('change', onChange)
return () => {
AppState.removeEventListener('change', onChange)
}
})
return appState
}
/**
* @flow
*/
import { useEffect, useState } from 'react'
import NetInfo, { NetInfoStateType } from '@react-native-community/netinfo'
type NetInfoState = {
type: any,
isConnected: boolean,
isInternetReachable: boolean,
details: any
}
const inititalState: NetInfoState = {
type: NetInfoStateType.unknown,
isConnected: false,
isInternetReachable: false,
details: null
}
/**
* A React Hook which updates when the connection state changes.
*
* @returns The connection state.
*/
export default function (): NetInfoState {
const [netInfo, setNetInfo] = useState(inititalState)
function onChange (newState) {
setNetInfo(newState)
}
useEffect(() => {
NetInfo.fetch().then(setNetInfo)
NetInfo.addEventListener('connectionChange', onChange)
return () => {
NetInfo.removeEventListener('connectionChange', onChange)
}
}, [])
return netInfo
}
/**
* @flow
*/
import React, { useEffect, useReducer, useRef } from 'react'
import {
HubConnection,
HubConnectionBuilder,
HubConnectionState
} from '@aspnet/signalr'
import useNetInfo from './useNetInfo'
import useAppState from './useAppState'
import { config } from '../services'
/**
* Create a new Hub connection.
*/
export const makeHubConnection = (accessToken: string): HubConnection => {
const { signalRUrl } = config
return new HubConnectionBuilder().withUrl(signalRUrl, {
// transport: HttpTransportType.WebSockets,
accessTokenFactory: () => accessToken,
logMessageContent: true
}).configureLogging({
log: function (logLevel, message) {
// TODO: Remove after finish testing
// console.log('SIGNALR: ' + new Date().toISOString() + ': ' + message)
}
}).build()
}
type Options = {
retryOnError: boolean,
retrySecondsDelay: number,
initialData: any,
newMethod: Function
}
function delay (timeout) {
return new Promise(resolve => setTimeout(resolve, timeout))
}
/**
* Initialize the connection with retries
* @param {HubConnection} connection - The current Hub connection.
* @param {Options} options - The options of the connection.
*/
async function startConnection (connection: HubConnection, options: Options) {
try {
await connection.start()
} catch (err) {
if (options.retrySecondsDelay) {
await delay(options.retrySecondsDelay)
return startConnection(connection, options)
} else throw err
}
};
type RealTimeState = {
data: any,
error?: Error,
loading: boolean,
isConnected: boolean,
accessToken?: string,
connection?: HubConnection
}
/**
* A generic hook to stablish a realtime connection when the device is connected to internet.
* @param {String} methodName - The name of the hub method to define.
* @param {*} options - The options of the realtime connection
*/
export const useSignalR = function (methodName: string, options: Options = {
retrySecondsDelay: 2000,
initialData: [],
retryOnError: true
}): [RealTimeState, React.Dispatch<any>] {
const netInfo = useNetInfo()
const appState = useAppState()
const unmounted = useRef(false)
const connecting = useRef(false)
const initialState = {
data: options.initialData,
error: null,
loading: true,
isConnected: netInfo.isConnected && netInfo.isInternetReachable,
accessToken: null,
connection: null
}
const reducer = (state: RealTimeState, newState: RealTimeState): RealTimeState => ({ ...state, ...newState })
const [state, setState] = useReducer(reducer, initialState)
const initializeConnection = async () => {
if (
state.connection.connectionState !== HubConnectionState.Connected &&
!connecting.current
) {
connecting.current = true
try {
await startConnection(state.connection, options)
setState({ loading: false, connection: state.connection })
} catch (error) {
setState({ error })
} finally {
connecting.current = false
}
}
}
/**
* Configure the connection
*/
useEffect(() => {
if (state.accessToken !== null && state.connection === null) {
const connection = makeHubConnection(state.accessToken)
connection.on(methodName, options.newMethod)
connection.onclose((error) => {
if (error && !unmounted.current) {
setState({ error })
if (options.retryOnError && state.isConnected) {
initializeConnection()
}
}
})
setState({ connection })
}
/**
* Close the connection before unmount
*/
return () => {
unmounted.current = true
if (
state.connection &&
state.connection.connectionState === HubConnectionState.Connected
) {
state.connection.stop()
}
}
}, [ state.accessToken, state.connection ])
/**
* Initialize or Stop the connection
*/
useEffect(() => {
if (state.connection) {
if (
!unmounted.current &&
state.isConnected &&
appState === 'active'
) {
initializeConnection()
} else if (state.connection.connectionState !== HubConnectionState.Disconnected) {
state.connection.stop()
}
}
}, [
appState,
state.isConnected,
state.accessToken,
state.loading,
state.connection
])
/**
* Detect app state and network changes
*/
useEffect(() => {
const isConnected = (
appState === 'active' &&
netInfo.isConnected !== false &&
netInfo.isInternetReachable !== false
)
setState({
isConnected,
error: isConnected ? null : new Error('Network Error')
})
}, [netInfo, appState])
return [state, setState]
}
export default useSignalR
@fukemy
Copy link

fukemy commented Oct 26, 2022

really good job, can u make sample full project?? thanks so much

@thnha
Copy link

thnha commented Jan 11, 2023

good job, thanks!

@prince272
Copy link

Great! Thanks for sharing...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment