<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Pragathi Jayaram</title>
    <description>The latest articles on DEV Community by Pragathi Jayaram (@pragathijayaram).</description>
    <link>https://dev.to/pragathijayaram</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4064360%2F590e266a-c0c8-4d48-9336-1150919d463e.png</url>
      <title>DEV Community: Pragathi Jayaram</title>
      <link>https://dev.to/pragathijayaram</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pragathijayaram"/>
    <language>en</language>
    <item>
      <title>The bug that survived being recycled: automaticallyAdjustKeyboardInsets and Fabric view recycling on iOS</title>
      <dc:creator>Pragathi Jayaram</dc:creator>
      <pubDate>Wed, 05 Aug 2026 14:08:17 +0000</pubDate>
      <link>https://dev.to/pragathijayaram/the-bug-that-survived-being-recycled-automaticallyadjustkeyboardinsets-and-fabric-view-recycling-5e1e</link>
      <guid>https://dev.to/pragathijayaram/the-bug-that-survived-being-recycled-automaticallyadjustkeyboardinsets-and-fabric-view-recycling-5e1e</guid>
      <description>&lt;p&gt;After logging a meal in &lt;a href="https://pragathijayaram.com/portfolio/" rel="noopener noreferrer"&gt;Anovi&lt;/a&gt;, the nutrition app I build, the next screen opened normally. The content was there. Then the first scroll ran past the end of it, into empty space that should not have existed, and stopped.&lt;/p&gt;

&lt;p&gt;Dragging in that space did nothing. Not a slow scroll, not a bounce. Nothing moved, because there was nothing under my finger. The content was somewhere above me and there was no way to reach it. The only way out was to kill the app.&lt;/p&gt;

&lt;p&gt;It was not every screen and not every time, which is what kept it alive through earlier rounds of testing. It produced no crash, no error, and no failed request.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;In short.&lt;/strong&gt; On iOS with the New Architecture, a scroll view that opted into keyboard inset handling can leave that behaviour switched on when React Native recycles it. The next screen to inherit that view gets a phantom inset it never asked for. Filed as &lt;a href="https://github.com/facebook/react-native/issues/57755" rel="noopener noreferrer"&gt;react-native#57755&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;If you just want to check and fix your own app, that is &lt;a href="https://pragathijayaram.com/articles/react-native-keyboard-insets-leaking/" rel="noopener noreferrer"&gt;a separate, shorter piece&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The evidence that was missing
&lt;/h2&gt;

&lt;p&gt;Every request in the sequence returned what it should, in the time it should. Nothing failed, nothing timed out, nothing appeared in the logs at the moment things went wrong. The app had not crashed and was not in an error state.&lt;/p&gt;

&lt;p&gt;Healthy logs are usually read as the absence of a finding. Here they were the finding, because they eliminated the entire category I had been searching in. That is what promoted the next detail from background noise to the only lead.&lt;/p&gt;

&lt;p&gt;The next detail was the keyboard. On the broken screens it appeared, briefly, and went away again. On a screen with no text input on it and nothing focused.&lt;/p&gt;

&lt;p&gt;A keyboard reacting where there is nothing to type into is not a rendering bug. Something was still listening.&lt;/p&gt;

&lt;h2&gt;
  
  
  The screen that broke was not the screen at fault
&lt;/h2&gt;

&lt;p&gt;React Native's new architecture recycles native views. Rather than destroying a component's native view and building a fresh one, it resets the existing view and hands it to the next component that needs the same kind. On iOS, that reset happens in a method called &lt;code&gt;prepareForRecycle&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Recycling is only safe if the reset is complete. Anything left behind belongs to whoever inherits the view next.&lt;/p&gt;

&lt;p&gt;A scroll view can opt into having iOS adjust its insets when the keyboard moves, with a prop called &lt;code&gt;automaticallyAdjustKeyboardInsets&lt;/code&gt;. Underneath, that sets a flag on the native view and gates a keyboard observer that lives for as long as the view does. &lt;code&gt;prepareForRecycle&lt;/code&gt; does not clear that flag.&lt;/p&gt;

