RORK LABJP
PLAY — Google Play's target API level 36 requirement took effect yesterday, August 31. From today, new apps and updates must target Android 16VISIBILITY — Apps still on API 35 stay listed but disappear for users on newer Android versions. No error is raised; new installs simply fade, which makes the change easy to missEXTENSION — If you missed the deadline, an extension through November 1, 2026 can be requested in Play Console — best filed alongside a concrete migration planAPPLE — On the Apple side, the event lands September 9 and iOS 27 is reported to ship September 14. Testing generated apps on iOS 27 hardware before release week is time well spentEXPO — Expo released expo-paste-input on August 28, a native module that brings image, GIF, and sticker paste to React Native TextInputEAS — EAS Observe reached general availability on August 20, putting crash and performance monitoring on the same EAS platform as builds and updatesPLAY — Google Play's target API level 36 requirement took effect yesterday, August 31. From today, new apps and updates must target Android 16VISIBILITY — Apps still on API 35 stay listed but disappear for users on newer Android versions. No error is raised; new installs simply fade, which makes the change easy to missEXTENSION — If you missed the deadline, an extension through November 1, 2026 can be requested in Play Console — best filed alongside a concrete migration planAPPLE — On the Apple side, the event lands September 9 and iOS 27 is reported to ship September 14. Testing generated apps on iOS 27 hardware before release week is time well spentEXPO — Expo released expo-paste-input on August 28, a native module that brings image, GIF, and sticker paste to React Native TextInputEAS — EAS Observe reached general availability on August 20, putting crash and performance monitoring on the same EAS platform as builds and updates
Articles/Dev Tools
Dev Tools/2026-05-15Intermediate

My Paywall and Review Prompt Showed at the Same Time — Fixing It with a ModalGate Pattern

How to prevent multiple modals from appearing simultaneously in Rork apps using a ModalGate pattern, with practical code examples from a real app update.

Rork548React Native234Modal2UI Design3Bug Fix

While updating Beautiful HD Wallpapers (Android v2.1.0), I ran into a bug where the PaywallDialog and ReviewInductionDialog were both appearing at the same time. Users couldn't interact with either — one was blocking the other.

After 12 years and over 50 million downloads across my personal app portfolio, I've come to recognize this as one of the trickiest categories of bugs to catch. React Native apps (which Rork generates) make it surprisingly easy to trigger multiple modals simultaneously, especially as features grow.

Why Modals Overlap in the First Place

In a typical Rork-generated React Native app, each modal has its own local state flag:

const [showPaywall, setShowPaywall] = useState(false);
const [showReviewPrompt, setShowReviewPrompt] = useState(false);
const [showRewardedAd, setShowRewardedAd] = useState(false);

When these are controlled independently, two events firing close together — say, a 3-second launch timer for your paywall and a 100th-launch check for a review prompt — can both resolve to true in the same render cycle.

A common but flawed fix is nesting the conditions:

// ❌ Nesting makes priority implicit and fragile
useEffect(() => {
  if (shouldShowPaywall) {
    setShowPaywall(true);
    if (shouldShowReview) {
      // Whether this line is reached depends on async timing
      setShowReviewPrompt(true);
    }
  } else if (shouldShowReview) {
    setShowReviewPrompt(true);
  }
}, [shouldShowPaywall, shouldShowReview]);

The problem: when shouldShowPaywall and shouldShowReview change asynchronously, which branch runs first depends on execution timing. This produces bugs that only appear in production and are nearly impossible to reproduce in tests.

The ModalGate Pattern — Central Ownership of Display Rights

The fix is to give one object — a ModalGate — exclusive authority over which modal is visible. The rule is simple: only one modal can hold the display right at a time.

// utils/modalGate.ts
type ModalType = 'paywall' | 'review' | 'rewardedAd' | null;
 
let currentModal: ModalType = null;
const listeners: Set<(modal: ModalType) => void> = new Set();
 
export const ModalGate = {
  request(modal: Exclude<ModalType, null>): boolean {
    if (currentModal !== null) {
      console.log(`[ModalGate] ${modal} rejected — ${currentModal} is active`);
      return false;
    }
    currentModal = modal;
    listeners.forEach(fn => fn(currentModal));
    return true;
  },
 
  release(modal: Exclude<ModalType, null>): void {
    if (currentModal === modal) {
      currentModal = null;
      listeners.forEach(fn => fn(null));
    }
  },
 
  subscribe(fn: (modal: ModalType) => void): () => void {
    listeners.add(fn);
    return () => listeners.delete(fn);
  },
};

Using it in a component looks like this:

// PaywallDialog.tsx
import { ModalGate } from '../utils/modalGate';
 
const PaywallDialog: React.FC = () => {
  const [visible, setVisible] = useState(false);
 
  const show = useCallback(() => {
    const granted = ModalGate.request('paywall');
    if (granted) {
      setVisible(true);
    }
  }, []);
 
  const hide = useCallback(() => {
    setVisible(false);
    ModalGate.release('paywall');
  }, []);
 
  return (
    <Modal
      visible={visible}
      onRequestClose={hide}
      onDismiss={hide}
    >
      {/* paywall content */}
    </Modal>
  );
};

Apply the same pattern to ReviewInductionDialog. Whichever dialog calls request() first wins the display right; any later request is automatically rejected.

Adding ModalGate to a Rork Project

In a Rork-generated project, the fastest way to add this is:

First, create utils/modalGate.ts with the code above. You can also prompt Rork directly: "Add a utility to prevent multiple dialogs from showing at the same time — only one modal should be open at once." Rork will generate a base implementation you can refine.

Then update each dialog component to route its show/hide calls through ModalGate. The key advantage is that this doesn't require restructuring your existing components — just intercept the show trigger and add release() to the dismiss handlers.

Three Real Collisions This Prevented

Here are the actual overlap scenarios ModalGate resolved in the Beautiful HD Wallpapers update:

Launch timer paywall vs. 100th-launch review prompt

A 3-second post-launch paywall timer and a launch-count check both resolved on the app's 100th launch. Without ModalGate, both dialogs appeared. With it, the paywall got the display right and the review prompt was silently deferred.

Rewarded ad completion callback vs. async state update

After a user watched a rewarded ad, the app updated the ad-free state and then queued a paywall prompt a few seconds later. Because the async completion callback doesn't fire at a consistent time, this collision was impossible to reproduce reliably in testing — but happened to real users.

Deep link navigation vs. normal launch flow

When users tapped certain push notifications, the deep link handling and the normal launch flow's modal logic both ran. The collision rate depended on whether the app was backgrounded or cold-started, making it another test-invisible but production-visible bug.

Guard Against Forgetting to Release

The main footgun with ModalGate is forgetting to call release(). If you skip it, no modal will ever show again for the rest of that session.

The safest approach: always add release() to both onRequestClose (Android back button) and onDismiss (iOS swipe-dismiss):

<Modal
  visible={visible}
  onRequestClose={hide}    // Android back button
  onDismiss={hide}         // iOS swipe-to-dismiss
>

My grandfather was a miyadaiku — a craftsman who built Shinto shrines. He used to say the joint is always where a structure fails, never the flat surface. I find that holds true for UI logic too: the dismiss path is more likely to be buggy than the show path.


If your Rork app has three or more modal triggers, it's worth adding utils/modalGate.ts now rather than waiting for overlap to happen in production. The file is small, the integration is minimal, and it eliminates an entire category of hard-to-reproduce bugs. Since introducing this pattern, I haven't received a single crash report from modal overlap in my apps.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $15 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Dev Tools2026-04-26
Why Your Rork Modal Won't Close — 5 Common Pitfalls and How to Fix Them
The close button doesn't fire, tapping the backdrop does nothing, and buttons inside the modal feel dead. In Rork-generated React Native code, eight times out of ten the cause is one of these five patterns.
Dev Tools2026-08-22
Every bulk replace exited zero. The damage was in the lines I did not delete
Run a bulk replace over generated code and the breakage lands on the neighbouring lines, not the matched ones. Here is what broke in a live project, and a dependency-free guard that checks the invariants a replace must preserve.
Dev Tools2026-08-14
Find the native edits expo prebuild will erase before you upgrade to SDK 57
Expo SDK 57 makes expo prebuild clear and regenerate ios and android by default. Here is how to audit your hand edits first, move them into config plugins, and why 57.0.9 matters for Reanimated apps.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →