First Look

Give CloudX the first chance to fill an ad placement, then fall back to your existing mediation setup

First Look gives CloudX the first chance to fill a placement on every ad opportunity, while your existing mediation setup remains the fallback path. Exactly one SDK owns the placement at any moment, and CloudX gets first look again on the next cycle. Start with one placement, verify load and show behavior, then expand to more placements.

The examples on this page use Google Ad Manager (GAM) as the fallback through react-native-google-mobile-ads. The same pattern applies to any other fallback mediator: load CloudX first, load the fallback only if CloudX does not fill, and keep the app flow moving when neither source is ready.

There are two shapes to the rule, and this page covers one hook for each:

  • Banner & MREC — an inline ad stays on screen and is never consumed, so the hook needs an explicit refresh cycle to give CloudX another first look. MREC follows this hook exactly.
  • Interstitial — a fullscreen ad is consumed by being shown, so the SDKs’ own “is an ad ready” answers go false on their own and the next load() starts at CloudX again. Rewarded follows this hook exactly.

A banner is not the interstitial with different method names. A fullscreen ad is consumed by being shown, so its readiness checks go false on their own and the next load() starts at CloudX again — the interstitial below rests on that. An inline ad is never consumed: left alone, the first fill owns the placement until the screen unmounts, so a single CloudX no-fill hands the slot to your fallback for the rest of the session.

The hook solves that with a refresh cycle. One cycle is one ad opportunity: CloudX first, GAM only if CloudX misses, winner swapped in. Swapping the winner in ends the cycle and starts the clock on the next one, which begins at CloudX again.

The examples use the component banner, CloudXBannerView, not the programmatic CloudXBannerAd. The component renders in your view tree, so it sits in your layout rather than as a native overlay above it — and the cycle needs that form, because mounting starts the load and unmounting destroys the ad.

With refresh disabled on both sides, each cycle works like this:

  1. Load a CloudX banner off-screen while the previous ad (if any) stays visible.
  2. On fill, swap it in: show the new banner and unmount the previous one, which destroys its native view.
  3. On CloudX no-fill, load the GAM banner for this cycle instead.
  4. If both no-fill, retry from CloudX with an exponential delay (1, 2, 4, 8, … seconds, capped at 64).
  5. Once the new ad is swapped in, wait 30 seconds, then start the next cycle at step 1 — CloudX gets first look again. A new attempt only starts while your app is in the foreground; a load already in flight finishes normally.

This lets you prepare the next fill in the background while the current banner is displaying, swap instantly when the refresh moment arrives, and benefit from CloudX optimistic loading.

Copy all four. Between them they are the whole flow.

They live in the CloudX React Native demo app, which is the version verified on device, so it is the one that gets fixed when something is wrong. That is why this page links them instead of pasting them: a copy here would drift, and it would be the stale one.

Why the slot is a separate file

The hook has no view. It tracks two attempts — the ad currently displayed and the ad currently loading off-screen — and the slot is what turns those into mounted components. Mounting is what triggers a load for both CloudXBannerView and GAM’s GAMBannerAd, so the slot’s structure is not cosmetic; it is how the cycle runs. Three rules live in it, and they are the ones an integration gets wrong:

  • Render the loading attempt hidden, not deferred. Nothing loads until it mounts, so a slot that waits for the refresh moment to mount the next ad shows an empty banner for the length of a load. The hidden slot is mounted the whole time and costs nothing until it fills.
  • Hide it with position: absolute and opacity: 0, plus pointerEvents="none" and the accessibility flags. Unmounting it would cancel the load; leaving it interactive would let a user tap an ad that is not on screen, which is an invalid click.
  • Key every callback to its attempt. The displayed ad and the loading ad share one set of handlers, and the displayed ad can emit its own events. Without the attempt key, a stale event from the ad already on screen promotes an unfilled hidden attempt and blanks the slot.

Take the slot as it is and you get all three. Which SDK wins a cycle, and when the next one starts, is the hook’s side and is commented in that file.

Wiring it up

Render <FirstLookBannerSlot /> wherever the banner belongs in your layout, and pass an optional observer to see what the cycle is doing. When the screen unmounts, both slots unmount with it, which destroys the native ads and clears the hook’s timers.

HomeScreen.tsx
import React, { useCallback, useMemo } from 'react';
import { View } from 'react-native';
import { FirstLookBannerSlot } from './firstlook/FirstLookBannerSlot';
import type { FirstLookBannerObserver } from './firstlook/useFirstLookBanner';

export function HomeScreen() {
  const report = useCallback((line: string) => console.log(line), []);

  /*
   * The observer is optional - the cycle runs whether or not you pass one.
   * The hook keeps it in a ref, so a fresh object on every render is harmless:
   * an observer identity change never restarts a running cycle.
   */
  const observer: FirstLookBannerObserver = useMemo(
    () => ({
      onAdLoaded: source => report(`Banner loaded (${source})`),
      onAdLoadFailed: (source, error) =>
        report(`Banner load failed (${source}): ${error}`),
      onAdClicked: source => report(`Banner clicked (${source})`),
    }),
    [report],
  );

  return (
    <View>
      <FirstLookBannerSlot observer={observer} />
    </View>
  );
}

