Last active
August 24, 2023 23:20
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @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 |
good job, thanks!
Great! Thanks for sharing...
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
really good job, can u make sample full project?? thanks so much