&lt;p&gt;So the broken screen was innocent. It had no text input, no keyboard props, and nothing to do with any of this. It had simply been handed a view that came out of the pool still holding someone else's settings.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where the keyboard came from
&lt;/h3&gt;

&lt;p&gt;The view came from the meal logging modal, which does have a scroll view with the prop set and a search field that focuses itself.&lt;/p&gt;

&lt;p&gt;The interesting part is what happened when I closed it. Tapping Done dismissed the keyboard, reset the modal's internal flow back to its search step, and hid the modal, all in the same render batch. Resetting the flow remounted the search field, its &lt;code&gt;autoFocus&lt;/code&gt; fired, and &lt;strong&gt;the app opened a keyboard inside a modal that was already closing.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is the flicker. Not a leftover from typing, but a keyboard my own code summoned into a dying screen. Its scroll view then went into the recycle pool while that keyboard was still up.&lt;/p&gt;

&lt;p&gt;None of that is a bug. A keyboard being up while a screen goes away is ordinary, and the framework is meant to survive it. What it did instead was hand the next screen a view that was still holding the last one's settings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building something that fails on demand
&lt;/h2&gt;

&lt;p&gt;An explanation that only reproduces inside a nutrition app is not much use to anyone, and it is not something a framework maintainer can act on. So the next step was the smallest thing that still fails: a modal with the prop and a focused input that kills itself mid-animation, then a plain scroll view that mounts straight afterwards and reports its own geometry.&lt;/p&gt;

&lt;h3&gt;
  
  
  The detector that cried wolf
&lt;/h3&gt;

&lt;p&gt;Getting the measurement right took longer than finding the bug.&lt;/p&gt;

&lt;p&gt;The first version reported failures that were not failures. A scroll view settling after a bounce moves on its own, and sampling at the wrong moment records that jitter as displacement. Several iterations went into separating "this view is still settling" from "this view has been moved by something else".&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A detector that reports a bug which is not there is worse than no detector, because it makes the real signal unfalsifiable.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Everything downstream of it, including the controls, would have been noise interpreted as evidence.&lt;/p&gt;

&lt;p&gt;Once it was honest, the numbers were unambiguous. A scroll view that never asked for keyboard insets, with the keyboard hidden, reporting a bottom inset of 217.3 points. Its content was 840 points tall in a 234 point window, so the furthest it should ever scroll is 606. It went to 823.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8y3ucqizqpgebpwt90w8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8y3ucqizqpgebpwt90w8.png" alt="The reproducer on React Native 0.86.2 showing a victim scroll view reporting a bottom inset of 217.3 with the keyboard hidden, content 840 and viewport 234, alongside counters for cycles, rogue keyboard events and corrupt readings." width="800" height="1739"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Physical iPhone, iOS 26.5.2, Release build, React Native 0.86.2, New Architecture. The victim sets no keyboard props of any kind.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The controls are the argument
&lt;/h2&gt;

&lt;p&gt;A reproducer that fails is a claim. A reproducer that fails, and stops failing when you remove exactly one ingredient, is evidence.&lt;/p&gt;

&lt;p&gt;Two controls, each from a freshly killed process. Turn off the inset prop: nothing. Dismiss the keyboard and let it settle before the modal closes: nothing. Each ingredient is necessary and neither is sufficient. The failure needs a view configured for keyboard insets to be recycled while its observer is still live.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measuring it instead of reasoning about it
&lt;/h2&gt;