Render one slot per placement. Navigators keep screens mounted, so two components on the same ad unit run two independent cycles and both receive that ad unit’s events — one instance’s failure arms the other’s fallback.

What the observer reports

Three callbacks, each carrying the source that served the ad.

CallbackMeaning
onAdLoaded(source)A source filled. This is also the swap: the ad goes on screen in the same handler. source is the answer to “is CloudX actually filling?”
onAdLoadFailed(source, error)Both sources missed and the opportunity is over. Only 'gam' is emitted today.
onAdClicked(source)The user tapped the ad. Reporting only.

The one to get right is onAdLoadFailed. It is not raised when CloudX alone misses, because that miss is what starts the GAM attempt — the cycle is still running, and treating it as terminal would double-book the opportunity while GAM is still loading.

Two silences to know about. A GAM banner click is never reported: react-native-google-mobile-ads wires no banner click event on iOS or Android, so onAdClicked covers CloudX banners only (the interstitial is unaffected and reports both sources). And the cycle pauses whenever your app leaves the foreground, with no callback for it. On iOS that includes the ATT prompt, which appears at launch just as the first cycle starts.

Interstitial

The simpler of the two, because showing consumes the ad: no slot has to be rendered and no cycle has to be driven. Rewarded is this hook with the rewarded calls substituted, plus the reward callback.

Call load() when the screen is ready to prepare the placement. At the placement moment, call show(). If it returns false, neither CloudX nor GAM had a ready ad, so continue the app flow without showing an ad.

The hook never reloads for you. onAdClosed, onAdLoadFailed and onAdShowFailed each mean the opportunity is over, and each is a place load() belongs — without the reload on close, the placement is dead after the first impression.

GameScreen.tsx
const { isReady, load, show } = useFirstLookInterstitial(
  CLOUDX_INTERSTITIAL_AD_UNIT_ID,
  GAM_INTERSTITIAL_AD_UNIT_ID,
  {
    onAdLoaded: source => console.log(`Interstitial ready (${source})`),
    // Showing consumes the ad, so without this the slot is dead after the
    // first impression.
    onAdClosed: () => load(),
    // No close follows a miss, so nothing else would refill. Space these with
    // an exponential delay rather than reloading immediately.
    onAdLoadFailed: (source, error) => scheduleRetry(),
    onAdShowFailed: (source, error) => scheduleRetry(),
  },
);

useEffect(() => {
  load();
}, [load]);

const continueToNextScreen = () => {
  if (!show()) {
    navigation.navigate('NextScreen');
  }
};

What the observer reports

Six callbacks, each carrying the source that served the ad.

CallbackMeaning
onAdLoaded(source)A source filled. source is the answer to “is CloudX actually filling?”
onAdLoadFailed(source, error)Both sources missed; the opportunity is over. Only 'gam' is emitted today.
onAdShown(source)The SDK confirmed the ad is on screen. Not inferred from show().
onAdShowFailed(source, error)A loaded ad could not be presented; the opportunity is over. Only 'gam' today — a CloudX display failure arrives on the load path instead, which is what triggers the fallback.
onAdClosed(source)The ad was dismissed. Call load() from here.
onAdClicked(source)The user tapped the ad. Reporting only; the placement is unaffected.

onAdLoadFailed follows the same terminal-only rule as the banner’s: the CloudX miss is what triggers the fallback, so it is not reported.

Error paths

  • CloudX load fails (no-fill or error): the hook loads the GAM interstitial for this opportunity. useCloudXInterstitial reports it by setting its error field, which is the hook’s only fallback trigger.
  • CloudX show fails: the ad is consumed and the same error field is set, so the hook loads GAM for this opportunity too — the fallback covers display failures, not only load failures. This includes an expired fill (ADAPTER_AD_EXPIRED): a CloudX interstitial held loaded but unshown for a long time can expire and fail at show, and the fallback picks it up.
  • A reload straight from the close: the SDK will not accept a load for a placement the instant it closes. The hidden event fires first, so a load() from onAdClosed is rejected with Cannot load while another ad is currently being displayed — indistinguishable from a no-fill, which would hand GAM an opportunity CloudX never got. The hook absorbs this: it waits out CLOSE_SETTLE_MS and retries the CloudX load rather than treating that rejection as a miss. Keep calling load() from onAdClosed; the timing is handled.
  • GAM load also fails: show() returns false and the app flow continues without an ad. onAdLoadFailed('gam', ...) is raised; space the retries with an exponential delay (1, 2, 4, 8, … seconds) rather than retrying immediately.
  • GAM never answers at all: after ATTEMPT_TIMEOUT_MS the hook raises onAdLoadFailed('gam', ...) so you are not left waiting. Read that message — it also tells you the GAM ad object cannot load again this session, because the plugin ignores a load while its own request is outstanding and only a close or an error clears that. Later opportunities still start at CloudX and serve normally; recovering the GAM leg means recreating the ad object.
  • GAM cannot present (on Android, no resumed activity): the promise rejects after show() has already returned true, so it is reported as onAdShowFailed. The fill is not lost — the next show() presents the ad still in hand.
  • After a successful show and close: call load() again when your app is ready to prepare the next opportunity — CloudX gets first look again.

