From d6527359d3c19c2ebe67fda3a56c614a73cf7842 Mon Sep 17 00:00:00 2001 From: ZayanKhan-12 Date: Sat, 19 Sep 2026 14:14:00 -0400 Subject: [PATCH] fix: stop mutating props in Camera render, restoring lost defaults MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #391 reported `forwardRef render functions do not support propTypes or defaultProps`, caused by `Camera.defaultProps = {...}`. The defaultProps block is gone, but what replaced it assigns straight onto `props`: props.zoom = props.zoom ?? -1; props.resetFocusWhenMotionDetected = props.resetFocusWhenMotionDetected ?? true; ... React 19 freezes `element.props` in its development build (the `Object.freeze(type.props)` in `react-jsx-runtime.development.js`), and `@react-native/babel-preset` emits no `"use strict"`, so every one of those assignments is silently discarded in a dev build and applied in a release one. Eight props on iOS and five on Android therefore never reach the native side in development. Some are harmless because the codegen spec repeats the default, but three are not: prop JS wants dev build actually gets resetFocusWhenMotionDetected true false (spec has no default) iOsDeferredStart true false (spec has no default) resetFocusTimeout 0 -1 (spec default is -1) `resetFocusWhenMotionDetected` is documented as defaulting to `true`, so debug builds have been getting the opposite of the documented behaviour. Build the defaults onto a derived object instead. Mutating props was already disallowed regardless of the freeze — React treats them as immutable and keeps the same object as `prevProps` for the next render. Compiled with the repo's babel config, direct `props.x =` assignments go from 13 (8 iOS + 5 Android) to 0. Fixes #391 --- src/Camera.android.tsx | 33 +++++++++++++++++++-------------- src/Camera.ios.tsx | 24 +++++++++++++++--------- 2 files changed, 34 insertions(+), 23 deletions(-) diff --git a/src/Camera.android.tsx b/src/Camera.android.tsx index 0a47aff6d..16d97e42d 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,25 @@ 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 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 + // + // These must land on a new object rather than on `props`: React freezes element.props in + // development, so assigning to `props` is silently dropped there while it works in release. + 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..b26bf9d2b 100644 --- a/src/Camera.ios.tsx +++ b/src/Camera.ios.tsx @@ -11,16 +11,22 @@ const Camera = React.forwardRef((props, ref) => { // 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; + // + // These must land on a new object rather than on `props`: React freezes element.props in + // development, so assigning to `props` is silently dropped there while it works in release. + const nativeProps: CameraProps = { + ...props, + zoom: props.zoom ?? -1, + maxZoom: props.maxZoom ?? -1, + scanThrottleDelay: props.scanThrottleDelay ?? -1, + faceDetectionThrottleMs: props.faceDetectionThrottleMs ?? -1, + iOsDeferredStart: props.iOsDeferredStart ?? true, - props.allowedBarcodeTypes = props.allowedBarcodeTypes ?? supportedCodeFormats; + allowedBarcodeTypes: props.allowedBarcodeTypes ?? supportedCodeFormats, - props.resetFocusTimeout = props.resetFocusTimeout ?? 0; - props.resetFocusWhenMotionDetected = props.resetFocusWhenMotionDetected ?? true; + resetFocusTimeout: props.resetFocusTimeout ?? 0, + resetFocusWhenMotionDetected: props.resetFocusWhenMotionDetected ?? true, + }; React.useImperativeHandle(ref, () => ({ capture: async () => { @@ -35,7 +41,7 @@ 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;