Device and App Info
Read build and device facts with expo-application and expo-device, and branch on OS version rather than on a model name.
const label = `${Application.nativeApplicationVersion} (${Application.nativeBuildVersion})`Two small modules answer most questions about where your app is running. expo-application describes the installed app; expo-device describes the hardware and operating system. Install them with npx expo install expo-application expo-device.
Constants, not hooks
Most of what these modules expose are plain constants read at startup, so Device.osVersion and Application.nativeApplicationVersion are values, not calls and not hooks. A few things need a call because the OS answers asynchronously: Application.getInstallationTimeAsync() resolves to a Date, and Device.getDeviceTypeAsync() resolves to a DeviceType member such as PHONE or TABLET. Anything the platform cannot answer is null, which is why Device.modelName ?? "Unknown" appears so often. That is shorthand for Device.modelName !== null ? Device.modelName : "Unknown".
Version against build number
nativeApplicationVersion is the human version: CFBundleShortVersionString on iOS, version on Android. nativeBuildVersion is the number the stores order uploads by: CFBundleVersion on iOS, versionCode on Android. Users quote the first; support needs the second. Print both.
Branch on the version, not the model
A model name is a marketing string that changes every year. Capability tracks the OS version. On Android, Platform.Version and Device.platformApiLevel are the API level as a number; on iOS Platform.Version is a version string and platformApiLevel is null. Compare those, never modelName.
Device.isDevice is false on a simulator, which is useful for keeping test payments out of production.
Example
import * as Application from "expo-application";
import * as Device from "expo-device";
import { Platform, StyleSheet, Text, View } from "react-native";
export default function AboutScreen() {
// These are constants, not function calls.
const version = Application.nativeApplicationVersion ?? "unknown";
const build = Application.nativeBuildVersion ?? "unknown";
// ?? is shorthand for: Device.osName !== null ? Device.osName : Platform.OS
const os = `${Device.osName ?? Platform.OS} ${Device.osVersion ?? ""}`.trim();
return (
<View style={styles.screen}>
<Text style={styles.heading}>About this app</Text>
<Text style={styles.row}>{`Version ${version} (build ${build})`}</Text>
<Text style={styles.row}>{os}</Text>
<Text style={styles.row}>{Device.modelName ?? "Unknown device"}</Text>
{/* A string must sit inside Text, so the simulator badge is a Text too. */}
{Device.isDevice ? null : <Text style={styles.badge}>Simulator build</Text>}
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, padding: 24, gap: 8 },
heading: { fontSize: 22, fontWeight: "600", marginBottom: 8 },
row: { fontSize: 16, color: "#374151" },
badge: { fontSize: 13, color: "#b45309", marginTop: 12 },
});When to use it
- Put version and build on a settings screen so support can match a report to a binary.
- Attach device and OS details to every crash report.
- Skip a feature on OS versions that do not support it.
- Point the app at a payment sandbox when Device.isDevice is false.
More examples
Branching on the OS version
This is the right shape for every capability check: a pure function of the OS and its version, plus one line that feeds it the real constants. Android 12 and earlier granted notifications at install, Android 13 added a runtime prompt, and the API level is the only reliable way to tell those apart. Comparing modelName instead would break on every device released after you wrote the check.
// capabilities.ts
import { Platform } from "react-native";
import * as Device from "expo-device";
// Takes the facts as arguments so it can be tested without a device.
export function needsNotificationPrompt(
os: string,
androidApiLevel: number | null,
): boolean {
if (os === "ios") return true; // iOS has always prompted
if (os !== "android") return false;
// Android 13 is API level 33, the release that added POST_NOTIFICATIONS.
return androidApiLevel !== null && androidApiLevel >= 33;
}
// The call site supplies the real values.
export const shouldPrompt = needsNotificationPrompt(
Platform.OS,
Device.platformApiLevel, // number on Android, null on iOS
);
console.log(needsNotificationPrompt("android", 33));
// Output: true
console.log(needsNotificationPrompt("android", 31));
// Output: false
console.log(needsNotificationPrompt("ios", null));
// Output: falseA diagnostics payload for bug reports
Attach this object to every crash report and support email. Three fields do most of the work: build tells you which binary, bundleId tells you which variant (a staging build has a different application ID), and realDevice separates a tester in a simulator from a real user. Keep it to facts the user can see on an about screen; identifiers that follow a person around belong in a privacy review, not in a convenience payload.
import * as Application from "expo-application";
import * as Device from "expo-device";
import { Platform } from "react-native";
export type Diagnostics = {
app: string;
build: string;
bundleId: string;
os: string;
device: string;
realDevice: boolean;
installedAt: string | null;
};
export async function collectDiagnostics(): Promise<Diagnostics> {
// Resolves to a Date on Android and iOS, null on web.
const installed = await Application.getInstallationTimeAsync();
return {
app: Application.nativeApplicationVersion ?? "unknown",
build: Application.nativeBuildVersion ?? "unknown",
bundleId: Application.applicationId ?? "unknown",
os: `${Device.osName ?? Platform.OS} ${Device.osVersion ?? "?"}`,
device: Device.modelName ?? "unknown",
realDevice: Device.isDevice,
installedAt: installed ? installed.toISOString() : null,
};
}Phone or tablet layout
getDeviceTypeAsync returns a DeviceType member, and comparing against Device.DeviceType.TABLET is clearer than guessing from screen size. Use it for decisions that are really about the class of device, such as whether a split view makes sense. For decisions about available space, measure the window instead, because a tablet in a small multitasking pane is as narrow as a phone. On screen: two stacked grey panes on a phone, side by side on a tablet.
import { useEffect, useState } from "react";
import { StyleSheet, Text, View } from "react-native";
import * as Device from "expo-device";
export default function Dashboard() {
const [twoColumn, setTwoColumn] = useState(false);
useEffect(() => {
let cancelled = false;
Device.getDeviceTypeAsync().then((type) => {
if (cancelled) return;
setTwoColumn(type === Device.DeviceType.TABLET);
});
return () => {
cancelled = true;
};
}, []);
return (
<View style={[styles.screen, twoColumn ? styles.row : styles.column]}>
<View style={styles.pane}>
<Text>List</Text>
</View>
<View style={styles.pane}>
<Text>Detail</Text>
</View>
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, gap: 12, padding: 12 },
// flexDirection is column by default, so row has to be stated.
row: { flexDirection: "row" },
column: { flexDirection: "column" },
pane: { flex: 1, backgroundColor: "#f3f4f6", borderRadius: 12, padding: 16 },
});
Discussion