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.
Banner & MREC
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:
- Load a CloudX banner off-screen while the previous ad (if any) stays visible.
- On fill, swap it in: show the new banner and unmount the previous one, which destroys its native view.
- On CloudX no-fill, load the GAM banner for this cycle instead.
- If both no-fill, retry from CloudX with an exponential delay (1, 2, 4, 8, … seconds, capped at 64).
- 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.
useFirstLookBanner.ts
Which SDK gets the cycle, and when the next one starts. The whole cycle, commented at the lines that matter.
FirstLookBannerSlot.tsx
How that cycle is rendered: the visible slot and the hidden slot that preloads the next ad.
firstLookTiming.ts
The four numbers worth tuning — refresh delay, backoff ceiling, attempt timeout, close settle.
FirstLookSource.ts
The 'cloudx' / 'gam' union every callback reports.
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: absoluteandopacity: 0, pluspointerEvents="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.
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.
| Callback | Meaning |
|---|---|
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.
useFirstLookInterstitial.ts
The complete hook, commented at the lines that matter. Copy it as is.
firstLookTiming.ts
The attempt timeout and the post-close settle window this hook reads. Shared with the banner.
FirstLookSource.ts
The 'cloudx' / 'gam' union every callback reports. Shared with the banner.
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.
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.
| Callback | Meaning |
|---|---|
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.
useCloudXInterstitialreports it by setting itserrorfield, which is the hook’s only fallback trigger. - CloudX show fails: the ad is consumed and the same
errorfield 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()fromonAdClosedis rejected withCannot 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 outCLOSE_SETTLE_MSand retries the CloudX load rather than treating that rejection as a miss. Keep callingload()fromonAdClosed; the timing is handled. - GAM load also fails:
show()returnsfalseand 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_MSthe hook raisesonAdLoadFailed('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 returnedtrue, so it is reported asonAdShowFailed. The fill is not lost — the nextshow()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.
| State | isReady | show() | load() |
|---|---|---|---|
| Idle | false | false — continue app flow | starts CloudX |
| CloudX loading | false | false — continue app flow | no-op |
| CloudX ready | true | shows CloudX | no-op |
| GAM loading | false | false — continue app flow | no-op |
| GAM ready | true | shows GAM | no-op |
| Presenting | true | false — a show is already in flight | no-op |
| Both failed | false | false — continue app flow | restarts 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 returnsfalse.useCloudXInterstitial’s ownshow()is not: called with no ad loaded it setserror, 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, asscheduleRetryabove.