Skip to content

Commit

Permalink
🏷♻️ improved types, refactored hooks for simplicity
Browse files Browse the repository at this point in the history
  • Loading branch information
Harley Alexander committed Feb 18, 2020
1 parent 876cc4c commit 0d77035
Show file tree
Hide file tree
Showing 10 changed files with 133 additions and 146 deletions.
3 changes: 3 additions & 0 deletions .netlify/state.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"siteId": "aa919ed6-b843-411b-b6f7-828ea279b040"
}
49 changes: 26 additions & 23 deletions src/PusherProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,49 +10,52 @@ const PusherContext = React.createContext<PusherContextValues>({});
export const __PusherContext = PusherContext;

/**
* Provider for the pusher service in an app
* Provider that creates your pusher instance and provides it to child hooks throughout your app.
* Note, you can pass in value={{}} as a prop if you'd like to override the pusher client passed.
* This is handy when simulating pusher locally, or for testing.
* @param props Config for Pusher client
*/
export function PusherProvider({

export const PusherProvider: React.FC<PusherProviderProps> = ({
clientKey,
cluster,
triggerEndpoint,
authEndpoint,
auth,
defer = false,
children,
...props
}: PusherProviderProps) {
}) => {
// errors when required props are not passed.
invariant(clientKey, 'A client key is required for pusher');
invariant(cluster, 'A cluster is required for pusher');
const { children, ...additionalConfig } = props;
const config: Options = { cluster, ...additionalConfig };
if (authEndpoint) config.authEndpoint = authEndpoint;
if (auth) config.auth = auth;

const pusherClientRef = useRef<Pusher>();
const config: Options = { cluster, ...props };

const pusherClientRef = useRef<Pusher | undefined>();

// track config for comparison
const previousConfig = useRef<Options | undefined>();
const previousConfig = useRef<Options | undefined>(props);
useEffect(() => {
previousConfig.current = config;
previousConfig.current = props;
});

useEffect(() => {
// if client exists and options are the same, skip.
if (dequal(previousConfig.current, config) && pusherClientRef.current !== undefined) {
// Skip creation of client if deferring, a value prop is passed, or config props are the same.
if (
defer ||
props.value ||
(dequal(previousConfig.current, props) && pusherClientRef.current !== undefined)
) {
return;
}

// optional defer parameter skips creating the class.
// handy if you want to wait for something async like
// a JWT before creating it.
if (!defer) {
pusherClientRef.current = new Pusher(clientKey, config);
}
// create the client and assign it to the ref
pusherClientRef.current = new Pusher(clientKey, config);

return () => pusherClientRef.current && pusherClientRef.current.disconnect();
}, [clientKey, config, defer, pusherClientRef]);
// cleanup
return () => {
pusherClientRef.current && pusherClientRef.current.disconnect();
};
}, [clientKey, props, defer, pusherClientRef]);

return (
<PusherContext.Provider
Expand All @@ -64,4 +67,4 @@ export function PusherProvider({
{...props}
/>
);
}
};
17 changes: 0 additions & 17 deletions src/helpers.ts

This file was deleted.

15 changes: 4 additions & 11 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,16 @@
import { Options, AuthOptions } from 'pusher-js';
import Pusher, { Options } from 'pusher-js';
import * as React from 'react';

export interface PusherContextValues {
client?: any | undefined;
client?: React.MutableRefObject<Pusher | undefined>;
triggerEndpoint?: string;
}

export interface PusherProviderProps extends Options {
clientKey: string;
cluster: string;
authEndpoint?: string;
auth?: AuthOptions;
cluster: 'mt1' | 'us2' | 'us3' | 'eu' | 'ap1' | 'ap2' | 'ap3' | 'ap4';
triggerEndpoint?: string;
defer?: boolean;
children: React.ReactNode;
// for testing purposes
value?: any;
}

export interface useChannelOptions {
skip?: boolean;
value?: PusherContextValues;
}
20 changes: 14 additions & 6 deletions src/useChannel.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,36 @@
import { useEffect, useState } from 'react';
import invariant from 'invariant';

import { Channel, PresenceChannel } from 'pusher-js';
import { usePusher } from './usePusher';

/**
* Subscribe to channel
* Subscribe to a channel
*
* @param channelName The name of the channel you want to subscribe to.
* @typeparam Type of channel you're subscribing to. Can be one of Channel or PresenceChannel.
* @returns Instance of the channel you just subscribed to.
*
* @example
* useChannel("my-channel")
* ```javascript
* const channel = useChannel("my-channel")
* channel.bind('some-event', () => {})
* ```
*/

export function useChannel(channelName: string) {
export function useChannel<T extends Channel & PresenceChannel>(channelName: string) {
// errors for missing arguments
invariant(channelName, 'channelName required to subscribe to a channel');

const { client } = usePusher();
const pusherClient = client && client.current;

const [channel, setChannel] = useState<any>();
const [channel, setChannel] = useState<T | undefined>();

useEffect(() => {
if (!pusherClient) return;
const channel = pusherClient.subscribe(channelName);
setChannel(channel);
const pusherChannel = pusherClient.subscribe(channelName);
setChannel(pusherChannel as T);
}, [channelName, pusherClient]);
return channel;
}
17 changes: 15 additions & 2 deletions src/useClientTrigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,20 @@ import { useCallback } from 'react';
import invariant from 'invariant';
import { Channel, PresenceChannel } from 'pusher-js';

export function useClientTrigger(channel: Channel | PresenceChannel) {
/**
*
* @param channel the channel you'd like to trigger clientEvents on. Get this from [[useChannel]] or [[usePresenceChannel]].
* @typeparam TData shape of the data you're sending with the event.
* @returns A memoized trigger function that will perform client events on the channel.
* @example
* ```javascript
* const channel = useChannel('my-channel');
* const trigger = useClientTrigger(channel)
*
* const handleClick = () => trigger('some-client-event', {});
* ```
*/
export function useClientTrigger<TData = {}>(channel: Channel | PresenceChannel) {
channel &&
invariant(
channel.name.match(/(private-|presence-)/gi),
Expand All @@ -11,7 +24,7 @@ export function useClientTrigger(channel: Channel | PresenceChannel) {

// memoize trigger so it's not being created every render
const trigger = useCallback(
(eventName: string, data: any = {}) => {
(eventName: string, data: TData) => {
invariant(eventName, 'Must pass event name to trigger a client event.');
channel && channel.trigger(eventName, data);
},
Expand Down
4 changes: 1 addition & 3 deletions src/useEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { Channel, PresenceChannel } from 'pusher-js';
* @param channel Pusher channel to bind to
* @param eventName Name of event to bind to
* @param callback Callback to call on a new event
* @param dependencies Dependencies the callback uses.
*/
export function useEvent<D>(
channel: Channel | PresenceChannel | undefined,
Expand All @@ -32,8 +31,7 @@ export function useEvent<D>(
useEffect(() => {
if (channel === undefined) {
return;
}
channel.bind(eventName, callback);
} else channel.bind(eventName, callback);
return () => {
channel.unbind(eventName, callback);
};
Expand Down
97 changes: 40 additions & 57 deletions src/usePresenceChannel.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,23 @@
import { useCallback, useEffect, useState } from 'react';
import { useEffect, useState } from 'react';

import { PresenceChannel } from 'pusher-js';
import { PresenceChannel, Members } from 'pusher-js';
import invariant from 'invariant';

import { useChannel } from './useChannel';

/**
* Subscribe to presence channel events and get members back
*
* @param channelName name of presence channel. Should have presence- suffix.
* @param eventName name of event to bind to
* @param onEvent callback to fire when event is called
* @param dependencies dependencies array that onEvent uses
* @param options optional argument to skip events
* @param channelName name of presence the channel. Should have `presence-` suffix.
* @returns Object with `channel`, `members` and `myID` properties.
*
* @example
* const {members} = usePresenceChannel(
* "my-channel",
* "my-event",
* (message) => console.log(message),
* )
* ```javascript
* const { channel, members, myID } = usePresenceChannel("presence-my-channel");
* ```
*/
export function usePresenceChannel(channelName: string) {
// errors for missing arguments
invariant(channelName, 'channelName required to subscribe to a channel');
invariant(
channelName.includes('presence-'),
"Presence channels should use prefix 'presence-' in their name. Use the useChannel hook instead."
Expand All @@ -32,44 +26,34 @@ export function usePresenceChannel(channelName: string) {
// Get regular channel functionality
const [members, setMembers] = useState({});
const [myID, setMyID] = useState();
/**
* Get members info on subscription success
*/
const handleSubscriptionSuccess = useCallback((members: any) => {
setMembers(members.members);
setMyID(members.myID);
}, []);

/**
* Add or update member in object.
* @note not using a new Map() here to match pusher-js library.
* @param member member being added
*/
const handleAdd = useCallback((member: any) => {
setMembers(previousMembers => ({
...previousMembers,
[member.id]: member.info,
}));
}, []);

/**
* Remove member from the state object.
* @param member Member being removed
*/
const handleRemove = useCallback((member: any) => {
setMembers(previousMembers => {
const nextMembers: any = { ...previousMembers };
delete nextMembers[member.id];
return nextMembers;
});
}, []);

/**
* Bind and unbind to membership events
*/
const channel = useChannel(channelName);
// bind and unbind member events events on our channel
const channel = useChannel<PresenceChannel>(channelName);
useEffect(() => {
if (channel) {
// Get membership info on successful subscription
const handleSubscriptionSuccess = (members: Members) => {
setMembers(members.members);
setMyID(members.myID);
};

// add a member to the members object
const handleAdd = (member: any) => {
setMembers(previousMembers => ({
...previousMembers,
[member.id]: member.info,
}));
};

// remove a member from the members object
const handleRemove = (member: any) => {
setMembers(previousMembers => {
const nextMembers: any = { ...previousMembers };
delete nextMembers[member.id];
return nextMembers;
});
};

// bind to all member addition/removal events
channel.bind('pusher:subscription_succeeded', handleSubscriptionSuccess);
channel.bind('pusher:member_added', handleAdd);
Expand All @@ -80,22 +64,21 @@ export function usePresenceChannel(channelName: string) {
setMembers(channel.members.members);
setMyID(channel.members.myID);
}
}

// cleanup
return () => {
if (channel) {
// cleanup
return () => {
channel.unbind('pusher:subscription_succeeded', handleSubscriptionSuccess);
channel.unbind('pusher:member_added', handleAdd);
channel.unbind('pusher:member_removed', handleRemove);
}
};
}, [channel, handleSubscriptionSuccess, handleAdd, handleRemove]);
};
}

const presenceChannel = channel as PresenceChannel;
// to make typescript happy.
return () => {};
}, [channel]);

return {
channel: presenceChannel,
channel,
members,
myID,
};
Expand Down
7 changes: 5 additions & 2 deletions src/usePusher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ import { __PusherContext } from './PusherProvider';
import { PusherContextValues } from './types';

/**
* Provides access to the pusher client
* Provides access to the pusher client instance.
*
* @returns a `MutableRefObject<Pusher|undefined>`. The instance is held by a `useRef()` hook.
* @example
* const {client} = usePusher();
* ```javscript
* const { client } = usePusher();
* client.current.subscribe('my-channel');
* ```
*/
export function usePusher() {
const context = useContext<PusherContextValues>(__PusherContext);
Expand Down
Loading

0 comments on commit 0d77035

Please sign in to comment.