diff --git a/.eslintrc b/.eslintrc index bf0e755..204574b 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,41 +1,34 @@ { - "parser": "@typescript-eslint/parser", - "plugins": [ - "@typescript-eslint", - "react-hooks", - "simple-import-sort" - ], - "extends": [ - "eslint:recommended", - "plugin:react/recommended", - "plugin:react-hooks/recommended", - "plugin:@typescript-eslint/recommended", - "prettier" - ], - "parserOptions": { - "ecmaVersion": 2020, - "sourceType": "module", - "ecmaFeatures": { - "jsx": true - } - }, - "rules": {}, - "overrides": [ - { - "files": [ - "*.test.ts", - "*.test.tsx" - ], - "rules": { - // Allow testing runtime errors to suppress TS errors - "@typescript-eslint/ban-ts-comment": "off" - } - } - ], - "settings": { - "react": { - "pragma": "React", - "version": "detect" - } - } + "parser": "@typescript-eslint/parser", + "plugins": ["@typescript-eslint", "react-hooks", "simple-import-sort"], + "extends": [ + "eslint:recommended", + "plugin:react/recommended", + "plugin:react-hooks/recommended", + "plugin:@typescript-eslint/recommended", + "prettier" + ], + "parserOptions": { + "ecmaVersion": 2020, + "sourceType": "module", + "ecmaFeatures": { + "jsx": true + } + }, + "rules": {}, + "overrides": [ + { + "files": ["*.test.ts", "*.test.tsx"], + "rules": { + // Allow testing runtime errors to suppress TS errors + "@typescript-eslint/ban-ts-comment": "off" + } + } + ], + "settings": { + "react": { + "pragma": "React", + "version": "detect" + } + } } diff --git a/.prettierrc.cjs b/.prettierrc.cjs index 2e0033a..ef8e9b5 100644 --- a/.prettierrc.cjs +++ b/.prettierrc.cjs @@ -2,12 +2,12 @@ * {@type require('prettier').Config} */ module.exports = { - useTabs: false, - printWidth: 100, - singleQuote: true, - trailingComma: 'none', - bracketSameLine: false, - semi: true, - tabWidth: 2, - quoteProps: 'consistent' + useTabs: false, + printWidth: 100, + singleQuote: true, + trailingComma: 'none', + bracketSameLine: false, + semi: true, + tabWidth: 2, + quoteProps: 'consistent' }; diff --git a/CHANGELOG.md b/CHANGELOG.md index 84b2ebf..6427ccb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,27 +3,34 @@ 2023-12-27 ### Breaking changes + None ### Additional changes -* Fix buggy behavior where the reference to `useChannel` functions would be changing on every render. This would cause your useEffects to run even if there should be no change. + +- Fix buggy behavior where the reference to `useChannel` functions would be changing on every render. This would cause your useEffects to run even if there should be no change. # 0.0.1-alpha.6 2023-12-23 ### Breaking changes + None ### Additional changes -* Fixed a bug where if you successfully connected to a channel, but then later on the topic supplied to `useChannel` had changed to `null` and then back to the valid topic, the `useChannel` hook functions like `push` would no longer be holding a valid reference to the channel. Now, the hook will successfully update the reference and the functions will work as if the channel topic never changed. -* Use the internel channel `ref` when using `useChannel`'s `leave` + +- Fixed a bug where if you successfully connected to a channel, but then later on the topic supplied to `useChannel` had changed to `null` and then back to the valid topic, the `useChannel` hook functions like `push` would no longer be holding a valid reference to the channel. Now, the hook will successfully update the reference and the functions will work as if the channel topic never changed. +- Use the internel channel `ref` when using `useChannel`'s `leave` + # 0.0.1-alpha.5 2023-12-17 ### Breaking changes -* The typescript type for `useChannel`'s `PushEvent` now aligns with the rest of the types + +- The typescript type for `useChannel`'s `PushEvent` now aligns with the rest of the types + ```jsx type PushEvent = { - type: string; @@ -35,12 +42,14 @@ type PushEvent = { ``` ### Additional changes -* Added rollup build tooling which should reduce bundle size slightly -* Phoenix.js is now marked as a peer dependency -* `useChannel` can now accept a short circuit operation to delay connecting to the channel until the condition is met. - - ```jsx - // Delay connecting until id is defined - const [channel] = useChannel(id && `room:${id}`) - ``` -* The `push` function type has been improved to catch more potential errors. + +- Added rollup build tooling which should reduce bundle size slightly +- Phoenix.js is now marked as a peer dependency +- `useChannel` can now accept a short circuit operation to delay connecting to the channel until the condition is met. + + ```jsx + // Delay connecting until id is defined + const [channel] = useChannel(id && `room:${id}`); + ``` + +- The `push` function type has been improved to catch more potential errors. diff --git a/README.md b/README.md index 573602b..671d1bb 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Wrap the intended part of your application with a `PhoenixProvider`. import { PhoenixProvider } from 'use-phoenix'; const Application = () => { - return ...; + return ...; }; ``` @@ -17,14 +17,14 @@ Passing a `url` and params to your `PhoenixProvder` will connect to your socket ```tsx return ( - - ... - + + ... + ); ``` @@ -44,13 +44,13 @@ Later on when you would like to connect the socket: import { usePhoenix } from 'use-phoenix'; const Component = () => { - const { socket, connect } = usePhoenix(); + const { socket, connect } = usePhoenix(); - useEffect(() => { - connect('ws://localhost:4000/socket', { - params: { token: 'xyz' } - }); - }, [connect]); + useEffect(() => { + connect('ws://localhost:4000/socket', { + params: { token: 'xyz' } + }); + }, [connect]); }; ``` @@ -135,7 +135,7 @@ Optionally, if you would rather capture the response in a callback you can (or b ```ts const { data } = useEvent(channel, 'join', (data) => { - console.log(response); + console.log(response); }); ``` @@ -215,8 +215,8 @@ users[0].metas.lastSeen; const [channel, { leave }] = useChannel('chat:lobby'); useEffect(() => { - return () => { - leave(); - }; + return () => { + leave(); + }; }, [leave]); ``` diff --git a/rollup.config.js b/rollup.config.js index f66dfcf..e130e45 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -10,36 +10,36 @@ import pkg from './package.json' assert { type: 'json' }; const isDev = process.env.BUILD !== 'production'; const cjs = { - file: pkg.main, - format: 'cjs', - exports: 'named', - sourcemap: true, - plugins: !isDev && [terser()] + file: pkg.main, + format: 'cjs', + exports: 'named', + sourcemap: true, + plugins: !isDev && [terser()] }; const esm = { - file: pkg.module, - format: 'esm', - exports: 'named', - sourcemap: true + file: pkg.module, + format: 'esm', + exports: 'named', + sourcemap: true }; const extensions = ['.js', '.ts', '.tsx', '.json']; const plugins = [ typescript(), - resolve({ extensions }), - commonjs(), - babel({ exclude: 'node_modules/**', extensions }), - replace({ - 'preventAssignment': true, - 'process.env.NODE_ENV': JSON.stringify(isDev ? 'development' : 'production') - }) + resolve({ extensions }), + commonjs(), + babel({ exclude: 'node_modules/**', extensions }), + replace({ + 'preventAssignment': true, + 'process.env.NODE_ENV': JSON.stringify(isDev ? 'development' : 'production') + }) ].filter(Boolean); export default { - input: 'src/index.ts', - output: isDev ? [esm] : [cjs, esm], - plugins, - external: Object.keys(pkg.peerDependencies) + input: 'src/index.ts', + output: isDev ? [esm] : [cjs, esm], + plugins, + external: Object.keys(pkg.peerDependencies) }; diff --git a/src/PhoenixProvider.tsx b/src/PhoenixProvider.tsx index 2bdc773..7f5d714 100644 --- a/src/PhoenixProvider.tsx +++ b/src/PhoenixProvider.tsx @@ -5,73 +5,73 @@ import { Socket } from 'phoenix'; import React, { useCallback, useEffect, useState } from 'react'; export type PhoenixProviderProps = { - url?: string; - options?: Partial; - children?: React.ReactNode; - onOpen?: () => void; - onClose?: () => void; - onError?: () => void; + url?: string; + options?: Partial; + children?: React.ReactNode; + onOpen?: () => void; + onClose?: () => void; + onError?: () => void; }; export function PhoenixProvider({ url, options, ...props }: PhoenixProviderProps) { - const { children, onOpen, onClose, onError } = props; + const { children, onOpen, onClose, onError } = props; - const [socket, set] = useState(null); - const [isConnected, setConnected] = useState(false); + const [socket, set] = useState(null); + const [isConnected, setConnected] = useState(false); - const socketRef = useLatest(socket); + const socketRef = useLatest(socket); - const defaultListeners = useCallback( - (socket: PhoenixSocket) => { - if (onOpen) socket.onOpen(onOpen); - if (onClose) socket.onClose(onClose); - if (onError) socket.onError(onError); + const defaultListeners = useCallback( + (socket: PhoenixSocket) => { + if (onOpen) socket.onOpen(onOpen); + if (onClose) socket.onClose(onClose); + if (onError) socket.onError(onError); - socket.onOpen(() => { - setConnected(true); - }); + socket.onOpen(() => { + setConnected(true); + }); - socket.onClose(() => { - setConnected(false); - }); - }, - [onClose, onError, onOpen] - ); + socket.onClose(() => { + setConnected(false); + }); + }, + [onClose, onError, onOpen] + ); - const connect = useCallback( - (url: string, options?: Partial): PhoenixSocket => { - const socket = new Socket(url, options ?? {}) as PhoenixSocket; - socket.connect(); - set(socket); - defaultListeners(socket); + const connect = useCallback( + (url: string, options?: Partial): PhoenixSocket => { + const socket = new Socket(url, options ?? {}) as PhoenixSocket; + socket.connect(); + set(socket); + defaultListeners(socket); - return socket; - }, - [defaultListeners] - ); + return socket; + }, + [defaultListeners] + ); - useEffect(() => { - if (!url) return; + useEffect(() => { + if (!url) return; - const socket = connect(url, options); + const socket = connect(url, options); - return () => { - socket.disconnect(); - }; - }, [url, options, connect]); + return () => { + socket.disconnect(); + }; + }, [url, options, connect]); - return ( - - {children} - - ); + return ( + + {children} + + ); } PhoenixProvider.defaultProps = { - options: {}, - onOpen: null, - onClose: null, - onError: null, - connect: true, - children: null + options: {}, + onOpen: null, + onClose: null, + onError: null, + connect: true, + children: null }; diff --git a/src/cache.ts b/src/cache.ts index 0c24dc2..550c56f 100644 --- a/src/cache.ts +++ b/src/cache.ts @@ -3,30 +3,30 @@ import { ChannelMeta } from './useChannel'; export const cache = new Map>(); const defaultMeta: ChannelMeta = { - data: null, - status: 'joining', - isSuccess: false, - isLoading: true, - isError: false, - error: null + data: null, + status: 'joining', + isSuccess: false, + isLoading: true, + isError: false, + error: null }; export default { - insert: (topic: string, channelMeta: ChannelMeta) => { - cache.set(topic, channelMeta); - }, - get: (topic: string | undefined | boolean | null): ChannelMeta => { - if (typeof topic !== 'string') return defaultMeta; + insert: (topic: string, channelMeta: ChannelMeta) => { + cache.set(topic, channelMeta); + }, + get: (topic: string | undefined | boolean | null): ChannelMeta => { + if (typeof topic !== 'string') return defaultMeta; - const result = cache.get(topic); + const result = cache.get(topic); - if (result) { - return result; - } else { - return defaultMeta; - } - }, - delete: (topic: string): boolean => { - return cache.delete(topic); - } + if (result) { + return result; + } else { + return defaultMeta; + } + }, + delete: (topic: string): boolean => { + return cache.delete(topic); + } }; diff --git a/src/index.ts b/src/index.ts index 861accc..6d045de 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,10 +4,6 @@ export { useEvent } from './useEvent'; export { usePhoenix } from './usePhoenix'; export { usePresence } from './usePresence'; -export type { - Channel, - ChannelMeta, - ChannelState -} from './useChannel'; +export type { Channel, ChannelMeta, ChannelState } from './useChannel'; export type { PhoenixSocket, SocketConnectOption } from './usePhoenix'; diff --git a/src/useChannel/types.ts b/src/useChannel/types.ts index 8e80232..3763ad2 100644 --- a/src/useChannel/types.ts +++ b/src/useChannel/types.ts @@ -3,37 +3,40 @@ import { Merge } from '../util'; export type { Push } from 'phoenix'; export { Channel } from 'phoenix'; -export type ChannelState = Merge, { leave: () => void; push: PushFunction }>; +export type ChannelState = Merge< + ChannelMeta, + { leave: () => void; push: PushFunction } +>; export type PushEvent = { - event: string; - data?: Record; -} + event: string; + data?: Record; +}; export type ChannelStatus = - | 'joining' - | 'success' - | 'error' - | 'internal server error' - | 'connection timeout' - | 'closed'; + | 'joining' + | 'success' + | 'error' + | 'internal server error' + | 'connection timeout' + | 'closed'; export type ChannelMeta = { - data: JoinResposne | null; - status: ChannelStatus; - isSuccess: boolean; - isLoading: boolean; - isError: boolean; - error: any; + data: JoinResposne | null; + status: ChannelStatus; + isSuccess: boolean; + isLoading: boolean; + isError: boolean; + error: any; }; -export type PushFunction = (( - event: Event['event'], - data?: Event['data'] -) => Promise); +export type PushFunction = ( + event: Event['event'], + data?: Event['data'] +) => Promise; export type ChannelOptions = { - params?: Params extends Record ? Params : undefined; + params?: Params extends Record ? Params : undefined; }; export type ChannelParams = Record; diff --git a/src/useEvent/types.ts b/src/useEvent/types.ts index 5e3a37f..6e662ef 100644 --- a/src/useEvent/types.ts +++ b/src/useEvent/types.ts @@ -1,6 +1,6 @@ export type EventAction = { - event: string; - data: any; + event: string; + data: any; }; export type UseEventListener = (response: EventResponse) => void; diff --git a/src/useLatest.ts b/src/useLatest.ts index 661bc9f..eef0b3c 100644 --- a/src/useLatest.ts +++ b/src/useLatest.ts @@ -2,7 +2,7 @@ import type { RefObject } from 'react'; import { useRef } from 'react'; export default (val: T): RefObject => { - const ref = useRef(val); - ref.current = val; - return ref; + const ref = useRef(val); + ref.current = val; + return ref; }; diff --git a/src/usePhoenix/types.ts b/src/usePhoenix/types.ts index 2003afe..71a8b17 100644 --- a/src/usePhoenix/types.ts +++ b/src/usePhoenix/types.ts @@ -3,19 +3,19 @@ import { Channel, Socket } from 'phoenix'; export type PhoenixSocket = Socket & { channels: Channel[] }; export interface SocketConnectOption { - binaryType: BinaryType; - params: Record | (() => object); - transport: new (endpoint: string) => object; - timeout: number; - heartbeatIntervalMs: number; - longpollerTimeout: number; - /** The function to encode outgoing messages, Defaults to JSON encoder */ - encode: (payload: object, callback: (encoded: any) => void | Promise) => void; - decode: (payload: string, callback: (decoded: any) => void | Promise) => void; - logger: (kind: string, message: string, data: any) => void; - reconnectAfterMs: (tries: number) => number; - rejoinAfterMs: (tries: number) => number; - vsn: string; + binaryType: BinaryType; + params: Record | (() => object); + transport: new (endpoint: string) => object; + timeout: number; + heartbeatIntervalMs: number; + longpollerTimeout: number; + /** The function to encode outgoing messages, Defaults to JSON encoder */ + encode: (payload: object, callback: (encoded: any) => void | Promise) => void; + decode: (payload: string, callback: (decoded: any) => void | Promise) => void; + logger: (kind: string, message: string, data: any) => void; + reconnectAfterMs: (tries: number) => number; + rejoinAfterMs: (tries: number) => number; + vsn: string; } /** diff --git a/src/usePhoenix/usePhoenix.ts b/src/usePhoenix/usePhoenix.ts index 31495ab..be2a462 100644 --- a/src/usePhoenix/usePhoenix.ts +++ b/src/usePhoenix/usePhoenix.ts @@ -2,13 +2,13 @@ import React from 'react'; import { ConnectFunction, PhoenixSocket } from './types'; export const PhoenixContext = React.createContext<{ - socket: PhoenixSocket | null; - connect: ConnectFunction; - isConnected: boolean; + socket: PhoenixSocket | null; + connect: ConnectFunction; + isConnected: boolean; } | null>(null); export const usePhoenix = () => { - const context = React.useContext(PhoenixContext); - if (context === null) throw new Error('usePhoenix must be used within a PhoenixProvider'); - return context; + const context = React.useContext(PhoenixContext); + if (context === null) throw new Error('usePhoenix must be used within a PhoenixProvider'); + return context; }; diff --git a/src/usePresence/types.ts b/src/usePresence/types.ts index 4a03a94..91607a5 100644 --- a/src/usePresence/types.ts +++ b/src/usePresence/types.ts @@ -1,15 +1,15 @@ export type Metas = { phx_ref?: string }[]; export type PresenceState = { - [id: string]: T & { metas: M }; + [id: string]: T & { metas: M }; }; export type PresenceData = T & { - id: string; - metas: M; + id: string; + metas: M; }; export type PresenceDiff = { - joins: T & { metas: M }; - leaves: T & { metas: M }; + joins: T & { metas: M }; + leaves: T & { metas: M }; }; diff --git a/src/usePresence/usePresence.tsx b/src/usePresence/usePresence.tsx index 2bb4ccd..d9135e0 100644 --- a/src/usePresence/usePresence.tsx +++ b/src/usePresence/usePresence.tsx @@ -5,55 +5,55 @@ import type { Metas, PresenceState } from './types'; import { Merge } from '../util'; export function usePresence( - topic: string | undefined + topic: string | undefined ): Merge[] { - const [_presence, _setPresence] = useState>({}); - const { socket } = usePhoenix(); - - useEffect(() => { - if (socket && topic) { - const channel = socket.channel(topic, {}); - - channel.on('presence_state', (newState) => { - _setPresence((prevState) => { - if (Object.keys(prevState).length === 0) return newState; - return Presence.syncState(prevState, newState); - }); - }); - - channel.on('presence_diff', (newDiff) => { - _setPresence((prevState) => { - if (Object.keys(prevState).length === 0) return prevState; - return Presence.syncDiff(prevState, newDiff); - }); - }); - - channel.join(); - - return () => { - channel.leave(); - _setPresence({}); - }; - } - - return () => {}; - }, [socket, _setPresence, topic]); - - const items = useMemo( - () => - _presence - ? Object.keys(_presence).map((key: string) => { - let metas = _presence[key].metas; - - if (Array.isArray(metas) && metas.length === 1) { - metas = metas[0]; - } - - return { id: key, ..._presence[key], metas }; - }) - : [], - [_presence] - ) as Merge[]; - - return items; + const [_presence, _setPresence] = useState>({}); + const { socket } = usePhoenix(); + + useEffect(() => { + if (socket && topic) { + const channel = socket.channel(topic, {}); + + channel.on('presence_state', (newState) => { + _setPresence((prevState) => { + if (Object.keys(prevState).length === 0) return newState; + return Presence.syncState(prevState, newState); + }); + }); + + channel.on('presence_diff', (newDiff) => { + _setPresence((prevState) => { + if (Object.keys(prevState).length === 0) return prevState; + return Presence.syncDiff(prevState, newDiff); + }); + }); + + channel.join(); + + return () => { + channel.leave(); + _setPresence({}); + }; + } + + return () => {}; + }, [socket, _setPresence, topic]); + + const items = useMemo( + () => + _presence + ? Object.keys(_presence).map((key: string) => { + let metas = _presence[key].metas; + + if (Array.isArray(metas) && metas.length === 1) { + metas = metas[0]; + } + + return { id: key, ..._presence[key], metas }; + }) + : [], + [_presence] + ) as Merge[]; + + return items; } diff --git a/src/util.ts b/src/util.ts index 05e8a8c..ac26b98 100644 --- a/src/util.ts +++ b/src/util.ts @@ -3,37 +3,37 @@ import { PhoenixSocket } from './usePhoenix'; import { ChannelMeta } from './useChannel'; export const findChannel = (socket: PhoenixSocket | null, topic: string): Channel | undefined => { - if (typeof topic !== 'string') return undefined; - return socket?.channels.find((channel) => channel.topic === topic); + if (typeof topic !== 'string') return undefined; + return socket?.channels.find((channel) => channel.topic === topic); }; export const createMeta = ( - isSuccess: ChannelMeta['isSuccess'], - isLoading: ChannelMeta['isLoading'], - isError: ChannelMeta['isError'], - error: ChannelMeta['error'], - data: ChannelMeta['data'], - status: ChannelMeta['status'] + isSuccess: ChannelMeta['isSuccess'], + isLoading: ChannelMeta['isLoading'], + isError: ChannelMeta['isError'], + error: ChannelMeta['error'], + data: ChannelMeta['data'], + status: ChannelMeta['status'] ): ChannelMeta => ({ - isSuccess, - isLoading, - isError, - error, - data, - status + isSuccess, + isLoading, + isError, + error, + data, + status }); export const pushPromise = (push: Push): Promise => - new Promise((resolve, reject) => { - push.receive('ok', resolve).receive('error', reject); - }); + new Promise((resolve, reject) => { + push.receive('ok', resolve).receive('error', reject); + }); export type Merge = { - [K in keyof A | keyof B]: K extends keyof A & keyof B - ? A[K] | B[K] - : K extends keyof B - ? B[K] - : K extends keyof A - ? A[K] - : never; + [K in keyof A | keyof B]: K extends keyof A & keyof B + ? A[K] | B[K] + : K extends keyof B + ? B[K] + : K extends keyof A + ? A[K] + : never; };