Code Review Standards for RN
Performance, accessibility, and native-boundary review checklist for Expo SDK 57 PRs. Standardize what reviewers verify so quality scales without the tech lead on every diff.
Search across all documentation pages
Performance, accessibility, and native-boundary review checklist for Expo SDK 57 PRs. Standardize what reviewers verify so quality scales without the tech lead on every diff.
Quick-reference PR review card - paste into
.github/pull_request_template.md.
## Mobile review checklist
### Evidence (UI / gesture / a11y changes)
- [ ] iOS screenshot or recording attached
- [ ] Android screenshot or recording attached
- [ ] VoiceOver OR TalkBack spot-check noted (which flow)
### Performance
- [ ] Lists use FlashList/FlatList - not ScrollView for long feeds
- [ ] `renderItem` / `keyExtractor` stable (useCallback or module scope)
- [ ] No new heavy imports in `app/_layout.tsx` without lazy route
### Accessibility
- [ ] `accessibilityLabel` on icon-only controls
- [ ] `accessibilityRole` matches behavior (button vs link)
- [ ] Dynamic type: layout survives large font (or `maxFontSizeMultiplier` justified)
### Native boundary
- [ ] New deps added via `npx expo install <pkg>`
- [ ] `app.config.ts` / config plugin changes called out in description
- [ ] Classified: JS-only (OTA OK) vs requires `eas build`
- [ ] `npx expo-doctor` green (CI link)
### Architecture
- [ ] Route file thin - logic in `features/`
- [ ] No deep imports across feature boundaries
- [ ] Server state in TanStack Query - not duplicated in Zustand
### Release (if touching eas.json, runtimeVersion, channels)
- [ ] Rollback steps in ticket / PR comment
- [ ] runtimeVersion bump justifiedWhen to reach for this:
// ❌ Reviewer blocks - ScrollView map of 200 orders
export function OrdersScreenBad() {
const { data } = useOrders();
return (
<ScrollView>
{data?.map((o) => <OrderRow key={o.id} order={o} />)}
</ScrollView>
);
}// ✅ Approve path - virtualized list + stable renderItem
import { FlashList } from "@shopify/flash-list";
import { useCallback } from "react";
export function OrdersScreen() {
const { data } = useOrders();
const renderItem = useCallback(
({ item }: { item: Order }) => <OrderRow order={item} />,
[]
);
return (
<FlashList
data={data ?? []}
renderItem={renderItem}
keyExtractor={(o) => o.id}
estimatedItemSize={72}
/>
);
}Reviewer questions:
renderItem every render (inline arrow)?See Performance Best Practices and Memory Leaks & List Churn.
// ❌ Block - icon-only, no name, wrong role
<Pressable onPress={onSave}>
<Icon name="check" />
</Pressable>// ✅ Approve - name, role, state
<Pressable
onPress={onSave}
accessibilityRole="button"
accessibilityLabel="Save changes"
accessibilityState={{ disabled: !dirty, busy: saving }}
>
<Icon name="check" accessible={false} />
</Pressable>Reviewer spot-check (30 seconds):
See accessibilityLabel & accessibilityRole.
// package.json - reviewer verifies expo install, not raw npm major
+ "expo-camera": "~17.0.10"// app.config.ts - plugin stanza present
plugins: [
[
"expo-camera",
{
cameraPermission: "Allow $(PRODUCT_NAME) to scan warehouse barcodes.",
},
],
],Reviewer checklist:
| Question | Pass criteria |
|---|---|
| OTA safe? | No - native module; PR labels requires-build |
| Permission strings human-readable? | Yes - store review reads these |
| Expo Go test claimed? | Flag misleading - needs dev client |
| ADR or ticket links native choice? | Architecture Decision Records |
// ❌ Block - fetch + branching in app/ route
// app/(tabs)/orders.tsx
export default function OrdersRoute() {
const [filter, setFilter] = useState("open");
const { data } = useQuery({ queryKey: ["orders", filter], queryFn: () => fetchOrders(filter) });
return <FlatList data={data} ... />;
}// ✅ Approve - thin route
// app/(tabs)/orders.tsx
export { OrdersScreen as default } from "@/features/orders/screens/OrdersScreen";Aligns with Mobile Architecture Basics and Feature-Sliced Design for RN.
| Severity | Examples | Action |
|---|---|---|
| Block | Missing permission plugin; ScrollView feed; secret in EXPO_PUBLIC_* | Must fix before merge |
| Request changes | Unstable list props; missing a11y label; deep feature import | Fix or ticket + follow-up |
| Comment | Naming, optional refactor, docs | Author discretion |
| Praise | Tests, rollback notes, device matrix evidence | Reinforce behavior |
Sim-only iOS Pro Max - Layout and perf lie. Fix: Require Android emu evidence for UI PRs.
"LGTM" on package.json only PR - Native drift without plugin. Fix: Native-boundary section mandatory when lockfile changes include expo-* native packages.
Reviewing Expo Go screenshot for reanimated worklet bug - Dev client differs. Fix: Preview build link in PR.
Approving Zustand cache of API list - Duplicate source of truth. Fix: Point to ADR: State Management Selection.
Skipping rollback on eas.json channel change - Wrong users get update. Fix: Release subsection required.
| Approach | Use When | Don't Use When |
|---|---|---|
| PR template checklist | Default - low friction | Fully automated visual diff only teams |
| Danger / custom bot | Enforce screenshot labels | Small team - maintenance cost |
| Pair review only | Complex native migration | Daily feature throughput |
CODEOWNERS on app.config.ts | Multi-squad monorepo | Solo maintainer overhead |
For native config and gesture PRs - yes. For copy-only TSX, device evidence in PR is enough if CI is green.
Target same business day for PRs under 400 lines. If longer, author should split or schedule review pairing.
Alternate per PR is fine; both platforms must be covered across the sprint - not all iOS.
Skip reviewing .expo/types line-by-line; do verify tsc passes and route params match usage.
Same checklist; seniors escalate architecture and native-boundary items. Juniors focus on a11y and list patterns first - Junior → Mid RN Leveling.
Stack versions: This page was written for React 19.2.3, React Native 0.86.0, and Expo SDK 57 (
expo~57.0.4).
Reviewed by Chris St. John·Last updated Jul 16, 2026