From 10d0472a28fc95d921e0b4e791f9f1c8f38a87dc Mon Sep 17 00:00:00 2001 From: Eric Xu Date: Sat, 6 Jun 2026 02:56:35 +0800 Subject: [PATCH] fix: don't mutate component props (crashes under React's frozen props) The `Camera` component mutates its own `props` to convert optional int/float view props to sentinels for codegen: props.zoom = props.zoom ?? -1; props.maxZoom = props.maxZoom ?? -1; // ... React freezes `element.props` in development, so this throws as soon as the module runs in JavaScript strict mode: Render Error: Cannot add new property 'zoom' ES modules are always strict, and bundlers increasingly emit per-module `"use strict"` (e.g. Metro 0.84 injects it for `sourceType: "module"`), so this crashes Camera in any dev build that freezes props (React 18 and 19 both do). It was masked before only where the module happened to run sloppy (older Metro / `@react-native/babel-preset` `strictMode: false`), where the frozen write is a silent no-op, and in release builds (React doesn't freeze there). Fix: build a new object with the defaults and spread it into NativeCamera, instead of mutating the (frozen) `props`. No behavior change otherwise. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Camera.android.tsx | 30 ++++++++++++++++-------------- src/Camera.ios.tsx | 37 +++++++++++++++++++++++-------------- 2 files changed, 39 insertions(+), 28 deletions(-) diff --git a/src/Camera.android.tsx b/src/Camera.android.tsx index 0a47aff6d..d5e8b7e81 100644 --- a/src/Camera.android.tsx +++ b/src/Camera.android.tsx @@ -8,16 +8,6 @@ import NativeCameraKitModule from './specs/NativeCameraKitModule'; const Camera = React.forwardRef((props, ref) => { const nativeRef = React.useRef(null); - // RN doesn't support optional view props yet (sigh) - // so we have to use -1 to indicate 'undefined' - // All int/float/double props from src/specs/CameraNativeComponent.ts need be mentioned here - props.zoom = props.zoom ?? -1; - props.maxZoom = props.maxZoom ?? -1; - props.scanThrottleDelay = props.scanThrottleDelay ?? -1; - props.faceDetectionThrottleMs = props.faceDetectionThrottleMs ?? -1; - - props.allowedBarcodeTypes = props.allowedBarcodeTypes ?? supportedCodeFormats; - React.useImperativeHandle(ref, () => ({ capture: async (options = {}) => { return await NativeCameraKitModule.capture(options, findNodeHandle(nativeRef.current) ?? undefined); @@ -30,10 +20,22 @@ const Camera = React.forwardRef((props, ref) => { }, })); - const transformedProps: CameraProps = { ...props }; - transformedProps.ratioOverlayColor = processColor(props.ratioOverlayColor) as any; - transformedProps.frameColor = processColor(props.frameColor) as any; - transformedProps.laserColor = processColor(props.laserColor) as any; + // RN can't express optional int/float view props yet, so we default + // undefined -> -1 (and other sentinels). Build a NEW object instead of + // mutating `props`: React freezes element props in dev, so writing to them + // throws "Cannot add new property" once the module runs in strict mode + // (ES modules are always strict). + const transformedProps: CameraProps = { + ...props, + zoom: props.zoom ?? -1, + maxZoom: props.maxZoom ?? -1, + scanThrottleDelay: props.scanThrottleDelay ?? -1, + faceDetectionThrottleMs: props.faceDetectionThrottleMs ?? -1, + allowedBarcodeTypes: props.allowedBarcodeTypes ?? supportedCodeFormats, + ratioOverlayColor: processColor(props.ratioOverlayColor) as any, + frameColor: processColor(props.frameColor) as any, + laserColor: processColor(props.laserColor) as any, + }; // @ts-expect-error props for codegen differ a bit from the user-facing ones return ; diff --git a/src/Camera.ios.tsx b/src/Camera.ios.tsx index 63d3c87b6..8c7ac2562 100644 --- a/src/Camera.ios.tsx +++ b/src/Camera.ios.tsx @@ -8,19 +8,22 @@ import NativeCameraKitModule from './specs/NativeCameraKitModule'; const Camera = React.forwardRef((props, ref) => { const nativeRef = React.useRef(null); - // RN doesn't support optional view props yet (sigh) - // so we have to use -1 to indicate 'undefined' - // All int/float/double props from src/specs/CameraNativeComponent.ts need be mentioned here - props.zoom = props.zoom ?? -1; - props.maxZoom = props.maxZoom ?? -1; - props.scanThrottleDelay = props.scanThrottleDelay ?? -1; - props.faceDetectionThrottleMs = props.faceDetectionThrottleMs ?? -1; - props.iOsDeferredStart = props.iOsDeferredStart ?? true; - - props.allowedBarcodeTypes = props.allowedBarcodeTypes ?? supportedCodeFormats; - - props.resetFocusTimeout = props.resetFocusTimeout ?? 0; - props.resetFocusWhenMotionDetected = props.resetFocusWhenMotionDetected ?? true; + // RN can't express optional int/float view props yet, so we default + // undefined -> -1 (and other sentinels). Build a NEW object instead of + // mutating `props`: React freezes element props in dev, so writing to them + // throws "Cannot add new property" once the module runs in strict mode + // (ES modules are always strict). + const transformedProps: CameraProps = { + ...props, + zoom: props.zoom ?? -1, + maxZoom: props.maxZoom ?? -1, + scanThrottleDelay: props.scanThrottleDelay ?? -1, + faceDetectionThrottleMs: props.faceDetectionThrottleMs ?? -1, + iOsDeferredStart: props.iOsDeferredStart ?? true, + allowedBarcodeTypes: props.allowedBarcodeTypes ?? supportedCodeFormats, + resetFocusTimeout: props.resetFocusTimeout ?? 0, + resetFocusWhenMotionDetected: props.resetFocusWhenMotionDetected ?? true, + }; React.useImperativeHandle(ref, () => ({ capture: async () => { @@ -35,7 +38,13 @@ const Camera = React.forwardRef((props, ref) => { })); // @ts-expect-error props for codegen differ a bit from the user-facing ones - return ; + return ( + + ); }); export default Camera;