Skip to content

Commit 1c850b6

Browse files
authored
fix: Ensure ref callback cleanup executes on MuxPlayer unmount (#1132)
Fixes #1112 ## Changes - Modified useCombinedRefs hook to collect cleanup functions returned by the function refs and ensure they are called. - Updated the toNativeProps utility to explicitly exclude the ref prop, ensuring that React handles the ref mechanism directly.
1 parent 4409304 commit 1c850b6

File tree

4 files changed

+68
-38
lines changed

4 files changed

+68
-38
lines changed

packages/mux-player-react/src/common/utils.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,9 @@ export const toNativeAttrValue = (propValue: any, _propName: string) => {
5151
return propValue;
5252
};
5353

54-
export const toNativeProps = (props = {}) => {
55-
return Object.entries(props).reduce<{ [k: string]: string }>((transformedProps, [propName, propValue]) => {
54+
export const toNativeProps = (props: { ref?: any; [key: string]: any } = {}) => {
55+
const { ref, ...restProps } = props;
56+
return Object.entries(restProps).reduce<{ [k: string]: string }>((transformedProps, [propName, propValue]) => {
5657
const attrName = toNativeAttrName(propName, propValue);
5758

5859
// prop was stripped. Don't add.

packages/mux-player-react/src/index.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import type MuxPlayerElement from '@mux/mux-player';
1515
import type { Tokens, MuxPlayerElementEventMap } from '@mux/mux-player';
1616
import { toNativeProps } from './common/utils';
1717
import { useRef } from 'react';
18-
import { useCombinedRefs } from './useCombinedRefs';
18+
import { useComposedRefs } from './useComposedRefs';
1919
import useObjectPropEffect, { defaultHasChanged } from './useObjectPropEffect';
2020
import { getPlayerVersion } from './env';
2121

@@ -141,7 +141,8 @@ const MuxPlayerInternal = React.forwardRef<MuxPlayerRefAttributes, MuxPlayerProp
141141
'mux-player',
142142
{
143143
suppressHydrationWarning: true, // prevent issues with SSR / player-init-time
144-
...toNativeProps({ ...props, ref }),
144+
...toNativeProps(props),
145+
ref,
145146
},
146147
children
147148
);
@@ -275,14 +276,14 @@ const MuxPlayer = React.forwardRef<
275276
Omit<MuxPlayerProps, 'playerSoftwareVersion' | 'playerSoftwareName'>
276277
>((props, ref) => {
277278
const innerPlayerRef = useRef<MuxPlayerElement>(null);
278-
const playerRef = useCombinedRefs(innerPlayerRef, ref);
279+
const playerRef = useComposedRefs(innerPlayerRef, ref);
279280
const [remainingProps] = usePlayer(innerPlayerRef, props);
280281
const [playerInitTime] = useState(props.playerInitTime ?? generatePlayerInitTime());
281282

282283
return (
283284
<MuxPlayerInternal
284285
/** @TODO Fix types relationships (CJP) */
285-
ref={playerRef as typeof innerPlayerRef}
286+
ref={playerRef as React.Ref<MuxPlayerElement>}
286287
defaultHiddenCaptions={props.defaultHiddenCaptions}
287288
playerSoftwareName={playerSoftwareName}
288289
playerSoftwareVersion={playerSoftwareVersion}

packages/mux-player-react/src/useCombinedRefs.ts

Lines changed: 0 additions & 32 deletions
This file was deleted.
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import * as React from 'react';
2+
3+
type PossibleRef<T> = React.Ref<T> | undefined;
4+
5+
/**
6+
* Set a given ref to a given value
7+
* This utility takes care of different types of refs: callback refs and RefObject(s)
8+
*/
9+
function setRef<T>(ref: PossibleRef<T>, value: T): (() => void) | void | undefined {
10+
if (typeof ref === 'function') {
11+
return ref(value);
12+
} else if (ref !== null && ref !== undefined) {
13+
(ref as React.MutableRefObject<T>).current = value;
14+
}
15+
}
16+
17+
/**
18+
* A utility to compose multiple refs together
19+
* Accepts callback refs and RefObject(s)
20+
*/
21+
function composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
22+
return (node) => {
23+
let hasCleanup = false;
24+
const cleanups = refs.map((ref) => {
25+
const cleanup = setRef(ref, node);
26+
if (!hasCleanup && typeof cleanup == 'function') {
27+
hasCleanup = true;
28+
}
29+
return cleanup;
30+
});
31+
32+
// React <19 will log an error to the console if a callback ref returns a
33+
// value. We don't use ref cleanups internally so this will only happen if a
34+
// user's ref callback returns a value, which we only expect if they are
35+
// using the cleanup functionality added in React 19.
36+
if (hasCleanup) {
37+
return () => {
38+
for (let i = 0; i < cleanups.length; i++) {
39+
const cleanup = cleanups[i];
40+
if (typeof cleanup == 'function') {
41+
cleanup();
42+
} else {
43+
setRef(refs[i], null);
44+
}
45+
}
46+
};
47+
}
48+
};
49+
}
50+
51+
/**
52+
* A custom hook that composes multiple refs
53+
* Accepts callback refs and RefObject(s)
54+
*/
55+
function useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
56+
// eslint-disable-next-line react-hooks/exhaustive-deps
57+
return React.useCallback(composeRefs(...refs), refs);
58+
}
59+
60+
export { composeRefs, useComposedRefs };

0 commit comments

Comments
 (0)