&lt;p&gt;Everything so far was inference, from reading source and watching behaviour. The mechanism deserved a measurement, so I instrumented the framework itself: every keyboard delivery logged with the view's address and whether it was on screen or in the recycle pool.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rec-in  view=0x108118c00 ivar=1          entering prepareForRecycle
rec-out view=0x108118c00 ivar=1          leaving it, flag still set
kbd     view=0x108118c00 ivar=1 POOLED
write   view=0x108118c00 bottom=217.3 POOLED    ‚Üê inset written while pooled
props   view=0x108118c00 old=1 new=0 ivar=0     ‚Üê next screen mounts, flag cleared
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The write lands &lt;strong&gt;while the view is sitting in the pool&lt;/strong&gt;, 105ms before anything mounts on it. When a screen does mount, the flag is cleared immediately, so the screen you can see is never written to at all.&lt;/p&gt;

&lt;p&gt;That ruled out the explanation I had been carrying. An earlier test, where a poisoned view's scroll position moved after I focused an unrelated text field, had looked like proof that the recycled view was still listening while mounted. It was not. The keyboard changed the layout, the window grew from 234 points to 299, the furthest legal scroll position dropped by 65, and iOS pulled the offset back to the new limit. Arithmetic, not a live listener.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is actually happening
&lt;/h2&gt;

&lt;p&gt;The pooled view keeps receiving keyboard notifications and keeps writing insets onto itself. It is not stale; it is current. It tracks the keyboard continuously, taking a real value when a keyboard rises and zero when it falls.&lt;/p&gt;

&lt;p&gt;Whatever value it happens to be holding at the moment someone inherits it is what that screen is stuck with, because the inset is only reassigned when a component's props change it, and a component that never set the prop has nothing to reassign.&lt;/p&gt;

&lt;p&gt;Which means the whole thing turns on timing. Sweeping the delay before the modal tears itself down, ten cycles each from a fresh process:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;modal torn down after&lt;/th&gt;
&lt;th&gt;corrupt readings&lt;/th&gt;
&lt;th&gt;phantom inset&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;~120ms&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;td&gt;217.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;~220ms&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;~320ms&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;~520ms&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;keyboard dismissed first&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Only the earliest teardown reproduces. Wait longer and the keyboard's own disappearance reaches the pooled view first, zeroes the inset, and the pool quietly cleans itself. The bug needs a screen to mount during the narrow window while a keyboard is up.&lt;/p&gt;

&lt;p&gt;That is also why it was intermittent in production, and why it survived earlier testing rounds. Most of the time nothing mounts inside that window.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why it could not be scrolled back
&lt;/h3&gt;

&lt;p&gt;The phantom inset extends the scrollable range past the end of the content, and that extra region has no view underneath it. So it takes no touches. A drag there produces no scroll events at all.&lt;/p&gt;

&lt;p&gt;In the reproducer a sliver of the last row stays visible, so you can grab that and pull yourself back. In the app the scroll views were full screen. Once the content had left the viewport there was nothing to grab, and no gesture in the app could recover it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A screen recording of the reproducer on a physical device is on the &lt;a href="https://pragathijayaram.com/articles/react-native-scrollview-recycling-bug/" rel="noopener noreferrer"&gt;original article&lt;/a&gt;: several poison cycles, a scroll up into the space that should not exist, then taps on an unrelated input.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the difference between a layout glitch and a screen that is finished until you kill the process.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened after filing
&lt;/h2&gt;

&lt;p&gt;I reproduced it unchanged on React Native 0.86.2, the latest release at the time, and filed it as &lt;a href="https://github.com/facebook/react-native/issues/57755" rel="noopener noreferrer"&gt;react-native#57755&lt;/a&gt; with a &lt;a href="https://github.com/PragathiJ/rn-aaki-recycle-repro" rel="noopener noreferrer"&gt;public reproducer&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Two pull requests followed, from people I have never met. The second carries a regression test covering the whole lifecycle, and a Meta engineer imported it for internal review the same morning it appeared.&lt;/p&gt;

