Skip to content

fix: stop mutating props in Camera render, restoring lost defaults - #820

Open
ZayanKhan-12 wants to merge 1 commit into
teslamotors:masterfrom
ZayanKhan-12:fix/391-no-props-mutation
Open

ZayanKhan-12 wants to merge 1 commit into
teslamotors:masterfrom
ZayanKhan-12:fix/391-no-props-mutation

Conversation

@ZayanKhan-12

Copy link
Copy Markdown

Fixes #391

Where #391 stands today

The literal warning in that issue — forwardRef render functions do not support propTypes or defaultProps — came from Camera.defaultProps = {...}, and that block is gone from src/. But the code that replaced it assigns straight onto props:

props.zoom = props.zoom ?? -1;
props.resetFocusWhenMotionDetected = props.resetFocusWhenMotionDetected ?? true;

That is still the same defaulting bug, just quieter — and it is worse than the warning was, because it changes behaviour instead of printing something.

Why the assignments do nothing

Two facts combine:

  1. React 19 freezes element.props in development. react-jsx-runtime.development.js:411 does Object.freeze && (Object.freeze(type.props), Object.freeze(type));, and that is the object handed to the render function.
  2. @react-native/babel-preset emits no "use strict". Compiling src/Camera.ios.tsx with the repo's own babel.config.js and testing for a strict-mode directive returns false.

Assigning to a frozen object in sloppy mode fails silently. So in a debug build every default below is discarded; in a release build (react.production.js does not freeze) they all apply. Under "use strict" the same line would instead throw TypeError: Cannot add property zoom, object is not extensible — verified directly.

What actually breaks

Eight props on iOS and five on Android never reach native in development. Most are harmless because CameraNativeComponent.ts repeats the same default via WithDefault. Three are not:

prop JS intends dev build actually gets why
resetFocusWhenMotionDetected true false no WithDefault in the spec; Swift property also defaults false
iOsDeferredStart true false no WithDefault in the spec
resetFocusTimeout 0 -1 spec default is WithDefault<Int32, -1>

resetFocusWhenMotionDetected is documented in the README as defaulting to true, so debug builds have been getting the documented default inverted.

The fix

Build the defaults onto a derived object rather than onto props. On Android this also folds the processColor conversions into the same literal, which were already being applied to a copy.

Mutating props was not safe even without the freeze: React treats props as immutable and retains the same object as prevProps for the next render, so in-place edits corrupt its comparison baseline.

The JSX is otherwise unchanged — style still sits before the spread, so a caller-supplied style keeps overriding the minWidth/minHeight fallback exactly as before.

Verification

Rendering both shapes through react-test-renderer in CommonJS (sloppy mode, matching Metro's output):

CURRENT (mutate frozen props):
  LOST zoom                           = undefined   (want -1)
  LOST iOsDeferredStart               = undefined   (want true)
  LOST allowedBarcodeTypes            = undefined   (want ["qr","ean-13"])
  LOST resetFocusTimeout              = undefined   (want 0)
  LOST resetFocusWhenMotionDetected   = undefined   (want true)

FIXED (derive a new object):
  ok   zoom                           = -1
  ok   iOsDeferredStart               = true
  ok   allowedBarcodeTypes            = ["qr","ean-13"]
  ok   resetFocusTimeout              = 0
  ok   resetFocusWhenMotionDetected   = true

defaults lost before fix: 5/5   after fix: 0/5

Compiling both files with the repo's babel config, direct props.x = assignments go from 13 (8 iOS + 5 Android) to 0.

yarn build (tsc) passes — which is the check that bites here, since yarn lint runs eslint with no file arguments and so lints nothing.

Note for maintainers

If you would rather these defaults lived in one place, the spec is the natural home: adding WithDefault entries for resetFocusWhenMotionDetected and iOsDeferredStart would let the JS-side defaulting shrink. Happy to do that instead or as a follow-up — this PR deliberately keeps the existing intent and only stops it being silently dropped.

teslamotors#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 teslamotors#391
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Warning: forwardRef render functions do not support propTypes or defaultProps. Did you accidentally pass a React component?

2 participants