Skip to content

Commit 65a5e2a

Browse files
authored
fix(stability): Improve hook state transitions (#4)
This ensures that the functions returned from any of our hooks are referentially stable and do not change e.g. when userMedia is obtained.
1 parent 833a795 commit 65a5e2a

File tree

5 files changed

+44
-61
lines changed

5 files changed

+44
-61
lines changed

packages/examples/src/WebcamPreview.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
export function WebcamPreview() {
99
const { request, isError, error, isLoading, isReady, media } =
1010
useMedia("user");
11+
1112
const {
1213
startRecording,
1314
stopRecording,

packages/react-user-media/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"name": "bengreenier",
1818
"url": "https://github.com/bengreenier"
1919
},
20-
"version": "0.1.0",
20+
"version": "0.1.1",
2121
"type": "module",
2222
"licenses": [
2323
{

packages/react-user-media/src/hooks/use-media-devices.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,6 @@ export function useMediaDevices(
112112

113113
const request = useCallback(
114114
function requestMediaDevices() {
115-
// don't request again if one is still pending
116-
if (isLoading) {
117-
return;
118-
}
119-
120115
if (!navigator.mediaDevices.enumerateDevices) {
121116
// see https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/enumerateDevices
122117
return setError(
@@ -141,7 +136,7 @@ export function useMediaDevices(
141136
},
142137
);
143138
},
144-
[isLoading, filter],
139+
[filter],
145140
);
146141

147142
useEffect(

packages/react-user-media/src/hooks/use-media-recorder.ts

Lines changed: 40 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
useCallback,
55
useSyncExternalStore,
66
useEffect,
7+
useRef,
78
} from "react";
89
import { ShallowShapeOf } from "../types";
910

@@ -160,9 +161,15 @@ export type RecorderState =
160161
* @returns See {@link RecorderState} for more information.
161162
*/
162163
export function useMediaRecorder(): RecorderState {
163-
const [recorder, setRecorder] = useState<MediaRecorder | undefined>(
164-
undefined,
165-
);
164+
// we need _both_ a referentially stable version of MediaRecorder and a mutable version
165+
// so that we can have stable start/stop functions, but also dynamic state updates
166+
const recorderRef = useRef<MediaRecorder | null>(null);
167+
const [isRecorderDirty, setIsRecorderDirty] = useState<boolean>(false);
168+
const recorder = useMemo(() => {
169+
if (isRecorderDirty) setIsRecorderDirty(false);
170+
171+
return recorderRef.current ?? undefined;
172+
}, [recorderRef, isRecorderDirty]);
166173

167174
const recorderState = useMediaRecorderState(recorder);
168175
const isRecording = useMemo(
@@ -182,60 +189,45 @@ export function useMediaRecorder(): RecorderState {
182189
const [startTime, setStartTime] = useState<DOMHighResTimeStamp | null>(null);
183190
const [endTime, setEndTime] = useState<DOMHighResTimeStamp | null>(null);
184191

185-
const startRecording = useCallback(
186-
function startRecordingMedia(
187-
media: MediaStream,
188-
options?: RecorderOptions,
189-
) {
190-
// don't allow multiple recordings at once
191-
if (isRecording) {
192-
return;
193-
}
194-
195-
const { timeslice, dataAvailableHandler, ...recorderOptions } = {
196-
timeslice: 30 * 1000 /* 30s */,
197-
dataAvailableHandler: (
198-
ev: BlobEvent,
199-
callback: (value: React.SetStateAction<Blob[]>) => void,
200-
) => {
201-
callback((current) => current.concat(ev.data));
202-
},
203-
...options,
204-
};
192+
const startRecording = useCallback(function startRecordingMedia(
193+
media: MediaStream,
194+
options?: RecorderOptions,
195+
) {
196+
const { timeslice, dataAvailableHandler, ...recorderOptions } = {
197+
timeslice: 30 * 1000 /* 30s */,
198+
dataAvailableHandler: (
199+
ev: BlobEvent,
200+
callback: (value: React.SetStateAction<Blob[]>) => void,
201+
) => {
202+
callback((current) => current.concat(ev.data));
203+
},
204+
...options,
205+
};
205206

206-
const recorder = new MediaRecorder(media, recorderOptions);
207+
const recorder = new MediaRecorder(media, recorderOptions);
207208

208-
recorder.addEventListener("dataavailable", function onDataAvailable(ev) {
209-
dataAvailableHandler(ev, setSegments);
210-
});
209+
recorder.addEventListener("dataavailable", function onDataAvailable(ev) {
210+
dataAvailableHandler(ev, setSegments);
211+
});
211212

212-
setSegments([]);
213+
setSegments([]);
213214

214-
const startTime = performance.now();
215-
recorder.start(timeslice);
215+
const startTime = performance.now();
216+
recorder.start(timeslice);
216217

217-
setStartTime(startTime);
218-
setRecorder(recorder);
219-
},
220-
[isRecording],
221-
);
218+
setStartTime(startTime);
222219

223-
const stopRecording = useCallback(
224-
function stopRecordingMedia() {
225-
// don't allow stopping of "nothing"
226-
if (!isRecording) {
227-
console.log("not recording");
228-
return;
229-
}
220+
recorderRef.current = recorder;
221+
setIsRecorderDirty(true);
222+
}, []);
230223

231-
const endTime = performance.now();
224+
const stopRecording = useCallback(function stopRecordingMedia() {
225+
const endTime = performance.now();
232226

233-
recorder?.stop();
227+
recorderRef.current?.stop();
234228

235-
setEndTime(endTime);
236-
},
237-
[isRecording, recorder],
238-
);
229+
setEndTime(endTime);
230+
}, []);
239231

240232
const state = {
241233
isError,

packages/react-user-media/src/hooks/use-media.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -192,11 +192,6 @@ export function useMedia<
192192
function requestUserMedia(
193193
...args: Parameters<inferMediaDef<TType>["requestType"]["request"]>
194194
) {
195-
// don't request again if one is still pending
196-
if (isLoading) {
197-
return;
198-
}
199-
200195
if (type === "user" && !navigator.mediaDevices.getUserMedia) {
201196
// see https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
202197
return setError(
@@ -258,7 +253,7 @@ export function useMedia<
258253
(type) satisfies never;
259254
}
260255
},
261-
[type, isLoading],
256+
[type],
262257
);
263258

264259
const state = {

0 commit comments

Comments
 (0)