&lt;p&gt;Its two lines are small: clear the flag when the view is recycled, and stop guarding the assignment behind a props comparison so a recycled view can be re-armed properly. I applied them to the reproducer and ran it on the phone that found the bug in the first place. Baseline immediately before: 36 corrupt readings in 10 cycles. With the patch: 49 cycles, zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would take from this
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The healthy logs were the finding.&lt;/strong&gt; They eliminated a whole category, which is what turned a flicker into the only lead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Controls before claims.&lt;/strong&gt; Any bug can be explained by a plausible story. The question is which single ingredient you can remove to make it stop, and whether you have actually tried removing it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A detector that lies is worse than no detector.&lt;/strong&gt; Most of the reproducer work went into eliminating false positives, not into finding the failure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Instrument the explanation, not just the bug.&lt;/strong&gt; A story that fits every observation is not the same as a correct one. Mine was in the right place and the wrong moment, and only logging the framework showed the difference.&lt;/p&gt;




&lt;p&gt;If you think your own app has this, the checks and the three ways to fix it are in &lt;a href="https://pragathijayaram.com/articles/react-native-keyboard-insets-leaking/" rel="noopener noreferrer"&gt;a shorter companion piece&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;The reproducer is at &lt;a href="https://github.com/PragathiJ/rn-aaki-recycle-repro" rel="noopener noreferrer"&gt;github.com/PragathiJ/rn-aaki-recycle-repro&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;I write about production engineering and applied AI at &lt;a href="https://pragathijayaram.com/articles/" rel="noopener noreferrer"&gt;pragathijayaram.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>ios</category>
      <category>debugging</category>
      <category>webdev</category>
    </item>
    <item>
      <title>automaticallyAdjustKeyboardInsets is leaking into your other screens: how to check and fix it</title>
      <dc:creator>Pragathi Jayaram</dc:creator>
      <pubDate>Wed, 05 Aug 2026 13:53:30 +0000</pubDate>
      <link>https://dev.to/pragathijayaram/automaticallyadjustkeyboardinsets-is-leaking-into-your-other-screens-how-to-check-and-fix-it-2pm7</link>
      <guid>https://dev.to/pragathijayaram/automaticallyadjustkeyboardinsets-is-leaking-into-your-other-screens-how-to-check-and-fix-it-2pm7</guid>
      <description>&lt;p&gt;A screen opens normally, then scrolls past the end of its content into empty&lt;br&gt;
space, and dragging in that space does nothing at all. Restarting the app is the&lt;br&gt;
only way out.&lt;/p&gt;

&lt;p&gt;If that sounds familiar, this is a React Native framework defect, not your code.&lt;br&gt;
This page is the operational version: how to confirm it, how to find every source&lt;br&gt;
in your project, and what to do. The investigation behind it, including why it&lt;br&gt;
took a while to understand, &lt;a href="https://pragathijayaram.com/articles/react-native-scrollview-recycling-bug/" rel="noopener noreferrer"&gt;is here&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Scope.&lt;/strong&gt; iOS only, New Architecture only. The code involved is the Fabric iOS&lt;br&gt;
scroll view, so the old architecture and Android take different paths.&lt;br&gt;
Reproduced on React Native 0.81.4 and 0.86.2. Filed as&lt;br&gt;
&lt;a href="https://github.com/facebook/react-native/issues/57755" rel="noopener noreferrer"&gt;react-native#57755&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  What is going wrong
&lt;/h2&gt;

&lt;p&gt;A scroll view that sets &lt;code&gt;automaticallyAdjustKeyboardInsets&lt;/code&gt; keeps that behaviour&lt;br&gt;
switched on when React Native recycles its native view. While the view sits in the&lt;br&gt;
recycle pool it carries on receiving keyboard notifications and writing insets&lt;br&gt;
onto itself. The next component to be handed that view inherits whatever value was&lt;br&gt;
there, and nothing puts it back, because a component that never set the prop has&lt;br&gt;
no inset of its own to assign.&lt;/p&gt;

&lt;p&gt;The result is a scroll view with extra scrollable space below its content. That&lt;br&gt;
space has no view underneath it, so it takes no touches, which is why you cannot&lt;br&gt;
drag your way back out of it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Confirm it
&lt;/h2&gt;

&lt;p&gt;The first sign is a keyboard appearing on a screen with nothing focused. That is&lt;br&gt;
the tell, not the bug.&lt;/p&gt;