On unmount, the CloudX hook releases its event subscriptions and destroys its interstitial instance, and the GAM event listener is unsubscribed by the effect cleanup.

State reference

GAM is reachable only through the CloudX error path, so exactly one source is ever loading, and the next opportunity always starts back at CloudX.

StateisReadyshow()load()
Idlefalsefalse — continue app flowstarts CloudX
CloudX loadingfalsefalse — continue app flowno-op
CloudX readytrueshows CloudXno-op
GAM loadingfalsefalse — continue app flowno-op
GAM readytrueshows GAMno-op
Presentingtruefalse — a show is already in flightno-op
Both failedfalsefalse — continue app flowrestarts at CloudX

Check your integration

The hooks decide the order. What they cannot decide is whether your app key, your ad unit ids and your dashboard configuration are right, and those live in your project. When one of them is wrong the symptom is silent: the fallback serves, ads appear, and everything looks healthy.

One check covers it. Log the source, which every callback already carries:

onAdLoaded: source => console.log(`First Look loaded: ${source}`);

Run with your real ad unit ids and look for cloudx on a fill. If you only ever see gam, CloudX is not filling at all — check the app key, the ad unit ids and the dashboard configuration before you look at the hook.

For a banner, watch for longer than one refresh delay: a fresh CloudX attempt should precede every GAM fill, not just the first. If CloudX is asked once and never again, something is ending the cycle — an SDK refresh timer that is still on, or a second component mounted on the same ad unit.

To exercise the fallback on purpose — to confirm your GAM unit is configured before you ship — point the CloudX ad unit id at a string that is not in your dashboard, so every CloudX load fails and the fallback has to serve. The sequence should read:

cloudx no-fill -> gam attempt -> gam fill -> (refresh delay) -> cloudx attempt

With a working CloudX id, the same watch confirms the negative case that matters: no load-failure line appears between the CloudX miss and Banner loaded (gam), because the CloudX miss is not terminal.

Common mistakes

These patterns apply to any fallback mediator, not only the one used in the examples above. Most of them end the same way: two sources loading for one opportunity.

  • Loading the fallback early “so one is always ready.” Every opportunity then produces two fills, and the discarded one is wasted; most mediation SDKs expire an interstitial that is held unshown — a GAM interstitial after about an hour, with no impression for it. The only trigger for a fallback load is a CloudX failure — load or show.

  • Leaving an old preloader running. If your app preloaded the fallback mediator at startup before you added CloudX, that code now runs in parallel with the hook even when the hook is correct. Give each ad unit exactly one owner; a fallback request at app start, before the first placement moment, means the legacy path is still live.

  • Falling back on a timer. A timeout that loads the fallback because CloudX is “slow” fires under normal auction latency and double-books the opportunity when CloudX then fills. Fall back only from a CloudX failure; if you need a deadline, cancel and replace the attempt like the banner hook’s 15-second timeout, never race two live loads.

  • Mounting one placement twice. Navigators keep screens mounted, so two slots on one ad unit run two cycles and double the request rate. Banner views do not cross-talk — each reports through its own props — they simply compete for the placement. Two interstitial hooks on one ad unit are worse: both subscribe to that ad unit’s events, so one instance’s miss arms the other’s fallback. Render one per placement.

  • Moving the request gates into useState. The gates — the fallback-requested flag and the timers — are refs because a guard must read and set in the same tick; state updates are batched, so two events can both pass the guard. Keep them as refs, and keep the interstitial hook’s ref mirror of the CloudX ready flags, which its event handlers read before the next render commits.

  • Reaching past the hook to show. The hook’s show() is safe to call at any time — with nothing loaded it just returns false. useCloudXInterstitial’s own show() is not: called with no ad loaded it sets error, and a CloudX error is what arms the fallback, so a speculative show during a CloudX load starts a parallel GAM load. Go through the hook.

  • Wrapping load() in your own retry logic. load() in the render body, an effect with unstable dependencies, or a generic retry helper wrapped around it turns one failing placement into a request storm. Load from a mount effect or a user-flow moment. The banner hook already backs off internally; for the interstitial the backoff is yours to write, as scheduleRetry above.