fix: stop mutating props in Camera render, restoring lost defaults - #820
Open
ZayanKhan-12 wants to merge 1 commit into
Open
ZayanKhan-12 wants to merge 1 commit into
ZayanKhan-12 wants to merge 1 commit into
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #391
Where #391 stands today
The literal warning in that issue —
forwardRef render functions do not support propTypes or defaultProps— came fromCamera.defaultProps = {...}, and that block is gone fromsrc/. But the code that replaced it assigns straight ontoprops: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:
element.propsin development.react-jsx-runtime.development.js:411doesObject.freeze && (Object.freeze(type.props), Object.freeze(type));, and that is the object handed to the render function.@react-native/babel-presetemits no"use strict". Compilingsrc/Camera.ios.tsxwith the repo's ownbabel.config.jsand testing for a strict-mode directive returnsfalse.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.jsdoes not freeze) they all apply. Under"use strict"the same line would instead throwTypeError: 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.tsrepeats the same default viaWithDefault. Three are not:resetFocusWhenMotionDetectedtruefalseWithDefaultin the spec; Swift property also defaultsfalseiOsDeferredStarttruefalseWithDefaultin the specresetFocusTimeout0-1WithDefault<Int32, -1>resetFocusWhenMotionDetectedis documented in the README as defaulting totrue, 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 theprocessColorconversions 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
prevPropsfor the next render, so in-place edits corrupt its comparison baseline.The JSX is otherwise unchanged —
stylestill sits before the spread, so a caller-suppliedstylekeeps overriding theminWidth/minHeightfallback exactly as before.Verification
Rendering both shapes through
react-test-rendererin CommonJS (sloppy mode, matching Metro's output):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, sinceyarn lintrunseslintwith 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
WithDefaultentries forresetFocusWhenMotionDetectedandiOsDeferredStartwould 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.