&lt;p&gt;The test is to read the inset on a scroll view that should not have one. Put this&lt;br&gt;
on a screen that sets no insets of its own, scroll it with the keyboard hidden,&lt;br&gt;
and watch the console:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reported&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ScrollView&lt;/span&gt;
  &lt;span class="na"&gt;scrollEventThrottle&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;onScroll&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// contentInset is iOS-only; on Android nativeEvent has no such field.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bottom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nativeEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentInset&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;bottom&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bottom&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;reported&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;reported&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;phantom bottom inset&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;ScrollView&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details decide whether this test is worth anything. The scroll view needs&lt;br&gt;
content taller than itself, or it never scrolls, &lt;code&gt;onScroll&lt;/code&gt; never fires, and you&lt;br&gt;
will conclude you are clean without having tested anything. And the reading has to&lt;br&gt;
be gated, because at a 16ms throttle an ungated &lt;code&gt;console.warn&lt;/code&gt; buries your log.&lt;/p&gt;

&lt;p&gt;A scroll view that never opted in should report zero. Anything else means it is&lt;br&gt;
sitting on a view that came out of the pool still armed.&lt;/p&gt;
&lt;h2&gt;
  
  
  Find every source
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"automaticallyAdjustKeyboardInsets"&lt;/span&gt; src/
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"contentInset"&lt;/span&gt; src/   &lt;span class="c"&gt;# also catches contentInsetAdjustmentBehavior&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The second search matters more than it looks. Any prop that writes native insets&lt;br&gt;
belongs to the native view, and therefore to the recycling pool, so a static&lt;br&gt;
&lt;code&gt;contentInset&lt;/code&gt; combined with an automatic &lt;code&gt;contentInsetAdjustmentBehavior&lt;/code&gt; is the&lt;br&gt;
same class of risk even though it has nothing to do with keyboards. In my own&lt;br&gt;
sweep that turned up a grid component the first search had missed.&lt;/p&gt;

&lt;p&gt;Then run both again against &lt;code&gt;node_modules&lt;/code&gt;. The pool is per process, not per&lt;br&gt;
module, so a scroll view inside a dependency arms it exactly as well as one of&lt;br&gt;
yours. You can clean your own code completely and still be handed a poisoned view by a library you did not write.&lt;/p&gt;
&lt;h2&gt;
  
  
  Fix it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Option 1, remove the prop.&lt;/strong&gt; Handle keyboard layout explicitly instead. This is&lt;br&gt;
what I shipped.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before: the flag does the layout, and the native view keeps listening&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Modal&lt;/span&gt; &lt;span class="na"&gt;visible&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;visible&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ScrollView&lt;/span&gt; &lt;span class="na"&gt;automaticallyAdjustKeyboardInsets&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TextInput&lt;/span&gt; &lt;span class="na"&gt;autoFocus&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;ScrollView&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Modal&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// After: explicit layout, nothing left armed on the native view&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Modal&lt;/span&gt; &lt;span class="na"&gt;visible&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;visible&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;KeyboardAvoidingView&lt;/span&gt; &lt;span class="na"&gt;behavior&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"padding"&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ScrollView&lt;/span&gt; &lt;span class="na"&gt;contentContainerStyle&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;paddingBottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TextInput&lt;/span&gt; &lt;span class="na"&gt;autoFocus&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;visible&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;ScrollView&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;KeyboardAvoidingView&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Modal&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;autoFocus={visible}&lt;/code&gt; change matters more than it looks. Focusing on mount is&lt;br&gt;
what starts a keyboard animation during modal presentation, which is exactly the&lt;br&gt;
window the defect needs. Watch for the same shape anywhere a modal resets its&lt;br&gt;
internal state as it closes: that can remount a focused field and open a keyboard&lt;br&gt;
inside a screen that is already going away.&lt;/p&gt;

