Last active
August 15, 2022 09:39
-
-
Save Eleven-am/e5d9f23e50015dbf72efec992830ceb4 to your computer and use it in GitHub Desktop.
This is a relatively typed phoenix channels hook that works incredibly well with react. you can create the same channels in multiple components as they share the same channel singleton
This file contains hidden or 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 { | |
createContext, | |
ReactNode, | |
useCallback, | |
useContext, | |
useEffect, useMemo, | |
useRef, | |
useState | |
} from "react"; | |
import {Channel, Presence, Socket} from "phoenix"; | |
type RealtimeChannel = { | |
channel: Channel, | |
presence: Presence | |
} | |
interface RealtimeContextProps { | |
channels: Map<string, RealtimeChannel>; | |
connect: (topic: string, params?: any) => void; | |
disconnect: (topic: string) => void; | |
} | |
const RealTimeContext = createContext<RealtimeContextProps>({ | |
channels: new Map(), | |
connect: () => { | |
}, | |
disconnect: () => { | |
} | |
}); | |
interface RealtimeProps { | |
children: ReactNode; | |
token: string; | |
endpoint: string; | |
} | |
export const RealtimeConsumer = ({children, token, endpoint}: RealtimeProps) => { | |
const [socket, setSocket] = useState<Socket | null>(null); | |
const [channels, setChannels] = useState<Map<string, RealtimeChannel>>(new Map()); | |
const connect = useCallback((topic: string, params?: any) => { | |
if (socket) { | |
const channel = socket.channel(topic, params); | |
const presence = new Presence(channel); | |
setChannels(channels => new Map(channels.set(topic, {channel, presence}))); | |
channel.join(); | |
} | |
}, [socket]); | |
const disconnect = useCallback((topic: string) => { | |
if (socket) { | |
const channel = channels.get(topic)?.channel; | |
if (channel) { | |
channel.leave(); | |
const tempChannels = new Map(channels); | |
tempChannels.delete(topic); | |
setChannels(tempChannels); | |
} | |
} | |
}, [socket, channels]); | |
useEffect(() => { | |
if (endpoint === '' || token === '') { | |
setSocket(null); | |
return; | |
} | |
const socket = new Socket(endpoint, {params: {token}}); | |
socket.connect(); | |
setSocket(socket); | |
}, [endpoint, token]); | |
if (socket) | |
return ( | |
<RealTimeContext.Provider value={{channels, connect, disconnect}}> | |
{children} | |
</RealTimeContext.Provider> | |
) | |
return null; | |
} | |
export const useChannel = (channelName: string, options: { username: string, identifier: string }) => { | |
const {connect: open, disconnect: close, channels} = useContext(RealTimeContext); | |
const [channel, setChannel] = useState<Channel | null>(null); | |
const [online, setOnline] = useState<any[]>([]); | |
const messageHandlers = useRef<Map<string, ((data: any) => void)>>(new Map()); | |
const connect = useCallback(() => { | |
open(channelName, options); | |
}, [channelName, options, open]); | |
const disconnect = useCallback(() => { | |
close(channelName); | |
}, [channelName, close]); | |
const handleSync = useCallback((presence: Presence) => { | |
const handler = messageHandlers.current.get('onSync'); | |
const presences: { metas: any[] }[] = presence.list() ?? []; | |
const users = presences.map(e => e.metas[0]); | |
setOnline(users); | |
handler && handler(users); | |
}, []); | |
const handleChannel = useCallback((chan: Channel, presence: Presence) => { | |
presence.onJoin((id, current, newPresence) => { | |
const handler = messageHandlers.current.get('onJoin'); | |
if (!current && handler) | |
handler(newPresence.metas[0]); | |
}); | |
presence.onLeave((id, current, leftPress) => { | |
const handler = messageHandlers.current.get('onLeave'); | |
if (current.metas.length === 0 && handler) | |
handler(leftPress.metas[0]); | |
}); | |
presence.onSync(() => handleSync(presence)); | |
chan.onMessage = (event, data) => { | |
const handler = messageHandlers.current.get(event); | |
if (handler) | |
handler(data); | |
return data; | |
}; | |
setChannel(chan); | |
handleSync(presence); | |
}, [handleSync]); | |
const manageChannel = useCallback(() => { | |
const channel = channels.get(channelName); | |
channel ? handleChannel(channel.channel, channel.presence) : setChannel(null); | |
}, [channels, channelName, handleChannel]); | |
const send = useCallback(<S extends object>(event: string, data: S) => { | |
if (channel && channel.state === 'joined') | |
channel.push(event, data); | |
}, [channel]); | |
const whisper = useCallback(<S extends object>(username: string, data: S) => { | |
send('whisper', {to: username, message: data}); | |
}, [send]); | |
const modifyPresenceState = useCallback(<S extends object>(newState: string, metadata?: S) => { | |
if (metadata) | |
send('modPresenceState', {presenceState: newState, metadata}); | |
else | |
send('modPresenceState', {presenceState: newState}); | |
}, [send]); | |
const on = useCallback(<S extends any>(event: string, handler: (data: S) => void) => { | |
messageHandlers.current.set(event, handler); | |
}, []); | |
const off = useCallback((event: string) => { | |
messageHandlers.current.delete(event); | |
}, []); | |
const onJoin = useCallback(<S extends any>(handler: (data: S) => void) => { | |
messageHandlers.current.set('onJoin', handler); | |
}, []); | |
const onLeave = useCallback(<S extends any>(handler: (data: S) => void) => { | |
messageHandlers.current.set('onLeave', handler); | |
}, []); | |
const onSync = useCallback(<S extends any>(handler: (data: S[]) => void) => { | |
messageHandlers.current.set('onSync', handler); | |
}, []); | |
const connected = useMemo(() => { | |
return channel && channel.state === 'joined'; | |
}, [channel]); | |
useEffect(() => { | |
manageChannel(); | |
}, [channels]); | |
return { | |
modifyPresenceState, on, off, | |
onJoin, onLeave, onSync, online, connect, | |
connected, transport: channel, disconnect, send, whisper | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment