Skip to content

Instantly share code, notes, and snippets.

@Eleven-am
Last active August 15, 2022 09:39
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Eleven-am/e5d9f23e50015dbf72efec992830ceb4 to your computer and use it in GitHub Desktop.
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
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