&lt;p&gt;One caveat: &lt;code&gt;KeyboardAvoidingView&lt;/code&gt; with &lt;code&gt;behavior="padding"&lt;/code&gt; inside a &lt;code&gt;Modal&lt;/code&gt;&lt;br&gt;
behaves differently depending on presentation style and often wants a&lt;br&gt;
&lt;code&gt;keyboardVerticalOffset&lt;/code&gt;. Treat it as a shape to adapt, not a drop-in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 2, keep the prop but dismiss the keyboard first.&lt;/strong&gt; Dismiss it and let it&lt;br&gt;
settle before the screen unmounts. This came back at zero in the reproducer. It is&lt;br&gt;
narrower than option 1, because it depends on every exit path from that screen&lt;br&gt;
doing the same thing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 3, patch the framework locally.&lt;/strong&gt; Until the upstream fix ships you can&lt;br&gt;
carry it as a &lt;code&gt;patch-package&lt;/code&gt; patch. In &lt;code&gt;RCTScrollViewComponentView.mm&lt;/code&gt;, clear the&lt;br&gt;
flag on recycle and assign it on every update instead of only when it changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight objective_c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// prepareForRecycle: disarm before the view goes back in the pool&lt;/span&gt;
&lt;span class="n"&gt;_automaticallyAdjustKeyboardInsets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;NO&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// updateProps: drop the old-vs-new condition around the assignment&lt;/span&gt;
&lt;span class="n"&gt;_automaticallyAdjustKeyboardInsets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;newScrollViewProps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;automaticallyAdjustKeyboardInsets&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both lines are needed. The first stops the leak. The second matters because&lt;br&gt;
&lt;code&gt;prepareForRecycle&lt;/code&gt; does not reset the stored props, so once the flag is cleared a&lt;br&gt;
view being reused by another component that &lt;em&gt;also&lt;/em&gt; wants keyboard insets would&lt;br&gt;
skip the assignment and silently lose the behaviour.&lt;/p&gt;

&lt;p&gt;This is the substance of &lt;a href="https://github.com/facebook/react-native/pull/57811" rel="noopener noreferrer"&gt;react-native#57811&lt;/a&gt;, which a Meta engineer has imported for internal review. I verified those changes against the reproducer on a physical device: 49 cycles, zero failures, where the unpatched build produced 36 failures in 10 cycles.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you are on 0.86.2 and patching the framework&lt;/strong&gt;, note that React core ships&lt;br&gt;
prebuilt, so edits to &lt;code&gt;RCTScrollViewComponentView.mm&lt;/code&gt; in &lt;code&gt;node_modules&lt;/code&gt; never&lt;br&gt;
reach the binary. You need &lt;code&gt;RCT_USE_PREBUILT_RNCORE=0&lt;/code&gt; and a fresh &lt;code&gt;pod install&lt;/code&gt;,&lt;br&gt;
or the patch silently does nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does not work
&lt;/h2&gt;

&lt;p&gt;Scrolling the screen back to the top when it opens. I shipped that early and it&lt;br&gt;
does clear the wrong opening position, which is enough to make it look like a&lt;br&gt;
cure. It does not remove the phantom inset, so the dead space is still waiting at&lt;br&gt;
the bottom.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to tell you have fixed it
&lt;/h2&gt;

&lt;p&gt;Run the confirmation check again after the flow that used to break things. Zero&lt;br&gt;
inset, and a scroll to the bottom that stops at the end of your content with&lt;br&gt;
nothing beneath it.&lt;/p&gt;

&lt;p&gt;The reproducer is at&lt;br&gt;
&lt;a href="https://github.com/PragathiJ/rn-aaki-recycle-repro" rel="noopener noreferrer"&gt;github.com/PragathiJ/rn-aaki-recycle-repro&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I write about production engineering and applied AI at&lt;br&gt;
&lt;a href="https://pragathijayaram.com/articles/" rel="noopener noreferrer"&gt;pragathijayaram.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>ios</category>
      <category>mobile</category>
      <category>debugging</category>
    </item>
  </channel>
</rss>
