Skip to content

Final hook prep #426

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion packages/react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,20 @@ Connect to private channel:
```ts
import { useEcho } from "@laravel/echo-react";

const { leaveChannel, leave } = useEcho(
const { leaveChannel, leave, stopListening, listen } = useEcho(
`orders.${orderId}`,
"OrderShipmentStatusUpdated",
(e) => {
console.log(e.order);
},
);

// Stop listening without leaving channel
stopListening();

// Start listening again
listen();

// Leave channel
leaveChannel();

Expand Down
84 changes: 84 additions & 0 deletions packages/react/src/config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import Echo, { type BroadcastDriver, type EchoOptions } from "laravel-echo";
import Pusher from "pusher-js";
import type { ConfigDefaults } from "../types";

let echoInstance: Echo<BroadcastDriver> | null = null;
let echoConfig: EchoOptions<BroadcastDriver> | null = null;

const getEchoInstance = <T extends BroadcastDriver>(): Echo<T> => {
if (echoInstance) {
return echoInstance as Echo<T>;
}

if (!echoConfig) {
throw new Error(
"Echo has not been configured. Please call `configureEcho()`.",
);
}

echoConfig.Pusher ??= Pusher;

echoInstance = new Echo(echoConfig);

return echoInstance as Echo<T>;
};

/**
* Configure the Echo instance with sensible defaults.
*
* @link https://laravel.com/docs/broadcasting#client-side-installation
*/
export const configureEcho = <T extends BroadcastDriver>(
config: EchoOptions<T>,
): void => {
const defaults: ConfigDefaults<BroadcastDriver> = {
reverb: {
broadcaster: "reverb",
key: import.meta.env.VITE_REVERB_KEY,
wsHost: import.meta.env.VITE_REVERB_HOST,
wsPort: import.meta.env.VITE_REVERB_PORT,
wssPort: import.meta.env.VITE_REVERB_PORT,
forceTLS:
(import.meta.env.VITE_REVERB_SCHEME ?? "https") === "https",
enabledTransports: ["ws", "wss"],
},
pusher: {
broadcaster: "reverb",
key: import.meta.env.VITE_PUSHER_APP_KEY,
cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER,
forceTLS: true,
wsHost: import.meta.env.VITE_PUSHER_HOST,
wsPort: import.meta.env.VITE_PUSHER_PORT,
wssPort: import.meta.env.VITE_PUSHER_PORT,
enabledTransports: ["ws", "wss"],
},
"socket.io": {
broadcaster: "socket.io",
host: import.meta.env.VITE_SOCKET_IO_HOST,
},
null: {
broadcaster: "null",
},
ably: {
broadcaster: "pusher",
key: import.meta.env.VITE_ABLY_PUBLIC_KEY,
wsHost: "realtime-pusher.ably.io",
wsPort: 443,
disableStats: true,
encrypted: true,
},
};

echoConfig = {
...defaults[config.broadcaster],
...config,
} as EchoOptions<BroadcastDriver>;

// Reset the instance if it was already created
if (echoInstance) {
echoInstance = null;
}
};

export const echo = <T extends BroadcastDriver>(): Echo<T> =>
getEchoInstance<T>();
Original file line number Diff line number Diff line change
@@ -1,75 +1,22 @@
import Echo, {
type BroadcastDriver,
type Broadcaster,
type EchoOptions,
} from "laravel-echo";
import Pusher from "pusher-js";
import { type BroadcastDriver } from "laravel-echo";
import { useCallback, useEffect, useRef } from "react";
import { echo } from "../config";
import type {
Channel,
ChannelData,
ChannelReturnType,
Connection,
ModelEvents,
ModelPayload,
} from "../types";
import { toArray } from "../util";

type Connection<T extends BroadcastDriver> =
| Broadcaster[T]["public"]
| Broadcaster[T]["private"]
| Broadcaster[T]["presence"];

type ChannelData<T extends BroadcastDriver> = {
count: number;
connection: Connection<T>;
};

type Channel = {
name: string;
id: string;
visibility: "private" | "public" | "presence";
};

type ConfigDefaults<O extends BroadcastDriver> = Record<
O,
Broadcaster[O]["options"]
>;

type ModelPayload<T> = {
model: T;
};

type ChannelReturnType<
T extends BroadcastDriver,
V extends Channel["visibility"],
> = V extends "presence"
? Broadcaster[T]["presence"]
: V extends "private"
? Broadcaster[T]["private"]
: Broadcaster[T]["public"];

// eslint-disable-next-line @typescript-eslint/no-unused-vars
type ModelName<T extends string> = T extends `${infer _}.${infer U}`
? ModelName<U>
: T;

type ModelEvents<T extends string> =
| `${ModelName<T>}Retrieved`
| `${ModelName<T>}Creating`
| `${ModelName<T>}Created`
| `${ModelName<T>}Updating`
| `${ModelName<T>}Updated`
| `${ModelName<T>}Saving`
| `${ModelName<T>}Saved`
| `${ModelName<T>}Deleting`
| `${ModelName<T>}Deleted`
| `${ModelName<T>}Trashed`
| `${ModelName<T>}ForceDeleting`
| `${ModelName<T>}ForceDeleted`
| `${ModelName<T>}Restoring`
| `${ModelName<T>}Restored`
| `${ModelName<T>}Replicating`;

let echoInstance: Echo<BroadcastDriver> | null = null;
let echoConfig: EchoOptions<BroadcastDriver> | null = null;
const channels: Record<string, ChannelData<BroadcastDriver>> = {};

const subscribeToChannel = <T extends BroadcastDriver>(
channel: Channel,
): Connection<T> => {
const instance = getEchoInstance<T>();
const instance = echo<T>();

if (channel.visibility === "presence") {
return instance.join(channel.name);
Expand All @@ -82,24 +29,6 @@ const subscribeToChannel = <T extends BroadcastDriver>(
return instance.channel(channel.name);
};

const getEchoInstance = <T extends BroadcastDriver>(): Echo<T> => {
if (echoInstance) {
return echoInstance as Echo<T>;
}

if (!echoConfig) {
throw new Error(
"Echo has not been configured. Please call `configureEcho()`.",
);
}

echoConfig.Pusher ??= Pusher;

echoInstance = new Echo(echoConfig);

return echoInstance as Echo<T>;
};

const leaveChannel = (channel: Channel, leaveAll: boolean): void => {
if (!channels[channel.id]) {
return;
Expand All @@ -112,17 +41,14 @@ const leaveChannel = (channel: Channel, leaveAll: boolean): void => {
}

if (leaveAll) {
getEchoInstance().leave(channel.name);
echo().leave(channel.name);
} else {
getEchoInstance().leaveChannel(channel.id);
echo().leaveChannel(channel.id);
}

delete channels[channel.id];
};

const toArray = <T>(item: T | T[]): T[] =>
Array.isArray(item) ? item : [item];

const resolveChannelSubscription = <T extends BroadcastDriver>(
channel: Channel,
): Connection<T> | void => {
Expand All @@ -148,66 +74,6 @@ const resolveChannelSubscription = <T extends BroadcastDriver>(
return channelSubscription;
};

/**
* Configure the Echo instance with sensible defaults.
*
* @link https://laravel.com/docs/broadcasting#client-side-installation
*/
export const configureEcho = <T extends BroadcastDriver>(
config: EchoOptions<T>,
): void => {
const defaults: ConfigDefaults<BroadcastDriver> = {
reverb: {
broadcaster: "reverb",
key: import.meta.env.VITE_REVERB_KEY,
wsHost: import.meta.env.VITE_REVERB_HOST,
wsPort: import.meta.env.VITE_REVERB_PORT,
wssPort: import.meta.env.VITE_REVERB_PORT,
forceTLS:
(import.meta.env.VITE_REVERB_SCHEME ?? "https") === "https",
enabledTransports: ["ws", "wss"],
},
pusher: {
broadcaster: "reverb",
key: import.meta.env.VITE_PUSHER_APP_KEY,
cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER,
forceTLS: true,
wsHost: import.meta.env.VITE_PUSHER_HOST,
wsPort: import.meta.env.VITE_PUSHER_PORT,
wssPort: import.meta.env.VITE_PUSHER_PORT,
enabledTransports: ["ws", "wss"],
},
"socket.io": {
broadcaster: "socket.io",
host: import.meta.env.VITE_SOCKET_IO_HOST,
},
null: {
broadcaster: "null",
},
ably: {
broadcaster: "pusher",
key: import.meta.env.VITE_ABLY_PUBLIC_KEY,
wsHost: "realtime-pusher.ably.io",
wsPort: 443,
disableStats: true,
encrypted: true,
},
};

echoConfig = {
...defaults[config.broadcaster],
...config,
} as EchoOptions<BroadcastDriver>;

// Reset the instance if it was already created
if (echoInstance) {
echoInstance = null;
}
};

export const echo = <T extends BroadcastDriver>(): Echo<T> =>
getEchoInstance<T>();

export const useEcho = <
TPayload,
TDriver extends BroadcastDriver = BroadcastDriver,
Expand All @@ -221,6 +87,7 @@ export const useEcho = <
) => {
const callbackFunc = useCallback(callback, dependencies);
const subscription = useRef<Connection<TDriver> | null>(null);
const listening = useRef(false);

const events = toArray(event);
const channel: Channel = {
Expand All @@ -231,11 +98,29 @@ export const useEcho = <
visibility,
};

const stopListening = () => {
const stopListening = useCallback(() => {
if (!listening.current) {
return;
}

events.forEach((e) => {
subscription.current!.stopListening(e, callbackFunc);
});
};

listening.current = false;
}, dependencies);

const listen = useCallback(() => {
if (listening.current) {
return;
}

events.forEach((e) => {
subscription.current!.listen(e, callbackFunc);
});

listening.current = true;
}, dependencies);

const tearDown = useCallback((leaveAll: boolean = false) => {
stopListening();
Expand All @@ -253,9 +138,7 @@ export const useEcho = <

subscription.current = channelSubscription;

events.forEach((e) => {
subscription.current!.listen(e, callbackFunc);
});
listen();

return tearDown;
}, dependencies);
Expand All @@ -270,9 +153,13 @@ export const useEcho = <
*/
leave: () => tearDown(true),
/**
* Stop listening for an event without leaving the channel
* Stop listening for event(s) without leaving the channel
*/
stopListening,
/**
* Listen for event(s)
*/
listen,
/**
* Channel instance
*/
Expand Down
8 changes: 7 additions & 1 deletion packages/react/src/index.iife.ts
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
export { configureEcho, echo, useEcho } from "./hook/use-echo";
export { configureEcho, echo } from "./config/index";
export {
useEcho,
useEchoModel,
useEchoPresence,
useEchoPublic,
} from "./hooks/use-echo";
8 changes: 7 additions & 1 deletion packages/react/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
export { configureEcho, echo, useEcho } from "./hook/use-echo";
export { configureEcho, echo } from "./config/index";
export {
useEcho,
useEchoModel,
useEchoPresence,
useEchoPublic,
} from "./hooks/use-echo";
Loading