Skip to content

Instantly share code, notes, and snippets.

@Eleven-am
Last active August 15, 2022 09:39
Show Gist options
  • 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 React, {createContext, ReactNode, useEffect, useReducer, useRef} from "react";
const Phoenix = require("phoenix-channels");
const RealtimeContext = createContext<{ state: { topic: string, channel: any }[], dispatch: React.Dispatch<{ info: "connect" | "disconnect", topic: string, params: default_t, socket: any }>, socket: any }>({
state: [],
dispatch: () => {
},
socket: null
});
interface Presence {
ref: string;
username: string;
online: string;
reference: string;
}
type default_t = {
[p: string]: any
}
function reducer(state: { topic: string, channel: any }[], action: { info: 'connect' | 'disconnect', topic: string, params: default_t, socket: any }) {
switch (action.info) {
case 'connect':
const sock = state.find(s => s.topic === action.topic);
if (sock)
return state;
const channel = action.socket.channel(action.topic, action.params);
return [...state, {topic: action.topic, channel}];
case 'disconnect':
const disconnect = state.find(s => s.topic === action.topic);
const newState = state.filter(s => s.topic !== action.topic);
if (disconnect)
disconnect.channel.leave();
return newState;
}
}
export const RealtimeConsumer = ({apiKey, children}: { apiKey: string, children: ReactNode }) => {
const [socket, setSocket] = React.useState<any>(null);
const [state, dispatch] = useReducer(reducer, []);
useEffect(() => {
const socket = new Phoenix.Socket("ws://localhost:4000/socket", {params: {apiKey}});
socket.connect();
setSocket(socket);
}, [apiKey]);
return (
<RealtimeContext.Provider value={{socket, state, dispatch}}>
{children}
</RealtimeContext.Provider>
);
}
export const useChannel = (topic: string, params: any) => {
const {socket, state, dispatch} = React.useContext(RealtimeContext);
const [joins, setJoins] = React.useState<Presence[]>([]);
const [leaves, setLeaves] = React.useState<Presence[]>([]);
const channel = useRef<any>();
const presenceRef = useRef<default_t>({});
const [online, setOnline] = React.useState<Presence[]>([]);
const messageHandlers = useRef<Map<string, ((data: default_t) => void)>>(new Map());
const toSend = useRef<Map<string, any>>(new Map());
const connect = () => dispatch({info: 'connect', topic, params, socket});
const disconnect = () => dispatch({info: 'disconnect', topic, socket, params: {}});
useEffect(() => {
const intChan = state.find(s => s.topic === topic);
if (intChan) {
const chan = intChan.channel;
chan.join().receive("ok", () => {
channel.current = chan;
chan.on("presence_state", (presence: any) => {
const p = Phoenix.Presence.syncState({}, presence);
presenceRef.current = p;
const o = readPresenceDiff(p);
setOnline(o);
});
chan.on("presence_diff", (diff: any) => {
const p = Phoenix.Presence.syncDiff(presenceRef.current, diff);
presenceRef.current = p;
const o = readPresenceDiff(p);
setOnline(o);
const {joins, leaves} = diff;
const j = Object.keys(joins).map(e => {
const {metas: [{phx_ref: ref, ...rest}]} = joins[e];
return {ref, ...rest};
});
const l = Object.keys(leaves).map(e => {
const {metas: [{phx_ref: ref, ...rest}]} = leaves[e];
return {ref, ...rest};
});
setJoins(j);
setLeaves(l);
});
// @ts-ignore
for (const [event, handler] of messageHandlers.current.entries()) {
chan.off(event);
chan.on(event, handler);
messageHandlers.current.delete(event);
}
// @ts-ignore
for (const [event, data] of toSend.current.entries()) {
chan.push(event, data);
toSend.current.delete(event);
}
});
}
return () => {
if (intChan)
intChan.channel.bindings = [];
};
}, [state, topic]);
const readPresenceDiff = (presence: any) => {
const keys = Object.keys(presence);
return keys.map(e => {
const {metas: [{phx_ref: ref, ...rest}]} = presence[e];
return {ref, ...rest};
});
}
const subscribe = (event: string, callback: (data: any) => void) => {
if (channel.current) {
channel.current.off(event);
channel.current.on(event, callback);
} else
messageHandlers.current.set(event, callback);
}
const unsubscribe = (event: string) => {
if (channel.current)
channel.current.off(event);
messageHandlers.current.delete(event);
}
const send = (event: string, data: any) => {
if (channel.current)
channel.current.push(event, data);
else
toSend.current.set(event, data);
}
const whisper = (ref: string, message: any) =>
send("whisper", {to: ref, message});
return {
whisper, disconnect,
channel, joins, connect, send,
leaves, online, subscribe, unsubscribe
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment