RORK LABJP
TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the roundTOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round
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.

Rork515React Native209Modal2UI 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 $10 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-07-15
The Comma That Fell to the Start of a Line: Rork, Quote Cards, and Japanese Line Breaking
React Native's Text component does not guarantee Japanese line-breaking rules. Here are the violation rates I measured, a WORD JOINER implementation that survives copy and VoiceOver, an onTextLayout audit harness, and how Rork Max (SwiftUI) differs.
Dev Tools2026-07-15
My Rork sleep timer faded out on time — but the sound didn't
Rork writes sleep-timer fades against the wall clock with setInterval. The numbers say 30 minutes, but the real audio fades early or cuts out abruptly. Here is how to drive the fade from the actual playback position, why a logarithmic curve sounds natural, and the honest limit of JS fades when the screen is locked.
📚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 →