RORK LABJP
ENGINE — Rork Max is powered by Claude Code and Claude Opus 4.6, generating native Swift apps directlyCORE ML — Rork Max reaches on-device Core ML inference alongside HealthKit, HomeKit, NFC, and App ClipsSEED — Rork raised a $15M seed led by Left Lane Capital in April 2026, joined by Peak XV and a16z SpeedrunM&A — Rork acquired the app builder Paperline and says it will stay acquisitive to bring in engineering talentMARKET — Gartner expects 75% of new applications to be built with low-code or no-code tools by the end of 2026GROWTH — The no-code AI platform market is projected to grow from $4.9B in 2024 to $24.8B by 2029ENGINE — Rork Max is powered by Claude Code and Claude Opus 4.6, generating native Swift apps directlyCORE ML — Rork Max reaches on-device Core ML inference alongside HealthKit, HomeKit, NFC, and App ClipsSEED — Rork raised a $15M seed led by Left Lane Capital in April 2026, joined by Peak XV and a16z SpeedrunM&A — Rork acquired the app builder Paperline and says it will stay acquisitive to bring in engineering talentMARKET — Gartner expects 75% of new applications to be built with low-code or no-code tools by the end of 2026GROWTH — The no-code AI platform market is projected to grow from $4.9B in 2024 to $24.8B by 2029
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.

Rork498React Native201Modal2UI 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-10
Adding React Compiler to Expo Let Me Delete 41 Hand-Written memo Calls
I enabled React Compiler on Rork-generated React Native screens and measured the rerender counts with Profiler. Here is how I decided which memo and useCallback calls were safe to delete, how to find the components the compiler bailed out on, and how to catch regressions in CI.
Dev Tools2026-07-07
The App Icon Badge Still Says 3 — Rebuilding Expo Badge Counts Around a Single Source of Truth
Why an Expo app's icon badge drifts out of sync with real unread counts and refuses to clear — and how to rebuild it around a single source of truth, with working recompute-and-sync code and the production pitfalls that bite you.
📚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 →