API
Every method, event and type of @hotcodepush/react-native-code-push and the useUpdates hook, with a signature and an example each.
The React Native SDK exports one object, HotCodePush, with 16 methods and 5 events, and one hook, useUpdates(). Every method takes at most one options object and resolves a typed result or nothing. Nothing an app should handle is thrown.
import { HotCodePush, useUpdates } from '@hotcodepush/react-native-code-push';The package exports every type on this page, and the examples below assume this import. A result is a discriminated union on status, with a reason where the status needs one. An app switches on typed values and never parses a message. The same object and hook work on Expo through the Expo SDK, which re-exports them.
Methods
| Method | What it does | Resolves |
|---|---|---|
sync() | Runs one cycle under the strategies: check, download, apply, report. | SyncResult |
checkForUpdate() | Checks and downloads nothing. | CheckForUpdateResult |
downloadUpdate() | Checks again, downloads and applies per the apply strategies. | DownloadUpdateResult |
applyUpdate() | Applies the downloaded update now. | ApplyUpdateResult |
notifyReady() | Ends the readiness gate under readySignal: manual. | NotifyReadyResult |
rollbackUpdate() | Rolls the running release back now. | nothing |
clearUpdates() | Drops every downloaded update and reloads the embedded bundle. | nothing |
setRestartAllowed() | Holds or releases the restart the SDK performs on its own. | nothing |
getState() | Answers a snapshot of what the SDK knows. | GetStateResult |
getChannel() | Answers the channel in effect and where it came from. | GetChannelResult |
setChannel() | Switches the device to a channel by name or by id. | nothing |
getDevice() | Answers the facts of the device report. | GetDeviceResult |
setAttributes() | Merges the attributes that attribute conditions match. | nothing |
showDebugScreen() | Opens the native debug screen. | nothing |
addListener() | Registers a listener for one event. | a handle with remove() |
removeAllListeners() | Removes every listener. | nothing |
sync()
sync(options?: SyncOptions): Promise<SyncResult>One cycle under the strategies, the same the SDK runs on its own at start, on resume and at the interval. It fetches the channel's index, evaluates it on the device, downloads and verifies, applies and reports. Its options override any of the three strategies for this cycle, and a second call while a cycle runs joins the running one.
It resolves once the outcome is known, with one of the statuses:
up_to_date, oravailablewhen the download stayed manualdownloadedwith theapplyStrategyin force, orappliedwith the reload followingskippedorfailedwith a reason
A release already downloaded answers downloaded again, never up_to_date. A change made in the console reaches the next sync() within about a minute (How an update reaches a device).
const result = await HotCodePush.sync();
switch (result.status) {
case 'applied':
// the reload follows this result
break;
case 'downloaded':
console.log(`release #${result.release.number} applies ${result.applyStrategy}`);
break;
case 'available':
console.log(`release #${result.release.number} waits for downloadUpdate()`);
break;
case 'skipped':
case 'failed':
console.log(`${result.status}: ${result.reason}`);
break;
case 'up_to_date':
break;
}checkForUpdate()
checkForUpdate(): Promise<CheckForUpdateResult>sync() with downloadStrategy pinned to manual: it fetches and evaluates, downloads nothing and moves nothing. It resolves up_to_date, available with the release, its notes and the pack's size, skipped or failed. A release already downloaded answers available too. It counts as the device's check and is recorded as getState().lastCheck.
const result = await HotCodePush.checkForUpdate();
if (result.status === 'available') {
console.log(`release #${result.release.number}, ${result.downloadSizeBytes} bytes: ${result.notes ?? ''}`);
}downloadUpdate()
downloadUpdate(options?: DownloadUpdateOptions): Promise<DownloadUpdateResult>sync() with downloadStrategy pinned to auto: it checks the index again, downloads and verifies, and applies per applyStrategy or mandatoryApplyStrategy, which options overrides for this cycle. It resolves downloaded with the applyStrategy in force, applied, up_to_date, skipped or failed, never available. The cycle is recorded as getState().lastCheck and resets no interval.
const result = await HotCodePush.downloadUpdate({ applyStrategy: 'manual' });
if (result.status === 'downloaded') {
console.log(`release #${result.release.number} is downloaded; call applyUpdate() when ready`);
}applyUpdate()
applyUpdate(): Promise<ApplyUpdateResult>Applies the downloaded update now, whatever the strategy says, and reloads the app, at once or as soon as the app is up. It resolves applied with the release, or nothing_to_apply with the release that runs. A downloaded release that has left the index since is dropped first. Holding restarts with setRestartAllowed() never holds it.
A call still waiting when the run ends is not kept, so the app calls it again. It is how an app on applyStrategy: manual finishes an update, a mandatory one under mandatoryApplyStrategy: manual included. The pattern is in Choose an apply strategy.
const result = await HotCodePush.applyUpdate();
if (result.status === 'applied') {
console.log(`release #${result.release.number} loads now`);
}notifyReady()
notifyReady(): Promise<NotifyReadyResult>Ends the readiness gate when readySignal is manual, and is safe to call at any time on either setting. It also says the app is up in this run, so a reload that waited for that runs. The result says whether this start follows a rollback: isRolledBack with rollbackReason, and previousRelease, each told once. currentRelease is null on the embedded bundle.
const result = await HotCodePush.notifyReady();
if (result.isRolledBack) {
console.log(`rolled back from #${result.previousRelease?.number}: ${result.rollbackReason}`);
}rollbackUpdate()
rollbackUpdate(options?: RollbackUpdateOptions): Promise<void>Rolls the running release back now and reloads at once. It reverts to the last confirmed release or the embedded bundle and marks the bundle as failed on this device. The device reports app_requested with reason as the detail.
It does nothing on the embedded bundle, and reason is at most 256 Unicode code points without a control character. A rollback that reaches every device is Roll back a release.
await HotCodePush.rollbackUpdate({ reason: 'checkout failed to load' });clearUpdates()
clearUpdates(): Promise<void>Clears every downloaded update and the list of failed bundles, keeps the device id, channel choice and attributes, and reloads into the embedded bundle. Before the app is up in this run it waits, and setRestartAllowed({ isAllowed: false }) never holds it. It is the escape hatch a support guide can name.
await HotCodePush.clearUpdates();setRestartAllowed()
setRestartAllowed(options: SetRestartAllowedOptions): Promise<void>isAllowed: false holds the restart the SDK would perform on its own, at most one, until isAllowed: true runs it. That covers an immediate apply, a next_resume apply, a mandatory release and the reload after a readiness timeout. Until the held restart runs, getState().currentRelease still names what runs. An apply still held when the run ends takes effect at the next start.
A restart the app asks for itself, applyUpdate(), rollbackUpdate() or clearUpdates(), is never held, and every start begins allowed. It is how an app protects a checkout or an unsaved form without giving up automatic applies.
await HotCodePush.setRestartAllowed({ isAllowed: false });
// ... the flow the user must finish ...
await HotCodePush.setRestartAllowed({ isAllowed: true });getState()
getState(): Promise<GetStateResult>A snapshot of what the SDK knows, everything the debug screen shows:
currentReleaseis the running release,nullon the embedded bundle, and what an app attaches to its own telemetry.nextReleaseis downloaded and waiting for its apply moment.fallbackReleaseis the last confirmed release, the one a rollback returns to.embeddedBundleIdcomes from the resource file and isnullin a build whose step created no binary.lastCheckholds the last cycle's time, trigger and result.indexis the channel index the device holds, withsequenceits publish time in milliseconds, the debug screen's Published at, andfetchedAtwhen the device fetched it.failedBundleIdslists the bundles that failed on this device and are never tried again here.reportedAtis the server time of the month's first acknowledged report.
const state = await HotCodePush.getState();
console.log(state.currentRelease?.bundleVersion ?? 'embedded', state.lastCheck?.result.status);getChannel()
getChannel(): Promise<GetChannelResult>The channel in effect with its source, runtime after setChannel() or config from the build. The id is null while no id is known, on a build without a channel and for a runtime name no sync has resolved yet. The name is the runtime name, null for the build's channel, which the build carries by id alone.
const channel = await HotCodePush.getChannel();
console.log(channel.source, channel.id ?? 'unresolved', channel.name ?? '');setChannel()
setChannel(options: SetChannelOptions): Promise<void>Switches the device to a channel, by { name } or by { id }. A name works for a channel flagged discoverable with channel update --discoverable. An id works for a channel the app was handed at runtime: a preview from a QR code, or a tenant's channel from your backend.
Passing null clears the runtime choice and returns to the build's channel. A name is stored and resolved at the next sync, which answers channel_unknown while the channel is not discoverable. A channel flagged a moment ago reaches the device within about a minute.
A deleted or expired channel serves no index: every check fails with channel_unknown, the device keeps the bundle it runs and its channel, and the SDK never changes the channel on its own. A runtime choice is yours to clear with setChannel(null), as Switch channels by link shows.
The patterns around the call are Test with preview channels, Switch channels by link and Set up environments and tenants.
await HotCodePush.setChannel({ name: 'staging' });
const result = await HotCodePush.sync();
if (result.status === 'failed' && result.reason === 'channel_unknown') {
await HotCodePush.setChannel(null);
}getDevice()
getDevice(): Promise<GetDeviceResult>The facts of the device report and nothing more:
idis the anonymous per-install id.platform,osVersionandsdkVersionareiosorandroid, the OS version and the SDK version.binaryVersionandbinaryBuildare the store build's version and build number.fingerprintis the native fingerprint the build carries,nullin a build that carries none.channelreads asgetChannel()answers it.attributesholds every attribute the app set.
What leaves the device is on Privacy and device data.
const device = await HotCodePush.getDevice();
console.log(device.id, device.platform, device.binaryVersion, device.fingerprint);setAttributes()
setAttributes(options: SetAttributesOptions): Promise<void>Merges string attributes for attribute conditions, userId, tenant, plan, cohort. A null value removes a key. They are evaluated on the device and reported when they change, which is what Target devices with attributes builds on. A key is letters, digits, _, - and ., at most 64 characters, and a value is at most 256 Unicode code points without a control character.
Attributes are labels, never personal data: an opaque user id yes, an email never.
await HotCodePush.setAttributes({ userId: '42', plan: 'pro', cohort: null });showDebugScreen()
showDebugScreen(): Promise<void>Opens the SDK's native debug screen over the app, which shows what the device knows. Check now runs a cycle with the trigger manual, a sync() on iOS and a checkForUpdate() on Android. Share opens the system share sheet with the screen as text. While a runtime choice is in effect, the screen offers Return to configured channel, which calls setChannel(null).
Put the call behind a settings entry or a shake handler. An app may ship it, since nothing on it is secret and the log never leaves the device on its own.
await HotCodePush.showDebugScreen();| Section | Line | Means |
|---|---|---|
| Device | Device id | Shows the anonymous id the SDK generated at first start, the one device probe takes. |
| Device | Binary, OS, SDK | Shows the build's version and build number, the OS version and the SDK version. |
| Device | Fingerprint | Shows the native fingerprint the build carries, or none. |
| Device | Debug build, Attributes | Shows yes or no, then every key=value the app set, or none. |
| Channel | Channel id, Name, Source | Shows the channel in effect, unresolved for a runtime name no sync has resolved yet, then its name, then config from the build or runtime after setChannel(). |
| Releases | Running, Downloaded, Fallback | Shows the release that runs, the one waiting for its apply moment and the one a rollback returns to. |
| Releases | Embedded bundle | Shows the id of the bundle the build ships, or not registered when the build step created no binary. |
| Releases | Failed bundles | Lists the bundles that failed on this device and are never tried again here. |
| Last check | When, Trigger, Result | Shows the last cycle's time, what started it, and its status with reason and condition as one code, skipped device_incompatible fingerprint. |
| Last check | Message | Shows the sentence behind the code. |
| Index | Published at | Shows when the channel index the device holds was published. |
| Index | Fetched at | Shows when the device fetched it, or never. |
| Index | Reported at | Shows the server time of the month's first acknowledged report, the moment that counts the device as active this month. |
| Configuration | Built at | Shows the build time. No release created before it is applied on this binary. |
| Configuration | Strategies, Ready signal, Debug builds, Public keys | Shows the values the build carries, the public keys as a count. |
| Log | every line | Shows this session's log, newest last: the time, a code and a sentence. It holds 200 entries and starts empty at every launch. |
addListener()
addListener<EventName extends HotCodePushEventName>(
eventName: EventName,
listener: (event: HotCodePushEvents[EventName]) => void,
): Promise<HotCodePushListenerHandle>Registers a listener for one of the five events and resolves a HotCodePushListenerHandle, { remove: () => Promise<void> }. The updateRolledBack event belongs to the start that follows a rollback, before any listener exists. The native side keeps it, and every listener added during that start receives it once.
const handle = await HotCodePush.addListener('updateDownloaded', event => {
console.log(`release #${event.release.number} applies ${event.applyStrategy}`);
});
// later, when the screen goes
await handle.remove();removeAllListeners()
removeAllListeners(): Promise<void>Removes every listener added through addListener(), the updateRolledBack ones included.
await HotCodePush.removeAllListeners();Events
No event fires for a cycle's start or end, for a check that found nothing or for a skipped release. The last cycle's outcome is getState().lastCheck. No event fires before a reload.
| Event | Payload | When |
|---|---|---|
updateAvailable | UpdateAvailableEvent | A check found a release the device qualifies for. |
updateDownloaded | UpdateDownloadedEvent | The download completed and the update waits for its apply moment. |
updateFailed | UpdateFailedEvent | A check or a download failed. |
downloadProgress | DownloadProgressEvent | A pack is downloading. |
updateRolledBack | UpdateRolledBackEvent | The app starts after a rollback. |
updateAvailable
{ downloadSizeBytes: number | null; notes: string | null; release: Release; trigger: SyncTrigger }A check found a release the device qualifies for. downloadSizeBytes is the pack's size, 0 for a release of the embedded bundle, which downloads nothing. The trigger says what started the cycle: start, resume, interval or manual. It is the event a manual download waits on, and the one behind a "new version" badge.
await HotCodePush.addListener('updateAvailable', event => {
console.log(`release #${event.release.number}, ${event.downloadSizeBytes} bytes, found on ${event.trigger}`);
});updateDownloaded
{ applyStrategy: 'next_start' | 'next_resume' | 'manual'; release: Release; trigger: SyncTrigger }The download completed and the update waits for its apply. applyStrategy names the moment, or manual for the app's own applyUpdate(). It never fires for an immediate apply, whose download goes straight to the reload. It is the event behind the restart dialog for a mandatory release under mandatoryApplyStrategy: manual, with release.isMandatory saying so.
await HotCodePush.addListener('updateDownloaded', event => {
if (event.applyStrategy === 'manual' && event.release.isMandatory) {
void HotCodePush.applyUpdate();
}
});updateFailed
{ message: string; reason: FailedReason; release: Release | null; trigger: SyncTrigger }A check or a download failed, with the reason from the catalog and a message for the log. A build without a channel starts no automatic check, so there the event follows a call the app made.
await HotCodePush.addListener('updateFailed', event => {
console.log(`${event.reason}: ${event.message}`);
});downloadProgress
{ downloadSizeBytes: number; progress: number; progressBytes: number; releaseId: string }While a pack downloads: progressBytes of downloadSizeBytes have arrived, and progress is their share from 0 to 1.
await HotCodePush.addListener('downloadProgress', event => {
console.log(`${Math.round(event.progress * 100)} % of ${event.downloadSizeBytes} bytes`);
});updateRolledBack
{ fromRelease: Release; reason: RollbackReason; toRelease: Release | null }Fires at the start that follows a rollback, before the readiness gate, and again at each start until the app has come up once. The toRelease is null for the embedded bundle. The event is kept until the first listener of that start registers, so a listener added after the start still receives it.
await HotCodePush.addListener('updateRolledBack', event => {
console.log(`release #${event.fromRelease.number} rolled back: ${event.reason}`);
});Hooks
useUpdates()
useUpdates(): UseUpdatesResultA React hook over the five events and getState(), sugar over the common API and never a capability of its own:
stateisgetState()read on mount and again after each event and each cycle the app runs, and the embedded state until the first read.lastSyncisstate.lastCheck.result, the SDK's own cycles included, ornullbefore the first.isSyncingistruewhile a cycle the app started runs or a pack downloads.downloadProgressis the download's progress from0to1while a pack downloads,nullotherwise.
import { HotCodePush, useUpdates } from '@hotcodepush/react-native-code-push';
import { Pressable, Text, View } from 'react-native';
export function ReleaseLabel() {
const { downloadProgress, isSyncing, lastSync, state } = useUpdates();
const syncText = isSyncing
? downloadProgress === null
? 'syncing…'
: `downloading ${Math.round(downloadProgress * 100)}%`
: (lastSync?.status ?? 'no check yet');
return (
<View>
<Text>{state.currentRelease?.bundleVersion ?? 'embedded'}</Text>
<Text>{syncText}</Text>
<Pressable disabled={isSyncing} onPress={() => void HotCodePush.sync()}>
<Text>Sync now</Text>
</Pressable>
</View>
);
}Types
The types as the package exports them, the SDK's own first. The HotCodePush object is typed by the package's HotCodePush interface: the 16 methods above, with addListener resolving HotCodePushListenerHandle.
interface HotCodePushListenerHandle {
remove: () => Promise<void>;
}
interface UseUpdatesResult {
// The download's progress from 0 to 1 while a pack downloads, null otherwise.
downloadProgress: number | null;
// A cycle the app started is running, or a pack is downloading.
isSyncing: boolean;
// The result of the last cycle, the SDK's own included; null before the first.
lastSync: SyncResult | null;
state: GetStateResult;
}The shared types, the same in every SDK:
// The strategies, the values of the matching hotcodepush.json keys.
type CheckStrategy = 'auto' | 'manual';
type DownloadStrategy = 'auto' | 'manual' | 'unmetered';
type ApplyStrategy = 'immediate' | 'manual' | 'next_resume' | 'next_start';
type MandatoryApplyStrategy = 'immediate' | 'manual';
type ReadySignal = 'manual' | 'render';
// When a scheduled apply runs; `manual` is not a moment.
type ApplyMoment = Exclude<ApplyStrategy, 'manual'>;
// What started a cycle: the SDK on its own, or the app's call.
type SyncTrigger = 'interval' | 'manual' | 'resume' | 'start';
type ConditionType = 'attribute' | 'binary' | 'device' | 'fingerprint' | 'os';
type Platform = 'android' | 'ios';
// The SDK's own release type: 5 fields mapped from an index entry.
interface Release {
bundleId: string;
bundleVersion: string;
id: string;
isMandatory: boolean;
number: number;
}
// The statuses, as the Statuses table below reads them.
type SyncStatus = 'applied' | 'available' | 'downloaded' | 'failed' | 'skipped' | 'up_to_date';
type ApplyUpdateStatus = 'applied' | 'nothing_to_apply';
type SyncResult =
| { release: Release | null; status: 'up_to_date' }
| { downloadSizeBytes: number | null; notes: string | null; release: Release; status: 'available' }
| { applyStrategy: Exclude<ApplyStrategy, 'immediate'>; notes: string | null; release: Release; status: 'downloaded' }
| { notes: string | null; release: Release; status: 'applied' }
| { conditionType?: ConditionType; reason: SkippedReason; release: Release | null; status: 'skipped' }
| { message: string; reason: FailedReason; release: Release | null; status: 'failed' };
// `sync()` with the download pinned to `manual`: nothing is downloaded or applied.
type CheckForUpdateResult = Exclude<SyncResult, { status: 'applied' | 'downloaded' }>;
// `sync()` with the download pinned to `auto`: the cycle never stops at `available`.
type DownloadUpdateResult = Exclude<SyncResult, { status: 'available' }>;
type ApplyUpdateResult =
| { release: Release; status: 'applied' }
| { release: Release | null; status: 'nothing_to_apply' };
interface NotifyReadyResult {
currentRelease: Release | null;
isRolledBack: boolean;
// The release before this start, when it changed.
previousRelease: Release | null;
rollbackReason?: RollbackReason;
}
interface GetStateResult {
currentRelease: Release | null;
// From the resource file; `null` in a build whose step created no binary.
embeddedBundleId: string | null;
failedBundleIds: string[];
fallbackRelease: Release | null;
index: { fetchedAt: string; sequence: number } | null;
lastCheck: { checkedAt: string; result: SyncResult; trigger: SyncTrigger } | null;
nextRelease: Release | null;
// The server time of the month's first acknowledged report.
reportedAt: string | null;
}
type ChannelSource = 'config' | 'runtime';
interface GetChannelResult {
// `null` while no id is known: a build without a channel, or a runtime name no sync has resolved yet.
id: string | null;
name: string | null;
source: ChannelSource;
}
type SetChannelOptions = { id: string } | { name: string } | null;
interface GetDeviceResult {
attributes: Record<string, string>;
binaryBuild: string;
binaryVersion: string;
channel: GetChannelResult;
// `fp1:<sha256>` from the resource file, `null` in a build that carries none.
fingerprint: string | null;
id: string;
osVersion: string;
platform: Platform;
sdkVersion: string;
}
// `null` removes a key; stored and reported stays Record<string, string>.
type SetAttributesOptions = Record<string, string | null>;
interface RollbackUpdateOptions {
// The app-side cause, carried as the failure's detail: at most 256 code points, no control character.
reason?: string;
}
// Each stage's strategy for this call, overriding the configuration.
interface SyncOptions {
applyStrategy?: ApplyStrategy;
downloadStrategy?: DownloadStrategy;
mandatoryApplyStrategy?: MandatoryApplyStrategy;
}
// The apply strategies for this call; the download strategy is pinned to `auto`.
type DownloadUpdateOptions = Omit<SyncOptions, 'downloadStrategy'>;
interface SetRestartAllowedOptions {
isAllowed: boolean;
}
type SkippedReason =
| 'build_debug' | 'bundle_failed_before' | 'channel_paused' | 'condition_unsupported' | 'connection_metered'
| 'device_incompatible' | 'device_not_in_rollout' | 'device_not_targeted' | 'release_older_than_binary'
| 'release_revoked' | 'spending_cap_reached';
type FailedReason =
| 'channel_unknown' | 'content_mismatched' | 'device_offline' | 'download_failed' | 'index_invalid'
| 'manifest_invalid' | 'signature_invalid';
type RollbackReason = 'app_crashed' | 'app_requested' | 'readiness_timed_out';
// What the device reports: the event rows of Results and reasons, and the pack a download came from.
type DeviceEventType = 'applied' | 'checked' | 'confirmed' | 'downloaded' | 'failed';
type PackKind = 'delta' | 'files' | 'full' | 'streamed';
// The events, by name.
interface UpdateAvailableEvent {
downloadSizeBytes: number | null;
notes: string | null;
release: Release;
trigger: SyncTrigger;
}
interface UpdateDownloadedEvent {
applyStrategy: Exclude<ApplyStrategy, 'immediate'>;
release: Release;
trigger: SyncTrigger;
}
interface UpdateFailedEvent {
message: string;
reason: FailedReason;
release: Release | null;
trigger: SyncTrigger;
}
interface DownloadProgressEvent {
downloadSizeBytes: number;
progress: number;
progressBytes: number;
releaseId: string;
}
interface UpdateRolledBackEvent {
fromRelease: Release;
reason: RollbackReason;
toRelease: Release | null;
}
interface HotCodePushEvents {
downloadProgress: DownloadProgressEvent;
updateAvailable: UpdateAvailableEvent;
updateDownloaded: UpdateDownloadedEvent;
updateFailed: UpdateFailedEvent;
updateRolledBack: UpdateRolledBackEvent;
}
type HotCodePushEventName = keyof HotCodePushEvents;The release is null where none could be evaluated: the index holds nothing for the embedded bundle, could not be read, or the cycle stopped before it. A failure after the evaluation names the release it tried. conditionType on skipped names the condition type behind device_incompatible or device_not_targeted, never a value.
Statuses
Seven statuses, named by what the app should do next.
| Status | From | Meaning |
|---|---|---|
up_to_date | sync(), checkForUpdate(), downloadUpdate() | There is nothing to do. A release that carries the running bundle answers it too. |
available | checkForUpdate(), and sync() when the download stayed manual | A release this device can take is not downloaded yet, or was downloaded already when checkForUpdate() asked. Call downloadUpdate(). |
downloaded | sync(), downloadUpdate() | The update is downloaded and verified. Under applyStrategy: manual call applyUpdate(), otherwise the strategy's moment applies it. |
applied | sync(), downloadUpdate(), applyUpdate() | The apply happened, and the update becomes the current release as the app reloads, now or once the app is up in this run. |
nothing_to_apply | applyUpdate() | Nothing waits. |
skipped | sync(), checkForUpdate(), downloadUpdate() | There is a newer release and this device will not take it now, and reason says why. |
failed | sync(), checkForUpdate(), downloadUpdate() | Something went wrong on the way, and reason says what. |
The reason on skipped and failed, and the rollback reasons on updateRolledBack and notifyReady(), are the unions above. Results and reasons is the catalog: what each code means and what to do.
Errors
No error carries a code, and every outcome an app can act on is a result. A method with nothing to do does nothing: applyUpdate() without a downloaded update answers nothing_to_apply, and rollbackUpdate() on the embedded bundle resolves. A method rejects with a plain error, a message and nothing else, for a programming mistake found in development:
- a strategy value outside its choices on
sync()ordownloadUpdate() isAllowedthat is not a boolean- an attribute key or value that breaks its rule, or a value that is neither a string nor
null - a rollback
reasonthat breaks the value rule - a channel
idthat is not a UUID
A build whose build step never ran has no resource file, and a build whose file the reader refused is the same case with the reason. Every method then rejects with one error until the app is rebuilt. The error says HotCodePush is not configured, names the missing file or the reader's reason, and points at npx hotcodepush init and a rebuild.
Where the native module is missing, on the web or in Expo Go, the package loads and every method rejects with one error, removeAllListeners() included. The error says the SDK needs a development or release build. The useUpdates() hook ignores the rejection and keeps the embedded state.
Guard the calls with the platform check you use for every native-only module, or catch the rejection.
Last updated on