Migrating from v1
Most code needs no changes. The API is the same apart from additions. What changed is behaviour: two fixes turn the camera off in cases where v1 left it on, which is breaking by definition even though it’s what you wanted.
npm install use-media-stream@latestCheck these
Section titled “Check these”stop() releases any stream, not just one from start()
Section titled “stop() releases any stream, not just one from start()”getMediaDevices() opens a stream to read device labels. In v1, stop() guarded on isStreaming
— which getMediaDevices() never set — so that stream stayed live with no way to release it.
Do something if: you worked around this with your own teardown. You can delete it.
Unmounting releases the stream
Section titled “Unmounting releases the stream”v1 had no cleanup at all. Navigating away left the camera on until the tab closed.
Do something if: you pass the MediaStream to something that outlives the component — a
recorder, a WebRTC peer connection, a global store. It will now be stopped on unmount. Keep the
hook mounted for as long as the stream is needed.
Arrays in constraints are replaced, not concatenated
Section titled “Arrays in constraints are replaced, not concatenated”v1 used deepmerge, which concatenated arrays:
// base: { deviceId: { exact: ['a'] } }// override: { deviceId: { exact: ['b'] } }
// v1 → { exact: ['a', 'b'] }// v2 → { exact: ['b'] }Objects still merge recursively, exactly as before.
Do something if: you relied on the concatenation. Almost nobody did — it produced a constraint matching either device, which is rarely what anyone means.
error is Error | null, was unknown
Section titled “error is Error | null, was unknown”// v1if (error instanceof Error) console.log(error.message);
// v2console.log(error?.message);Existing narrowing still compiles. Only code that passed error somewhere expecting unknown
needs a look.
Request states are a union, not string
Section titled “Request states are a union, not string”getStreamRequest and getMediaDevicesRequest are now
'IDLE' | 'PENDING' | 'FULFILLED' | 'REJECTED'. Exhaustive switch statements typecheck;
assigning an arbitrary string no longer does.
defaultMediaDeviceConstraints changed shape
Section titled “defaultMediaDeviceConstraints changed shape”audio is true rather than { deviceId: '' }, and video no longer carries deviceId: ''. An
empty non-exact deviceId matched nothing and was ignored.
Do something if: you read the exported constant. Behaviour is unchanged.
Node 18 or newer
Section titled “Node 18 or newer”engines.node moved from >=16 to >=18, relevant only if you render server-side. The React peer
range is unchanged at >= 16.
Things that got better on their own
Section titled “Things that got better on their own”deepmergeis gone. It was apeerDependency, so it sat in your top-level tree — and pnpm and yarn 1 never installed it at all. The package now has zero runtime dependencies.- Both entry points load.
require()andimportwere each broken from Node in different ways. Bundlers hid it. - Server rendering works. v1 crashed with
ReferenceError: navigator is not definedon Node 18 and 20. - Mute flags recover. Nothing listened for
unmute, so once a track went silent the flags stayed wrong forever. - Device arrays are stable. They were rebuilt every render, so
useEffect(..., [audioInputDevices])looped forever. start()reports unsupported browsers instead of throwing a rawTypeError.
New in v2
Section titled “New in v2”unmutelisteners, matching themuteones- Types are exported —
UseMediaStreamProps,UseMediaStreamReturn,RequestStateand the rest. v1 exported nothing but the hook. - The hook as a named export as well as the default
mediaDeviceConstraintsandresetStreamare optional, as they always were at runtime- Published with provenance
The full list is in the changelog.