A React Native Fabric signature pad on iOS sometimes appears already filled with a previous delivery's strokes on a new screen; why, and how do you fix it?
answer
- unmounted views go to a pool
- same component type reuses them
- state outside props survives
- prepareForRecycle requires super
- Android recycling is opt-in
basics
~20 siOS Fabric recycles component views: an unmounted RCTViewComponentView is pooled per component type and reused by a later mount. Strokes held in the view, not in props, survive. Reset them in prepareForRecycle, or opt the class out of recycling.
solid answer
~50 sWhen a Fabric component unmounts on iOS, React Native's `RCTComponentViewRegistry` calls `prepareForRecycle` on the view and puts it in a recycle pool for that component type (capped at 1024 views) instead of destroying it. The next `<SignaturePad>` to mount — on the next delivery's screen — may receive that same `RCTSignaturePad` instance. Props are re-applied from the new element, but the drawn strokes live in the view's canvas, which no prop describes, so they reappear. The fix is to override `- (void)prepareForRecycle`, call `super` (it is marked `NS_REQUIRES_SUPER`), and reset everything local: clear strokes, cancel timers, drop cached images. A class that cannot be reset safely can opt out with a class method `+ (BOOL)shouldBeRecycled` returning `NO`, and its views are then invalidated instead of pooled. On Android, view recycling is opt-in and off by default in 0.87, which is why the bug usually shows only on iOS.
code
objective-cpp · 19 lines@implementation RCTSignaturePad {
SignatureCanvas *_canvas;
}
// Called when the view moves to Fabric's recycle pool
- (void)prepareForRecycle
{
[super prepareForRecycle]; // RCTViewComponentView requires the super call
[_canvas clear]; // strokes are local state, not props
[_canvas resetUndoHistory];
}
// Alternative: opt this component out of recycling entirely
// + (BOOL)shouldBeRecycled
// {
// return NO;
// }
@endgo deeper
Remember that on iOS a native component's view can be reused after it unmounts, so anything it holds outside props must be reset.
Explain the recycle pool, the prepareForRecycle contract, why props are re-applied but local state is not, and the Android opt-in difference.
Diagnose state bleeding between screens, fix it with prepareForRecycle or shouldBeRecycled, and add a mount-unmount-remount test to the component's checks.
Treat recycling as a privacy and correctness risk for sensitive components, and set a rule that every stateful native view documents its reset path.
## The symptom and why it is serious A parcel-delivery app shows a proof-of-delivery screen per parcel. Occasionally, on iOS only, the signature box opens **already containing strokes** — the previous customer's signature. Beyond looking broken, that is a real data exposure: one customer's signature appears on another customer's delivery. The cause is not a caching bug in JavaScript; it is **view recycling** in React Native's iOS renderer. ## How Fabric recycles views on iOS Creating native views is expensive, so the iOS side of Fabric keeps unmounted component views for reuse: 1. When a component unmounts, `RCTComponentViewRegistry` checks whether the view may be recycled and whether the pool for that **component type** is full (the cap is `RCTComponentViewRegistryRecyclePoolMaxSize`, 1024). 2. If it may, the registry calls **`prepareForRecycle`** on the view and pushes it into the pool; otherwise it calls `invalidate` and lets the view go. 3. The next time a component of the same type mounts — anywhere in the app — the registry pops a pooled view instead of allocating a new one, then applies the new element's props, layout and event emitter. The protocol documentation for `prepareForRecycle` is explicit: it is called right after the view moves to the recycle pool, and the receiver **must reset any local state and release non-reusable resources**. ## Why props don't save you Everything React Native knows about the pad — `strokeWidth`, `strokeColor`, `disabled` — is re-applied from the new element, so those come out right. The strokes are different: they are **local state inside the native canvas**, created by touches, not described by any prop. React Native has no way to know they exist, so unless your class clears them, a recycled `RCTSignaturePad` carries them into its next life. | State | Held in | Reset on reuse by | |---|---|---| | `strokeWidth`, `strokeColor`, `disabled` | Props | React Native, through `updateProps:oldProps:` | | Layout frame | Layout metrics | React Native | | Drawn strokes, undo stack | Your canvas | **You**, in `prepareForRecycle` | | Timers, observers, cached export image | Your view | **You**, in `prepareForRecycle` | ## The fix - **Override `prepareForRecycle`.** It is declared `NS_REQUIRES_SUPER` on `RCTViewComponentView`, so call `[super prepareForRecycle]`, then clear the canvas, the undo history, any pending export work and any cached image. - **Or opt out.** If the view holds state that cannot be reset reliably, give the class a `+ (BOOL)shouldBeRecycled` method returning `NO`. React Native checks for it when it registers the component class; such views are invalidated instead of pooled, trading a little mount cost for safety. - **Cancel asynchronous work from the previous life.** A signature export still encoding when the view is pooled finishes later and, if its completion reads the view's current event emitter, reports through whichever element owns the view by then. Cancel it, or tag it with a generation counter that `prepareForRecycle` increments and ignore stale completions. - **Don't treat the first `updateProps:oldProps:` as "fresh view".** On a recycled view, that call follows a previous life; initialise per-use state in `prepareForRecycle` rather than relying on `init`. ## What about Android? Android view managers support recycling too — `setupViewRecycling()`, `prepareToRecycleView` and `recycleView` on `ViewManager` — but it is **opt-in**: a manager must call `setupViewRecycling()`, and in React Native 0.87 the `enableViewRecycling` feature flag defaults to `false`. That asymmetry is why the pad works on Android and misbehaves on iOS. On Android, release per-view resources in `onDropViewInstance`, and if you ever enable recycling, reset state in `prepareToRecycleView`. ## Checklist for any stateful native component 1. List every piece of state the native view holds that is not a prop. 2. Reset each one in `prepareForRecycle` on iOS, after calling `super`. 3. Decide deliberately whether the class should opt out with `shouldBeRecycled`. 4. Test the path: mount, draw, unmount, mount another instance, and confirm it starts blank.
- Why does the bug appear on iOS but not on Android?Recycling is on by default for iOS component views, capped per component type, while Android recycling is opt-in. A manager must call `setupViewRecycling()`, and the `enableViewRecycling` feature flag defaults to `false` in 0.87. So on Android each mount usually gets a fresh view from `createViewInstance`, and leftover strokes cannot leak.
- When would you opt out with shouldBeRecycled instead of resetting in prepareForRecycle?When the view's state cannot be reset reliably or cheaply: an embedded third-party view with hidden internal state, or a sensitive surface where a missed field would leak data. Returning `NO` makes React Native invalidate the view instead of pooling it. You pay a new allocation on every mount in exchange for a guaranteed clean view.
Fabric's recycle pool works like a courier depot reusing clipboards. A returned clipboard goes back on the shelf and the next courier takes whichever one is on top. The depot swaps the delivery sheet (the props), but if nobody wipes the scribbles in the margin (local view state), the next customer sees the previous customer's signature.
saying these in an interview costs you the question
- A native view is always destroyed when its React element unmounts
- React Native resets all native state on reuse because it reapplies props
- prepareForRecycle can skip calling super
- The bug must be in JavaScript state since native views are never shared across screens
- Android and iOS recycle Fabric views the same way by default