Magic Modallatest

One API for web, iOS, and Android

Modals you can await

Mount one MagicModalPortal at the root of your app. Call show() from any async flow and await the typed result on web, iOS, and Android.

add react-native-magic-modal
rating-flow.tsx
01const rating = await magicModal
  .show<RatingAnswer>(RatingModal);02if (rating.reason !==
  MagicModalHideReason.INTENTIONAL_HIDE) {
  return { interruptedBy: rating.reason };
}03await magicModal.show(
  rating.data.score < 4
    ? FeedbackModal : StoreReviewModal
);04await magicModal
  .show(ThanksModal);05return rating.data;
promise pending
MagicModalPortalSTACK: 1 ENTRY
9:41
Moments
01RatingModal

Rate the app

How would you rate the app from zero to five?

Drag down or tap outside to close. Scores 0-3 ask for feedback; scores 4-5 offer a store review.
Why it exists
WHY IT EXISTS

Two callers can open a modal at the same time

The first Magic Modal flow collected feedback after someone liked a post. Several views could start it, await a score, and open the matching follow-up from one shared function.

CALLER OWNED4 pieces of visibility state

Every caller that can start the flow repeats the modal tree and its cleanup.

PORTAL OWNED4 callers share one function

The flow lives outside the UI. Each caller awaits the same sequence.

IDLE0 modal bodies mounted

The portal stays empty until show() runs.

OPENOne ID and promise per entry

Concurrent prompts keep their own place in the stack.

CODE4 callers share startRatingFlow()

The shared flow owns the modal components and visibility state.

See the portal tests
LIVE PACKAGE

Test two calls in one stack

Start the rating flow and stack a notification over it. Close the top entry and the first promise is still waiting underneath.

PLATFORM SUPPORTWeb / iOS / Android
0open modals
ONE PORTAL AT THE APP ROOT

One portal owns the stack

Start the rating flow or stack a notification above it. The upload example demonstrates the advanced update() API. Each demo mounts through the same MagicModalPortal.

CALLERrating-flow.ts
const result = await magicModal
  .show(RatingPrompt)
ACTIVE STACKTop entry stays visible
0

The stack is empty.Modal content mounts after show() runs.

PROMISE RESULTSEach promise resolves separately

No calls yet.Resolved values appear here.

COMMON FLOWS

Await a result and keep the flow in one place

The first two examples compose show(), useMagicModal().hide(), and typed results. The upload example uses update() for externally controlled progress.
Read the useMagicModal reference

delete-post.tsx
type Confirmation = { confirmed: boolean };

function DeletePostModal() {
  const { hide } = useMagicModal<Confirmation>();

  return (
    <Button onPress={() => hide({ confirmed: true })}>
      Delete post
    </Button>
  );
}

const result = await magicModal
  .show<Confirmation>(() => (
    <DeletePostModal />
  ));

if (
  result.reason === INTENTIONAL_HIDE &&
  result.data.confirmed
) {
  await deletePost(postID);
}
MagicModalPortal1 entry

Delete this post?

This action cannot be undone.

RESULTPromise pending
APP ROOT

Mount one portal at the app root

Modal calls can come from anywhere in the app. Each show() gets an ID and promise, so one flow cannot overwrite the modal that was already open.

See the flow pattern
CALLERSpost.tsxcomment.tsxproduct.tsxnotifications.ts
FLOWstartRatingFlow()rating(); branch(); thanks();
APP ROOTMagicModalPortal
RETURN VALUE

What magicModal.show() returns

The returned handle is itself the promise, so await it directly, or target this stack entry by ID. The advanced update() API replaces progress controlled outside the modal.

Promise<HideReturn<T>> & {
  modalID: string;
  update: (next: React.FC) => void;
  hide: (data?: T) => void;
}
AVAILABLE IMMEDIATELY
await handle01
Resolves when this entry closes.
modalID02
Targets this entry from another call site.
updateADVANCED
Remounts the component while its ID, stack position, and pending promise stay put.

USE SPARINGLYupdate() remounts the component and resets local React state. Keep ordinary UI changes inside the modal.

CLOSE RESULTS

What HideReturn<T> records

A submitted answer, backdrop tap, swipe, Android back press, and hideAll() resolve differently. Only hide(data) returns a payload.

Read HideReturn<T>
Test a close action
Choose an answer

Answer here, tap the backdrop, swipe down, or use a close action above.

HideReturn<Answer>promise pending
const result = await handle;
// waiting for a close
Press one action to resolve the promise
GET STARTED

Add MagicModalPortal to your app root

The setup guide mounts the app-root portal and shows how to await a typed HideReturn<T>.

Set up the portal
add react-native-magic-modal