Skip to content

Commit

Permalink
Merge pull request #95 from semoal/improvements
Browse files Browse the repository at this point in the history
Some improvements...

#89 Incompatible with React versions >16
#40 is this hooks automatically disconnect channels?
#43 Introduce to hoist channel connection
#44 useChannel(false) to prevent eager channel connection
  • Loading branch information
mayteio authored Aug 11, 2022
2 parents ae20585 + 4cb5d35 commit 7ac463b
Show file tree
Hide file tree
Showing 13 changed files with 220 additions and 59 deletions.
31 changes: 31 additions & 0 deletions .github/workflows/workflow.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: "🚀 CI Workflow"

on:
push:
branches:
- master
workflow_call: {}
pull_request: {}

jobs:
test:
name: 🃏 Test
runs-on: ubuntu-latest
steps:
- name: 🛑 Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.9.1

- name: ⬇️ Checkout repo
uses: actions/checkout@v3

- name: ⎔ Setup node
uses: actions/setup-node@v3
with:
cache: "yarn"
node-version: 16

- name: 📥 Download deps
run: yarn install

- name: 🃏 Test
run: yarn test
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@
"dependencies": {
"dequal": "^2.0.1",
"invariant": "^2.2.4",
"pusher-js": "^7.0.0"
"pusher-js": "^7.3.0"
},
"peerDependencies": {
"react": "^16.9.0"
"react": ">=16.9.0"
},
"devDependencies": {
"@babel/core": "^7.8.6",
Expand Down
43 changes: 24 additions & 19 deletions src/__tests__/useChannel.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
import { PusherChannelMock } from "pusher-js-mock";
import { PusherChannelMock, PusherMock } from "pusher-js-mock";
import React from "react";
import { renderHook } from "@testing-library/react-hooks";
import { renderHookWithProvider } from "../testUtils";
import { useChannel, NO_CHANNEL_NAME_WARNING } from "../core/useChannel";
import { useChannel } from "../core/useChannel";
import { __PusherContext } from "../core/PusherProvider";
import { ChannelsProvider } from "../web";

describe("useChannel()", () => {
test("should throw an error when no channelName present", () => {
const wrapper: React.FC = (props) => (
<__PusherContext.Provider value={{ client: {} as any }} {...props} />
test("should return undefined when channelName is falsy", () => {
const wrapper: React.FC = ({ children }) => (
<__PusherContext.Provider value={{ client: {} as any }}>
<ChannelsProvider>{children}</ChannelsProvider>
</__PusherContext.Provider>
);
const { result } = renderHook(() => useChannel(""), {
wrapper,
});

jest.spyOn(console, "warn");
renderHook(() => useChannel(undefined), { wrapper });
expect(console.warn).toHaveBeenCalledWith(NO_CHANNEL_NAME_WARNING);
expect(result.current).toBeUndefined();
});

test("should return undefined if no pusher client present", () => {
const wrapper: React.FC = (props) => (
<__PusherContext.Provider value={{ client: undefined }} {...props} />
const wrapper: React.FC = ({ children }) => (
<__PusherContext.Provider value={{ client: undefined }}>
<ChannelsProvider>{children}</ChannelsProvider>
</__PusherContext.Provider>
);
const { result } = renderHook(() => useChannel("public-channel"), {
wrapper,
Expand All @@ -35,19 +41,18 @@ describe("useChannel()", () => {
});

test("should unsubscribe on unmount", async () => {
const mockUnsubscribe = jest.fn();
const client = {
subscribe: jest.fn(),
unsubscribe: mockUnsubscribe,
};
const wrapper: React.FC = (props) => (
<__PusherContext.Provider value={{ client: client as any }} {...props} />
const client = new PusherMock("key");
client.unsubscribe = jest.fn();
const wrapper: React.FC = ({ children, ...props }) => (
<__PusherContext.Provider value={{ client: client as any }} {...props}>
<ChannelsProvider>{children}</ChannelsProvider>
</__PusherContext.Provider>
);
const { unmount } = await renderHook(() => useChannel("public-channel"), {
const { unmount } = renderHook(() => useChannel("public-channel"), {
wrapper,
});
unmount();

expect(mockUnsubscribe).toHaveBeenCalled();
expect(client.unsubscribe).toHaveBeenCalled();
});
});
88 changes: 88 additions & 0 deletions src/core/ChannelsProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { Channel, PresenceChannel } from "pusher-js";
import React, { useCallback, useRef } from "react";
import { ChannelsContextValues } from "./types";

import { usePusher } from "./usePusher";

// context setup
const ChannelsContext = React.createContext<ChannelsContextValues>({});
export const __ChannelsContext = ChannelsContext;

type AcceptedChannels = Channel | PresenceChannel;
type ConnectedChannels = {
[channelName: string]: AcceptedChannels[];
};

/**
* Provider that creates your channels instances and provides it to child hooks throughout your app.
*/

export const ChannelsProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const { client } = usePusher();
const connectedChannels = useRef<ConnectedChannels>({});

const subscribe = useCallback(
<T extends Channel & PresenceChannel>(channelName: string) => {
/** Return early if there's no client */
if (!client || !channelName) return;

/** Subscribe to channel and set it in state */
const pusherChannel = client.subscribe(channelName);
connectedChannels.current[channelName] = [
...(connectedChannels.current[channelName] || []),
pusherChannel,
];
return pusherChannel as T;
},
[client, connectedChannels]
);

const unsubscribe = useCallback(
(channelName: string) => {
/** Return early if there's no props */
if (
!client ||
!channelName ||
!(channelName in connectedChannels.current)
)
return;
/** If just one connection, unsubscribe totally*/
if (connectedChannels.current[channelName].length === 1) {
client.unsubscribe(channelName);
delete connectedChannels.current[channelName];
} else {
connectedChannels.current[channelName].pop();
}
},
[connectedChannels, client]
);

const getChannel = useCallback(
<T extends Channel & PresenceChannel>(channelName: string) => {
/** Return early if there's no client */
if (
!client ||
!channelName ||
!(channelName in connectedChannels.current)
)
return;
/** Return channel */
return connectedChannels.current[channelName][0] as T;
},
[connectedChannels, client]
);

return (
<ChannelsContext.Provider
value={{
unsubscribe,
subscribe,
getChannel,
}}
>
{children}
</ChannelsContext.Provider>
);
};
20 changes: 12 additions & 8 deletions src/core/PusherProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Options } from "pusher-js";
import { PusherContextValues, PusherProviderProps } from "./types";
import React, { useEffect, useRef, useState } from "react";

import React, { useEffect, useMemo, useRef, useState } from "react";
import { dequal } from "dequal";
import { PusherContextValues, PusherProviderProps } from "./types";
import { ChannelsProvider } from "./ChannelsProvider";

// context setup
const PusherContext = React.createContext<PusherContextValues>({});
Expand Down Expand Up @@ -30,7 +30,10 @@ export const CorePusherProvider: React.FC<PusherProviderProps> = ({
if (!cluster) console.error("A cluster is required for pusher");
}, [clientKey, cluster]);

const config: Options = { cluster, ...props };
const config: Options = useMemo(
() => ({ cluster, ...props }),
[cluster, props]
);

// track config for comparison
const previousConfig = useRef<Options | undefined>(props);
Expand All @@ -44,24 +47,25 @@ export const CorePusherProvider: React.FC<PusherProviderProps> = ({
if (
!_PusherRuntime ||
defer ||
!clientKey ||
props.value ||
(dequal(previousConfig.current, props) && client !== undefined)
) {
return;
}

// @ts-ignore
setClient(new _PusherRuntime(clientKey, config));
}, [client, clientKey, props, defer]);
}, [client, clientKey, props, defer, _PusherRuntime, config]);

return (
<PusherContext.Provider
value={{
client,
triggerEndpoint,
}}
children={children}
{...props}
/>
>
<ChannelsProvider>{children}</ChannelsProvider>
</PusherContext.Provider>
);
};
20 changes: 19 additions & 1 deletion src/core/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { default as Pusher, Options } from "pusher-js";
import {
Channel,
default as Pusher,
Options,
PresenceChannel,
} from "pusher-js";
import * as React from "react";
import "jest-fetch-mock";

Expand All @@ -8,8 +13,21 @@ export interface PusherContextValues {
triggerEndpoint?: string;
}

export interface ChannelsContextValues {
subscribe?: <T extends Channel & PresenceChannel>(
channelName: string
) => T | undefined;
unsubscribe?: <T extends Channel & PresenceChannel>(
channelName: string
) => void;
getChannel?: <T extends Channel & PresenceChannel>(
channelName: string
) => T | undefined;
}

export interface PusherProviderProps extends Options {
_PusherRuntime?: typeof Pusher;
children: React.ReactNode;
clientKey: string | undefined;
cluster:
| "mt1"
Expand Down
30 changes: 9 additions & 21 deletions src/core/useChannel.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Channel, PresenceChannel } from "pusher-js";
import { useEffect, useState } from "react";
import { usePusher } from "./usePusher";
import { useChannels } from "./useChannels";

/**
* Subscribe to a channel
Expand All @@ -16,31 +16,19 @@ import { usePusher } from "./usePusher";
* ```
*/

export const NO_CHANNEL_NAME_WARNING =
"No channel name passed to useChannel. No channel has been subscribed to.";

export function useChannel<T extends Channel & PresenceChannel>(
channelName: string | undefined
) {
const { client } = usePusher();
const [channel, setChannel] = useState<T | undefined>();
useEffect(() => {
/** Return early if there's no client */
if (!client) return;
const [channel, setChannel] = useState<Channel & PresenceChannel>();
const { subscribe, unsubscribe } = useChannels();

/** Return early and warn if there's no channel */
if (!channelName) {
console.warn(NO_CHANNEL_NAME_WARNING);
return;
}

/** Subscribe to channel and set it in state */
const pusherChannel = client.subscribe(channelName);
setChannel(pusherChannel as T);
useEffect(() => {
if (!channelName || !subscribe || !unsubscribe) return;

/** Cleanup on unmount/re-render */
return () => client?.unsubscribe(channelName);
}, [channelName, client]);
const _channel = subscribe<T>(channelName);
setChannel(_channel);
return () => unsubscribe(channelName);
}, [channelName, subscribe, unsubscribe]);

/** Return the channel for use. */
return channel;
Expand Down
19 changes: 19 additions & 0 deletions src/core/useChannels.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useContext, useEffect } from "react";
import { __ChannelsContext } from "./ChannelsProvider";
import { ChannelsContextValues } from "./types";

/**
* Provides access to the channels global provider.
*/

export function useChannels() {
const context = useContext<ChannelsContextValues>(__ChannelsContext);
useEffect(() => {
if (!context || !Object.keys(context).length)
console.warn(NOT_IN_CONTEXT_WARNING);
}, [context]);
return context;
}

const NOT_IN_CONTEXT_WARNING =
"No Channels context. Did you forget to wrap your app in a <ChannelsProvider />?";
5 changes: 4 additions & 1 deletion src/core/useTrigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,13 @@ export function useTrigger<TData = {}>(channelName: string) {
(eventName: string, data?: TData) => {
const fetchOptions: RequestInit = {
method: "POST",
body: JSON.stringify({ channelName, eventName, data })
body: JSON.stringify({ channelName, eventName, data }),
};

// @ts-expect-error deprecated since 7.1.0, but still supported for backwards compatibility
// now it should use channelAuthorization instead
if (client && client.config?.auth) {
// @ts-expect-error deprecated
fetchOptions.headers = client.config.auth.headers;
} else {
console.warn(NO_AUTH_HEADERS_WARNING);
Expand Down
2 changes: 2 additions & 0 deletions src/native/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,7 @@ export * from "../core/usePresenceChannel";
export * from "../core/useEvent";
export * from "../core/useClientTrigger";
export * from "../core/useTrigger";
export * from "../core/useChannels";
export * from "../core/ChannelsProvider";
export * from "./PusherProvider";
export { __PusherContext } from "../core/PusherProvider";
Loading

0 comments on commit 7ac463b

Please sign in to comment.