Luyện Phỏng Vấn IT — 2000+ Câu Hỏi Phỏng Vấn IT Có Đáp Án 2026

Danh mục

React Native iconReact Native

ReactJS là thư viện JavaScript render ra DOM trong browser, output là HTML/CSS. React Native là framework dùng cùng model component + JSX nhưng render ra native UI components (UIView trên iOS, ViewGroup/Android.View trên Android), không có DOM, không có HTML tag.

Khác biệt cụ thể:
- Tag: Web dùng <div>/<p>/<button>; RN dùng <View>/<Text>/<Pressable>. Mọi text bắt buộc nằm trong <Text> — viết text trần trong <View> là crash.
- Style: Web dùng CSS file/inline với units px/rem/%; RN dùng StyleSheet.create() với object JS, đơn vị là DIP (density-independent pixels), không có cascading, không có display: block/inline, default flexDirection: column.
- Routing: Web dùng react-router/Next.js; RN dùng react-navigation (stack/tab/drawer).
- Build: Web build ra HTML+JS bundle; RN build ra binary .ipa/.apk qua Metro bundler + native toolchain (Xcode/Gradle).
- API: RN có thêm Platform, Dimensions, AppState, AsyncStorage, không có window/document.

Điểm chung: hooks, JSX, component composition, props/state, context — code logic gần như copy-paste được.

Cú pháp JSX giống hệt — {} cho expression, className không tồn tại (RN dùng style), nhưng set tag hợp lệ khác hoàn toàn. Web JSX cho phép mọi HTML element vì cuối cùng compile ra DOM. RN JSX chỉ chấp nhận các component được export từ react-native hoặc do bạn viết — không có <div>, <span>, <p>, <button>, <input>, <a>, <ul>/<li>.

Mapping thường dùng:
- <div><View>
- <span>/<p>/<h1><Text>
- <button><Pressable> hoặc <TouchableOpacity>
- <input><TextInput>
- <img><Image source={{ uri: ... }} />
- <a><Pressable onPress={() => Linking.openURL(...)}>

Thuộc tính style cũng nhận object JS thay vì string CSS, và không có pseudo-class (:hover, :focus) — phải tự xử qua state.

Trên iOS và Android, string không phải là một loại view. UIView không biết cách render ký tự — chỉ UILabel/TextView mới làm được. RN buộc bạn bao text trong <Text> để binding sang đúng native component.

Nếu viết <View>Hello</View>, RN throw runtime error: "Text strings must be rendered within a <Text> component." Trên web, <div>Hello</div> chạy được vì DOM cho phép text node bất cứ đâu.

Khác biệt phụ kéo theo:
- Style font, color, lineHeight chỉ áp dụng được trên <Text> — set trên <View> không có tác dụng.
- <Text> lồng trong <Text> thì kế thừa style cha (giống <span> trong <p>); nhưng <Text> lồng trong <View> thì không kế thừa.
- numberOfLines, ellipsizeMode chỉ tồn tại trên <Text>.

Giống: RN dùng cùng React core nên hooks (useEffect, useLayoutEffect, useRef...) và class lifecycle (componentDidMount, componentWillUnmount) hoạt động y hệt web. Mount/update/unmount cycle giống.

Khác:
- Không có DOM event lifecycle (load, DOMContentLoaded).
- useLayoutEffect vẫn tồn tại nhưng semantics khác theo architecture: trên Old Arch (Paper), commit UIView async qua bridge nên hook chạy "trước paint" không nghiêm ngặt như web; trên New Arch (Fabric), commit đồng bộ qua C++ ShadowTree nên hook thật sự chạy trước khi UI flush ra native.
- AppState là khái niệm mới: app có thể vào background (background/inactive) khi user lock screen hoặc switch app. Nên subscribe AppState.addEventListener cho các tác vụ pause/resume (timer, video player).
- Screen focus lifecycle (qua react-navigation): useFocusEffect/useIsFocused thay vì gắn vào component mount, vì màn hình bị navigation cache (không unmount khi đẩy stack).
- Khi app bị OS kill (out of memory), không có lifecycle callback nào chạy — phải persist state vào storage chủ động.

Hoàn toàn không khác — đây là React core, RN không can thiệp. Props vẫn là dữ liệu cha truyền xuống, immutable từ phía con. State vẫn local, mutable qua setState/useState, trigger re-render khi đổi.

Vài điểm cần lưu ý đặc thù RN:
- Performance impact lớn hơn: mỗi re-render RN phải thông qua native side để cập nhật UIView — list lớn re-render lặp gây jank rõ rệt hơn web. Vì vậy React.memo/useCallback quan trọng hơn web.
- State persist: state mất khi user kill app. Nếu cần giữ (vd login token), persist vào AsyncStorage/MMKV.
- Props drilling cross-screen: vì navigation không phải parent-child relationship, không thể truyền prop xuyên screen — dùng route params, context, hoặc store toàn cục.
- useState initializer pattern: useState(() => expensiveCompute()) quan trọng trên RN vì khởi tạo chậm sẽ block JS thread → chậm transition.

StyleSheet.create({ ... }) validate keys lúc dev (warn nếu sai tên prop, vd colour), freeze object trả về để giữ reference ổn định giữa các render. Lịch sử (RN <0.59) còn thay object bằng integer id để tối ưu bridge — tối ưu này đã bị bỏ trong RN hiện đại nên đừng nhớ nhầm.

Inline style style={{ padding: 10, color: 'red' }} tạo object mới mỗi render → nếu prop này truyền vào React.memo child, shallow compare fail → child re-render thừa.

Khi nào dùng cái nào:
- StyleSheet.create cho style cố định, ưu tiên mặc định. Lợi ích thực tế 2026: dev-time validation, reference stability, ý đồ rõ ràng cho người đọc.
- Inline cho style động phụ thuộc props/state: style={{ opacity: pressed ? 0.5 : 1 }}. Pattern chuẩn: gộp vào array để giữ phần tĩnh trong StyleSheet — style={[styles.btn, pressed && styles.btnPressed]}.

Với Fabric (New Arch) việc commit style hoàn toàn không qua bridge nên performance giữa hai cách gần như tương đương — chọn StyleSheet.create chủ yếu vì readability và lint, không vì runtime perf.

Số trong style: { width: 100 } không phải physical pixel — đó là DIP (density-independent pixel), tương tự dp Android hay pt iOS. RN tự nhân với PixelRatio.get() để ra physical pixel khi render.

Ví dụ trên iPhone 14 (3x density), width: 100 thật sự là 300 physical pixel; trên màn 1x là 100. Mục đích: cùng một số → kích thước vật lý ngang nhau giữa các màn hình mật độ khác nhau, để UI không bị tí hon trên màn 4K hay khổng lồ trên màn cũ.

Thông tin liên quan:
- PixelRatio.get() trả 1, 1.5, 2, 2.5, 3, 3.5, 4 tùy device.
- PixelRatio.getPixelSizeForLayoutSize(100) chuyển DIP → physical pixel (cần khi gọi native API yêu cầu pixel thật, vd Canvas).
- StyleSheet.hairlineWidth là 1 physical pixel — dùng cho border mảnh (borderBottomWidth: StyleSheet.hairlineWidth).
- Image asset gắn @2x/@3x để Metro pick đúng độ phân giải theo density.

<View> là container cơ bản — tương đương <div> web, không có default text rendering. Dùng cho mọi layout block (row/column).

<Text>bắt buộc cho mọi chuỗi ký tự. Hỗ trợ numberOfLines, ellipsizeMode, font style, lồng <Text> con để inline format.

<Image source={{ uri }} /> (remote) hoặc <Image source={require('./logo.png')} /> (local). Phải set width/height rõ ràng cho remote — không có intrinsic size như HTML.

<ScrollView> cho nội dung ngắn, render toàn bộ children ngay lập tức. Không dùng cho list dài (>20 items) vì không virtualize → memory leak, scroll lag. Dùng <FlatList> thay.

Quy tắc nhanh: container không text → View; text → Text; ảnh → Image; nội dung scroll cố định → ScrollView; list dữ liệu động → FlatList.

keyExtractor trả về unique string cho mỗi item, để React reconciler biết phân biệt khi data thay đổi (giống key prop trong list React thường).

tsx
<FlatList
  data={users}
  keyExtractor={(item) => item.id.toString()}
  renderItem={({ item }) => <UserRow user={item} />}
/>

Nếu không cung cấp, FlatList fallback dùng item.key rồi item.id, cuối cùng là index. Dùng index gây bugs nghiêm trọng:
- Xóa item ở giữa → các item sau "shift" key, RN tưởng là item cũ đổi nội dung → state internal (textinput, animation, scroll position con) chuyển nhầm sang item khác.
- Animation transition không match: item mới fade-in trông như item cũ đổi data.

Quy tắc: luôn dùng id thật từ backend, hoặc UUID stable. Đừng dùng Math.random() — mỗi render sinh key mới làm toàn bộ list re-render từ đầu.

Controlled (chuẩn React, recommend default): state cha giữ value, onChangeText cập nhật.

tsx
const [name, setName] = useState('')
<TextInput value={name} onChangeText={setName} />

Lợi thế: validate/format on-the-fly, sync với store toàn cục, easy reset.

Uncontrolled: dùng defaultValueonChangeText cache giá trị vào ref ngoài, không bind vào React state.

tsx
const valueRef = useRef('')
<TextInput
  defaultValue=""
  onChangeText={(t) => { valueRef.current = t }}
/>
// Lúc submit:
console.log(valueRef.current)

RN không có inputRef.current.value như DOM web — phải tự cache qua onChangeText.

Thực tế RN, uncontrolled thuần ít gặp; pattern thường gặp hơn là react-hook-form dùng Controller ẩn ref bên trong → có cảm giác controlled cho user nhưng không re-render parent mỗi keystroke. Form lớn (10+ inputs) trên Android low-end thấy khác biệt rõ.

Ngoài ra, controlled <TextInput> từng có vấn đề race condition trên Android dưới Bridge cũ: user gõ 5 ký tự liên tục, JS state update async qua bridge, đôi khi caret nhảy ngược hoặc ký tự "rớt". New Architecture với JSI làm commit đồng bộ → vấn đề này biến mất ở RN 0.76+.

RN dùng Yoga engine của Meta — implement subset Flexbox. Khác biệt quan trọng:

  • Default flexDirectioncolumn (web là row). Cần nhớ: <View> xếp children theo chiều dọc mặc định.
  • flex: 1 viết tắt cho flexGrow: 1, flexShrink: 1, flexBasis: 0 (đa số case web cũng vậy).
  • Không có display: block/inline — chỉ flex hoặc absolute. <Text> luôn inline-like trong cùng một <Text> cha, ngoài ra render block.
  • Không có gap đến RN 0.71; từ 0.71+ có gap/rowGap/columnGap.
  • Không có auto cho margin trong vài context cũ — marginLeft: 'auto' để đẩy phải hoạt động trong flex.
  • alignSelf: stretch là default (web là auto).
  • % đơn vị chỉ work cho width/height/margin/padding/flexBasis — không cho mọi prop.
  • Không có float, grid, multi-column, position: sticky.

Một số quirk: aspectRatio được hỗ trợ, transform array thay vì string, position: absolute định vị theo gần nhất parent có position: relative — giống web.

Stack Navigator (@react-navigation/native-stack hoặc stack): điều hướng kiểu push/pop — màn hình mới đè lên màn cũ, có animation slide từ phải. Dùng cho: detail flow, wizard, checkout. Mỗi navigate('X') push thêm screen vào stack, goBack() pop ra.

Tab Navigator (bottom-tabs / material-top-tabs): chuyển ngang giữa các "section" của app — Home / Search / Profile. State của mỗi tab được giữ độc lập (chuyển tab không reset scroll). Dùng cho: navigation chính của app, không phải sub-flow.

Drawer Navigator (drawer): menu trượt từ cạnh — phổ biến trên Android, ít dùng iOS. Dùng cho: app có 5+ section chính không vừa tab bar. iOS HIG khuyến nghị tab bar hơn drawer.

Pattern lồng (nested): thường có Tab navigator chứa các Stack navigator con — ví dụ Home tab có HomeStack với Home screen → Detail screen → Comments screen.

@react-native-async-storage/async-storage: key-value persistent, async API, dữ liệu lưu file (iOS) hoặc SQLite (Android). Cross-platform, ổn định, hệ sinh thái lớn. Nhược: async (Promise mỗi call) → block render khi data nhiều, plain text không mã hóa, limit ~6MB Android.

react-native-mmkv: key-value siêu nhanh, đồng bộ API, dùng MMKV của Tencent (mmap-based). 30x nhanh hơn AsyncStorage. Hỗ trợ encryption AES (option). API:

ts
const storage = new MMKV()
storage.set('token', 'abc') // sync, không await
const token = storage.getString('token')

Ưu: Sync (đọc trong render, không Promise), encryption built-in, performance đỉnh.

Expo SecureStore / react-native-keychain: dùng Keychain (iOS) / Keystore (Android) để lưu sensitive data (token, password). Mã hóa hardware-backed. Slow hơn nhiều so với MMKV vì hardware roundtrip.

Quy tắc:
- Sensitive (auth token, refresh token): SecureStore/Keychain.
- App preferences, cache nhỏ, sync cần thiết: MMKV.
- Compatibility với lib cũ, dữ liệu không sensitive: AsyncStorage.

LayoutAnimation là built-in API — khai báo "tôi muốn next layout change được animate", và RN tự interpolate giữa layout cũ và mới.

tsx
import { LayoutAnimation, UIManager, Platform } from 'react-native'

if (Platform.OS === 'android') {
  UIManager.setLayoutAnimationEnabledExperimental?.(true)
}

const toggle = () => {
  LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
  setExpanded(!expanded)
}

Use case phù hợp:
- Add/remove item trong list ngắn (FlatList animation chuyển vị trí).
- Expand/collapse accordion.
- Show/hide UI block khi state đổi.

Hạn chế:
- Animation đơn giản — chỉ có 3 preset (easeInEaseOut, linear, spring).
- Khó custom: timing fix, interpolation curve hạn chế.
- Apply cho toàn bộ tree trong tick đó — không scope được vào component cụ thể.
- Conflict với Reanimated layout animation nếu dùng song song.

Khi nào dùng cái khác:
- Cần animation precise → Reanimated entering={FadeIn}, layout={Layout.springify()}.
- Cần gesture-driven → Reanimated worklet.
- Cần per-item animation trong list dài → FlashList với cellAnimation.

Thực tế 2026: dùng LayoutAnimation cho prototype nhanh hoặc UI rất đơn giản. Production lớn dùng Reanimated. RN dev khuyến nghị deprecate LayoutAnimation sau khi Reanimated layout API stable.

Bridge cũ (legacy) là hàng đợi message bất đồng bộ giữa JS thread và Native thread. Mỗi lần JS gọi native (vd setState → re-render UIView), payload được serialize JSON, đẩy qua bridge, deserialize bên kia. Đặc điểm:
- Async-only: không thể gọi native sync; mọi tương tác phải qua callback/Promise.
- Batched: message gom batch để giảm overhead, nhưng vẫn có lag visible khi list scroll nặng.
- JSON serialization: dữ liệu lớn (image, blob) tốn nhiều CPU.

JSI (JavaScript Interface) thay bridge bằng C++ layer. JS engine (Hermes/JSC) giữ tham chiếu trực tiếp tới object C++ → có thể gọi native function đồng bộ, không serialize, không queue. Lợi ích:
- TurboModule load lazy thay vì load toàn bộ lúc startup.
- Fabric renderer dùng JSI để commit UI tree đồng bộ với React reconciler.
- Reanimated 3 chạy worklet trên UI thread qua JSI — animation 60/120fps không bị block.

RN 0.74 (2024) giới thiệu Bridgeless mode; RN 0.76 (Q4 2024) đặt New Architecture (Fabric + TurboModules + Bridgeless) làm default cho project mới khởi tạo qua npx @react-native-community/cli@latest init. Project cũ vẫn có thể opt-in.

Hermes là JavaScript engine open-source do Meta viết cho RN, tối ưu cho mobile. JSC (JavaScriptCore) là engine mặc định cũ — chính là engine của Safari/iOS.

Khác biệt chính:
- Bytecode pre-compile: Hermes compile JS bundle thành bytecode lúc build (hbc file). App startup không cần parse JS → giảm time-to-interactive 30–50%, đặc biệt thấy rõ trên Android low-end.
- Memory: Hermes tiêu thụ RAM ít hơn JSC ~30% nhờ generational GC và string deduplication.
- APK/IPA size: bytecode + Hermes runtime nhỏ hơn JSC bundle (RN 0.71+ Hermes được lazy-load đẹp).
- Async/await native: Hermes implement async/await không cần regenerator-runtime polyfill.
- Debugger: RN DevTools (RN 0.76+) dựa Chrome DevTools Protocol qua Hermes.

Nhược điểm Hermes: chậm hơn JSC ở vài benchmark CPU-intensive (regex, JSON.parse lớn). Bật Hermes mặc định từ RN 0.70 (Android) và 0.71 (iOS); chỉ tắt khi gặp lib không tương thích.

JS code không thay đổi giữa platform, nhưng layer dưới khác hẳn:

iOS: mỗi <View> map sang một UIView (Objective-C/Swift), <Text>RCTTextView/UILabel, <Image>RCTImageView/UIImageView. Layout dùng Yoga (cross-platform Flexbox engine của Meta), kết quả set frame cho UIView.

Android: <View> map sang ViewGroup hoặc View (Java/Kotlin), <Text>ReactTextView/TextView, <Image>ReactImageView. Cũng qua Yoga để compute layout, sau đó áp lên Android View hierarchy.

Hệ quả thực tế:
- Style giống nhau (cùng Yoga) → flex layout consistent cross-platform.
- Behaviour native khác: scrollbar style, text rendering (font hinting), shadow (iOS dùng shadow*, Android dùng elevation), borderRadius overflow trong <Image> từng có bug Android vài năm.
- Kích thước màn hình Dimensions.get('screen') trên Android có thể bao gồm system UI (status bar, navigation bar), trên iOS không — cần test cẩn thận.
- zIndex có phản ứng khác giữa hai platform; ưu tiên đổi thứ tự render thay vì lệ thuộc zIndex.

Inline check (Platform.OS / Platform.select) dùng cho khác biệt nhỏ trong cùng một component:

tsx
const styles = StyleSheet.create({
  shadow: Platform.select({
    ios: { shadowColor: '#000', shadowOpacity: 0.2, shadowRadius: 4 },
    android: { elevation: 4 },
  }),
})

Tách file (Component.ios.tsx + Component.android.tsx) dùng khi:
- Code khác biệt nhiều (>30%) — vd map view dùng react-native-maps iOS gọi Apple Maps, Android gọi Google Maps qua key khác.
- Native module chỉ tồn tại một bên.
- Performance: tránh ship code không dùng — Metro bundler tự pick .ios.tsx cho iOS, .android.tsx cho Android, code platform khác không vào bundle.

Trade-off: tách file khó maintain (logic share phải extract ra Component.shared.ts), nên chỉ tách khi thật sự cần. Còn Platform.OS === 'ios' rải rác hơn 5–6 chỗ trong file thì chính là tín hiệu nên tách.

ScrollView: render toàn bộ children ngay lập tức. Dùng cho UI ngắn (form, profile screen), không có pattern data-driven. Chậm và tốn memory với list >20 items.

FlatList: virtualized list — chỉ render items đang visible + windowSize buffer. Lazy mount và unmount khi scroll qua. API: data, renderItem, keyExtractor. Dùng cho list đồng nhất một loại item (chat, feed).

SectionList: như FlatList nhưng có header/footer cho từng section. Data shape [{ title, data: [...] }]. Dùng cho contact list, settings có grouping.

Hậu quả nếu chọn sai:
- ScrollView 1000 items: app freeze 5–10s lúc mount, RAM spike, scroll lag.
- FlatList với initialNumToRender={1000}: tương đương ScrollView, mất ý nghĩa virtualize.
- Lồng FlatList trong ScrollView: VirtualizedList warning, virtualization không hoạt động (parent extend chiều cao vô hạn). Nếu cần list trong scroll, dùng <FlatList ListHeaderComponent={...} ListFooterComponent={...}>.

RN core có <SafeAreaView>, hoạt động trên iOS only (đặt padding theo notch/home indicator). Trên Android render như <View> thường — không có effect. Bị deprecate-ish và không xử lý đúng khi rotate hay multi-window.

react-native-safe-area-context (lib mặc định trong Expo) cung cấp:
- Hoạt động cả iOS và Android (Android có status bar inset, gesture nav inset từ Android 10+).
- useSafeAreaInsets() hook trả { top, right, bottom, left } numeric — dễ tùy biến hơn component bao toàn bộ.
- SafeAreaProvider ở root để inset context lan xuống mọi screen.
- Update đúng khi rotate, split-screen, foldable.

Pattern phổ biến:

tsx
const insets = useSafeAreaInsets()
<View style={{ paddingTop: insets.top, paddingBottom: insets.bottom }} />

Dùng <SafeAreaView edges={['top']}> từ lib này khi muốn chỉ thoát notch trên (vd screen có tab bar bottom riêng).

<KeyboardAvoidingView> đẩy nội dung lên khi keyboard mở để không che TextInput. Prop behavior:

  • padding (recommend cho iOS): thêm padding bottom = chiều cao keyboard. Children giữ nguyên, chỉ vùng dưới được "đẩy" lên. Mượt và predictable.
  • height: shrink chiều cao container = screenHeight - keyboardHeight. Hữu ích khi container có background image — tránh image bị resize.
  • position: dịch toàn bộ container lên trên. Dễ gây content bị cắt nếu không có ScrollView ngoài.
  • undefined (Android): default Android có windowSoftInputMode="adjustResize" trong AndroidManifest.xml xử lý việc này tự động.

Pattern thực tế cross-platform:

tsx
<KeyboardAvoidingView
  behavior={Platform.OS === 'ios' ? 'padding' : undefined}
  style={{ flex: 1 }}
  keyboardVerticalOffset={headerHeight}
>
  <ScrollView keyboardShouldPersistTaps="handled">{...}</ScrollView>
</KeyboardAvoidingView>

keyboardVerticalOffset cần khi có header navigation bên trên — bù phần header để keyboard đẩy đúng vị trí.

<Image> core trong RN có cache rất cơ bản: iOS dùng NSURLCache (limit 20MB default), Android dùng OkHttp memory cache. Không có disk cache mạnh, không có placeholder/blur, không có priority queue. Hậu quả: list ảnh lớn → reload từ network khi user scroll lên/xuống nhiều lần, RAM spike.

expo-image (recommended 2026):
- Dùng SDWebImage (iOS) + Glide (Android) bên dưới — cache mạnh, disk + memory.
- Hỗ trợ placeholder (blurhash, thumbnail), transition, contentFit (như CSS object-fit).
- Hỗ trợ format mới: AVIF, WebP, GIF, animated WebP.
- Cài qua Expo (dùng được cả bare workflow).

react-native-fast-image (lib cũ hơn): cũng dùng SDWebImage/Glide, có priority/headers/cache control. Maintain chậm dần khi expo-image phổ biến.

Khi nào dùng image core: ảnh local nhỏ (icon, logo). Khi nào nâng cấp: bất kỳ list/feed/gallery có ảnh remote >50 items, hoặc cần placeholder để chống layout shift.

Pressable (recommend từ RN 0.63+): API hiện đại, một component thay cho cả family Touchable*.

  • Function children có signature ({ pressed }) => ReactNode để render khác state, hoặc style function style={({ pressed }) => [...]} để áp style động.
  • Hỗ trợ onLongPress, delayLongPress, hitSlop, pressRetentionOffset.
tsx
<Pressable
  onPress={...}
  style={({ pressed }) => [styles.btn, pressed && { opacity: 0.6 }]}
  hitSlop={10}
>
  <Text>Submit</Text>
</Pressable>

TouchableOpacity: opacity flash khi press (default 0.2). Ngắn gọn hơn cho case đơn giản.

TouchableHighlight: đổi underlayColor khi press — phù hợp list row.

TouchableWithoutFeedback: không feedback gì — chỉ dùng cho overlay invisible (vd dismiss keyboard).

Quy tắc 2026: default Pressable cho mọi nút mới. Touchable* để legacy code yên — không cần migrate ồ ạt vì performance và hành vi tương đương.

Built-in <Modal>: render qua native modal API (UIViewController iOS, Dialog Android).

  • Ưu điểm: tự xử lý back button Android, status bar, true full-screen overlay vượt qua mọi layer.
  • Nhược: animation hạn chế (slide, fade, none), khó style header/backdrop, prop presentationStyle chỉ iOS.
tsx
<Modal visible={open} animationType="slide" onRequestClose={() => setOpen(false)}>
  <SafeAreaView style={{ flex: 1 }}>{...}</SafeAreaView>
</Modal>

Custom modal qua portal-like pattern: render <View style={StyleSheet.absoluteFill}> ở root layout, animate qua Reanimated. Lib phổ biến: react-native-modal, @gorhom/bottom-sheet. Ưu: animation tự do, gesture-driven (kéo xuống dismiss), backdrop blur, snap point. Nhược: phải manage stacking thủ công, overlay với native UI (vd date picker) có thể bị che.

Quy tắc thực tế:
- Confirm dialog đơn giản: built-in Modal.
- Bottom sheet: @gorhom/bottom-sheet.
- Toast/snackbar: lib khác (react-native-toast-message).
- Full-screen overlay với navigation gesture: react-navigation presentation: 'modal' thay vì Modal component.

JS stack (@react-navigation/stack): toàn bộ animation và transition viết bằng JS qua Reanimated. Linh hoạt: custom transition, modal effect, gesture-driven dismiss. Nhược: animation chạy JS thread → có thể jank nếu thread busy lúc transition.

Native stack (@react-navigation/native-stack): wrap react-native-screens — dùng UINavigationController (iOS) và Fragment + FragmentTransaction (Android) bên dưới. Animation chạy trên native thread, hành vi y hệt navigation native (swipe-back iOS, header transition, large title iOS). Performance tốt hơn, đặc biệt trên Android low-end.

Trade-off:
- Native stack: customization animation hạn chế (chỉ vài preset), header style theo platform.
- JS stack: tự do thiết kế, nhưng phải tự test performance.

Khuyến nghị 2026: mặc định native stack. Chỉ dùng JS stack khi cần animation hoàn toàn custom (vd hero image transition cross-screen). Sự khác biệt rõ trên các app có nhiều screen — push/pop 30+ screen, native stack giữ FPS ổn định, JS stack bắt đầu drop frame.

Deep link cho phép URL bên ngoài (web link, push notification) mở thẳng vào một screen cụ thể trong app.

Bước 1 — Khai báo URL scheme:
- iOS: Info.plist thêm CFBundleURLTypes với scheme custom (myapp://).
- Android: AndroidManifest.xml thêm <intent-filter> cho android.intent.action.VIEW + <data android:scheme="myapp" />.
- Universal Link / App Link (recommend): dùng URL HTTPS thật (https://example.com/...) thay vì scheme custom — cần config Apple App Site Association và Android Asset Links file.

Bước 2 — Cấu hình react-navigation:

tsx
const linking = {
  prefixes: ['myapp://', 'https://example.com'],
  config: {
    screens: {
      HomeTab: { screens: { Detail: 'product/:id' } },
    },
  },
}
<NavigationContainer linking={linking}>...</NavigationContainer>

Link https://example.com/product/42 → mở Detail screen với route.params.id === '42'.

Bước 3 — Test:
- iOS simulator: xcrun simctl openurl booted "myapp://product/42".
- Android: adb shell am start -a android.intent.action.VIEW -d "myapp://product/42".

Pitfall: Universal Link bị tắt nếu user "long-press → copy" link (hành vi iOS). Test trên device thật, click từ Mail/Notes app.

react-navigation v7 cung cấp generic types để type-safe navigation prop và route params.

Khai báo param list:

ts
type RootStackParamList = {
  Home: undefined
  Detail: { productId: string; from?: 'home' | 'search' }
  Profile: { userId: number }
}

Trong screen component:

tsx
import type { NativeStackScreenProps } from '@react-navigation/native-stack'

type Props = NativeStackScreenProps<RootStackParamList, 'Detail'>
function DetailScreen({ route, navigation }: Props) {
  const { productId, from } = route.params // typed
  navigation.navigate('Profile', { userId: 42 }) // typed
}

Tip 2026: declare global type-safe navigation bằng module augmentation:

ts
declare global {
  namespace ReactNavigation {
    interface RootParamList extends RootStackParamList {}
  }
}

Khi đó useNavigation() không cần generic — TypeScript tự suy luận.

navigation.navigate('Detail', { product }) truyền params qua route. Vài pitfall:

1. Object reference vs value: params được serialize ngầm (clone) nội bộ navigation state. Truyền object lớn (nested deep, function, Date) trigger warning "Non-serializable values were found in the navigation state". Hậu quả: state không persist được, dev tools trace bị lỗi.

2. Function trong params: không bao giờ truyền callback onSubmit: (data) => ... qua params — vì serialize fail, và function không stable giữa render. Thay bằng:
- Lift state lên context/store rồi screen sau đọc.
- Dùng navigation.navigate('Prev', { result: x }) từ screen sau quay lại.

3. Stale params: nếu user navigate Detail 2 lần với product khác nhau, react-navigation cache screen instance (theo native stack) → component không remount, useEffect([]) không chạy lại. Phải useEffect([route.params.id], ...) để fetch khi id đổi.

4. Default params: Screen.options không nhận default cho params — phải check route.params?.x ?? defaultX trong component.

5. Deep link params là string: mọi param từ URL là string ('42', không phải 42). Phải parse khi cần number/boolean.

Redux Toolkit (RTK): full-featured. RTK Query thay axios cho server state, devtools tốt nhất, time-travel debugging. Phù hợp app lớn (10+ engineers), cần audit trail rõ ràng. Boilerplate vẫn nặng dù RTK đã giảm so với Redux cổ điển. Trên RN, devtools cần Reactotron hoặc Flipper plugin.

Zustand: API tối giản (hook + closure), không context, không boilerplate. Khoảng 1KB bundle. Dễ test (store là plain object). Persist middleware cho RN dùng MMKV/AsyncStorage adapter.

Jotai: atom-based — chia state thành nhiều atom nhỏ, component subscribe atom nào re-render atom đó. Phù hợp UI nhiều nguồn state độc lập (form tự do, canvas).

Khuyến nghị 2026:
- App nhỏ-trung (≤ 50 screen, ≤ 5 dev): Zustand. Đủ tính năng, vô đối về DX.
- App lớn cần audit, time-travel: RTK + RTK Query.
- UI editor/canvas/form đa ngả: Jotai.
- Server state: TanStack Query kèm bất kỳ store nào ở trên cho client state.

React Query lưu cache trong memory; reload app là mất. Persister cho phép serialize cache vào storage để hydrate khi mở lại app, UI hiển thị data ngay không chờ network.

Setup với MMKV (recommend RN):

ts
import { QueryClient, focusManager } from '@tanstack/react-query'
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister'
import { MMKV } from 'react-native-mmkv'
import { AppState } from 'react-native'

const storage = new MMKV()
const persister = createSyncStoragePersister({
  storage: {
    getItem: (key) => storage.getString(key) ?? null,
    setItem: (key, value) => storage.set(key, value),
    removeItem: (key) => storage.delete(key),
  },
})

const queryClient = new QueryClient({
  defaultOptions: { queries: { staleTime: 5 * 60_000, gcTime: 24 * 60 * 60_000 } },
})

// Bật refetch khi app trở về foreground
focusManager.setEventListener((handleFocus) => {
  const sub = AppState.addEventListener('change', (s) => handleFocus(s === 'active'))
  return () => sub.remove()
})

// Wrap root
export function App() {
  return (
    <PersistQueryClientProvider
      client={queryClient}
      persistOptions={{ persister, maxAge: 24 * 60 * 60_000 }}
    >
      <RootNavigator />
    </PersistQueryClientProvider>
  )
}

Lưu ý:
- focusManager cần custom event vì RN không có window focus.
- onlineManager.setEventListener nối với @react-native-community/netinfo để pause query khi offline.
- Đừng persist sensitive data (token, payment info) — không mã hóa nếu MMKV instance không có encryption key.

Context không có shallow compare — bất kỳ consumer nào cũng re-render khi value object đổi reference, kể cả khi field họ đọc không đổi.

Anti-pattern phổ biến:

tsx
function AuthProvider({ children }) {
  const [user, setUser] = useState(null)
  const [theme, setTheme] = useState('dark')
  // value mới mỗi render → mọi consumer re-render
  return (
    <AuthContext.Provider value={{ user, setUser, theme, setTheme }}>
      {children}
    </AuthContext.Provider>
  )
}

Vấn đề: child chỉ cần theme cũng re-render khi user đổi.

Cách fix:
1. Tách context: AuthContext riêng, ThemeContext riêng. Đơn giản và hiệu quả.
2. Memo hóa value: useMemo(() => ({ user, setUser }), [user]).
3. Tách "value" và "setter" thành 2 context: một cho data (đổi nhiều), một cho action (stable). Pattern Kent C. Dodds đề xuất.
4. Dùng store ngoài (Zustand) thay context cho global state — selector có shallow compare built-in.

Hệ quả trên RN nặng hơn web: mỗi re-render → bridge serialize → UIView update. List 100 row mà có context không tách → mỗi keystroke trong TextInput re-render hết list.

Form lớn (5+ inputs) trong RN thường gặp 2 vấn đề:
1. Controlled state ở cha → mỗi keystroke re-render toàn form, lag visible trên Android low-end.
2. Validation phức tạp (cross-field, async) viết tay rất rối.

react-hook-form giải quyết:
- State giữ trong ref nội bộ, không trong React state — không re-render component cha khi typing.
- Component nào subscribe field đó qua useController hoặc Controller mới re-render.
- Plugin Zod/Yup cho validation declarative.

tsx
import { useForm, Controller } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'

function LoginForm() {
  const { control, handleSubmit } = useForm({ resolver: zodResolver(schema) })
  return (
    <Controller
      control={control}
      name="email"
      render={({ field: { onChange, value } }) => (
        <TextInput value={value} onChangeText={onChange} />
      )}
    />
  )
}

Lựa chọn khác: Formik (cũ, re-render nhiều hơn), TanStack Form (mới, đang phát triển, type-safe hơn). 2026 react-hook-form vẫn là default trong RN ecosystem.

fetch: built-in, đủ dùng cho 80% case. Yếu điểm: không timeout default (phải dùng AbortController), không retry, parse JSON cần .json() rời.

axios: API gọn hơn, transformer built-in, interceptor cho auth header và refresh token, timeout config dễ.

ts
const api = axios.create({ baseURL: 'https://api.example.com', timeout: 10_000 })
api.interceptors.request.use((config) => {
  config.headers.Authorization = `Bearer ${getToken()}`
  return config
})

Retry / backoff: dùng axios-retry hoặc viết qua TanStack Query (built-in retry: 3, retryDelay: exponential).

Offline detection: @react-native-community/netinfo:

ts
import NetInfo from '@react-native-community/netinfo'
NetInfo.addEventListener((state) => console.log(state.isConnected, state.type))

Pattern thường thấy: wrap toàn app trong NetInfoProvider → expose isOnline. TanStack Query onlineManager.setEventListener để pause query khi offline. Hiển thị banner "You are offline" toàn cục. Mutation queue: khi offline, lưu request vào MMKV; khi online lại, replay.

RN-specific gotcha: trên iOS simulator, NetInfo.fetch().isConnected có thể trả true ngay cả khi không có internet thật — luôn test trên device.

FlatList virtualize: chỉ mount items hiện đang trong "window" (vùng nhìn thấy + buffer hai phía), unmount items ngoài window khi scroll. Memory ổn định không phụ thuộc tổng số item.

Tham số quan trọng:

  • windowSize (default 21): số "screenful" được render. 21 nghĩa là 1 screen visible + 10 screen trên + 10 screen dưới. Giảm xuống 5–10 cho list rất dài (10k+) → tiết kiệm RAM.
  • maxToRenderPerBatch (default 10): số item render mỗi batch khi scroll. Tăng (15–20) cho hardware mạnh → giảm trắng khi scroll nhanh; giảm (5) cho hardware yếu → giữ FPS scroll.
  • initialNumToRender (default 10): item render lúc mount đầu. Set bằng số item visible thực tế trên màn hình tiêu chuẩn.
  • getItemLayout: nếu mọi item cao bằng nhau, cung cấp function:
ts
getItemLayout={(_, index) => ({ length: 80, offset: 80 * index, index })}

→ FlatList biết offset trước khi render → scroll đến index xa, jump nhanh, không lag.

  • removeClippedSubviews (default false iOS, true Android): unmount khỏi native view tree khi scroll qua. Đôi khi gây bug ảnh trắng — test kỹ.

Pattern thực tế: với list dài, cân nhắc thay bằng @shopify/flash-list — recycler view, performance gấp 2–3 lần FlatList trên list 10k+.

Quy tắc giống React web nhưng stake cao hơn vì re-render trong RN tốn hơn (bridge/UIView update).

useCallback ổn định reference của function giữa các render. Cần khi:
1. Truyền callback vào React.memo component → tránh unmemoize.
2. Truyền vào FlatList props (renderItem, keyExtractor, onEndReached) — nếu function mới mỗi render, FlatList re-render hết items visible.
3. Dùng làm dependency của useEffect — function mới mỗi render gây effect chạy lại liên tục.

useMemo cache giá trị tính toán đắt:
1. Filter/sort/transform array lớn (>1000 items).
2. Tạo object/array làm prop cho React.memo component.
3. Compute style object phức tạp.

Khi nào KHÔNG cần:
- Function/value dùng nội bộ component, không pass cho memoized child → useCallback/useMemo cũng tốn memory để cache.
- Compute primitive đơn giản (x * 2, string concat ngắn) — overhead memo lớn hơn lợi ích.

Cảnh báo: useCallback không "ngăn re-render" của component cha — nó chỉ ổn định reference. Component cha vẫn re-render khi state nó đổi; chỉ child bọc React.memo mới hưởng lợi.

Hermes là default từ RN 0.70 (Android) và 0.71 (iOS). Dự án mới Expo SDK 50+ và bare CLI đều bật sẵn.

Bật/tắt Hermes:
- Expo: app.json"jsEngine": "hermes" (hoặc "jsc"). Build qua EAS.
- Bare:
- android/gradle.propertieshermesEnabled=true.
- ios/Podfile:hermes_enabled => true. Sau đó pod install.

Đo improvement:

1. Time-to-Interactive (TTI):

ts
import { Performance } from 'react-native'
   // hoặc: const start = global.performance.now()

So sánh TTI giữa Hermes vs JSC build → thường thấy giảm 30–50% trên Android low-end.

2. App size: check .apk/.ipa size. Hermes build thường nhỏ hơn 20–40% nhờ bytecode + bỏ bundle JS plain text.

3. Memory: Android Studio Profiler hoặc Xcode Instruments → so sánh peak memory. Hermes tiết kiệm 20–30%.

4. JS bundle parse time: Trace bằng RN DevTools Performance tab hoặc Systrace (Android). Hermes skip parse step hoàn toàn — bytecode load thẳng vào engine.

Khi nào tắt: chỉ khi gặp lib không tương thích (rất hiếm 2026). Đa số legacy package đã update để hỗ trợ Hermes.

InteractionManager là API cũ trong RN cho phép defer task nặng cho đến khi animation/transition hoàn thành. Mục đích: tránh block animation, giữ FPS smooth lúc transition.

Use case kinh điển:

tsx
function DetailScreen() {
  const [data, setData] = useState(null)
  useEffect(() => {
    // Đừng fetch ngay — wait until navigation animation done
    const handle = InteractionManager.runAfterInteractions(() => {
      fetchHeavyData().then(setData)
    })
    return () => handle.cancel()
  }, [])
}

Khi user navigation.navigate('Detail'), animation slide chạy 300ms. Nếu fetch chạy ngay, JSON.parse + parse data có thể block frame → animation jank. runAfterInteractions đẩy fetch xuống sau animation.

Khi nào KHÔNG cần (2026):
- Native stack từ react-navigation chạy animation trên native thread → JS work không block.
- Reanimated chạy worklet trên UI thread → animation độc lập với JS.
- TanStack Query với useQuery đã có cơ chế suspend/loading state đẹp hơn.

Modern alternative: dùng useTransition từ React 18+:

tsx
const [pending, startTransition] = useTransition()
startTransition(() => setHeavyData(...))

Trong RN với New Architecture + Fabric, concurrent rendering giúp transition tự nhiên smooth hơn — InteractionManager trở thành legacy.

List nhiều ảnh thường gặp:

1. Layout shift do ảnh chưa load: ảnh remote không có intrinsic size → khi load xong, item thay đổi chiều cao → list jump. Fix: set width/height cứng hoặc aspectRatio.

2. White flash trước khi ảnh load: dùng placeholder. Với expo-image:

tsx
<Image
  source={{ uri }}
  placeholder={{ blurhash: 'L6PZfSi_.AyE_3t7t7R**0o#DgR4' }}
  transition={200}
/>

Blurhash là string ngắn (28 chars) encode preview blur — encode lúc upload server, decode trong app gần như miễn phí.

3. Prefetch ảnh sắp scroll tới:

ts
import { Image } from 'expo-image'
await Image.prefetch([url1, url2, url3]) // download trước, vào disk cache

Gọi trong onEndReached của FlatList khi user scroll gần cuối — pre-fetch trang sau.

4. Thumbnail/full-res hai-bước: load ảnh nhỏ trước, swap sang full-res khi sẵn sàng. expo-image có recyclingKey + priority hỗ trợ pattern này.

5. Memory blow-up: ảnh 4K hiển thị 100×100 vẫn decode full size vào RAM. Fix: server return URL có ?w=200&h=200 hoặc dùng image CDN (Cloudinary, imgix). Trên client, resizeMode="cover" không downsample — phải dùng expo-image với transition={...} và prop contentFit (chỉ format hiển thị).

6. Cache invalidation: key uri cần stable. Nếu URL đổi mỗi request (signed URL có timestamp), cache không hit. Dùng cachePolicy="memory-disk" + tách signing parameter.

Mặc định trong RN cũ, mỗi screen của react-navigation render thành một <View> trong cùng cây React. Tất cả screen trong stack đều mount → giữ nguyên React tree → tốn memory và slow xuống khi navigate sâu.

enableScreens() (default true từ react-navigation v6+) bật react-native-screens package — wrap mỗi screen trong native screen container (UIViewController iOS, Fragment Android). Lợi ích:

1. OS-managed memory: screen không visible bị OS detach native views → giảm memory footprint.
2. Native back gesture iOS: swipe-back hoạt động đúng (built-in iOS UINavigationController behavior).
3. Native large title iOS: large title scroll-collapse animation chạy native.
4. Performance navigate: transition trên native thread, JS thread tự do.
5. Required cho native-stack: @react-navigation/native-stack mandate react-native-screens.

Setup:

ts
import { enableScreens } from 'react-native-screens'
enableScreens()

Gọi một lần ở top file index.js/App.tsx trước khi import navigation.

Pitfall: vài lib cũ (vd react-native-keyboard-aware-scroll-view) bị bug khi screen detach. 2026 đa số lib đã sync compat. Nếu cần debug, enableScreens(false) để verify problem có liên quan không.

New Architecture là tên gọi chung cho 3 phần Meta viết lại RN từ năm 2020:

1. JSI (JavaScript Interface): C++ layer thay Bridge cũ. JS engine có tham chiếu trực tiếp tới object C++/native — gọi sync, không serialize JSON.

2. TurboModules: thay native modules cũ. Load lazy (chỉ khi gọi đến), type-safe qua Codegen, gọi qua JSI sync.

3. Fabric: renderer mới thay UIManager cũ. Concurrent rendering từ React 18 hoạt động đúng (Suspense, transitions, automatic batching). Layout chạy trong C++ (Yoga + ShadowTree), commit đồng bộ.

Thêm Bridgeless mode (RN 0.74+): bỏ hoàn toàn Bridge legacy, tất cả native interop qua JSI. Đây là đỉnh điểm New Arch.

Mặc định:
- RN 0.68 (2022): opt-in lần đầu, vẫn buggy, chưa khuyến nghị production.
- RN 0.71 (2023): ổn hơn, vài app lớn (Discord, Microsoft) chuyển.
- RN 0.74 (2024): Bridgeless ra mắt.
- RN 0.76 (2024-Q4): default cho project mới qua npx @react-native-community/cli@latest init. Project cũ vẫn opt-in được.
- RN 0.77+ (2025): old architecture deprecated, sẽ removed dần.

Tác động dev: API JS hầu hết không đổi. Pain point chính là migrate native modules cũ không tương thích — phải refactor sang TurboModule spec.

Quy tắc: luôn ưu tiên npm package trước. Lý do: lib có sẵn được test trên nhiều device, có CI cross-platform, được community fix bug. Tự viết = bạn own toàn bộ maintenance.

Khi cần viết native module:

1. Feature platform mới chưa có lib: vd iOS Live Activity (App Intents), Android Predictive Back gesture, Apple Watch companion. Lib mất 6–12 tháng để theo kịp.

2. Wrap SDK third-party native: Stripe, Firebase Performance, Sentry, AdMob, Branch.io. Đa số có official RN wrapper, nhưng SDK ngách (vendor riêng, banking SDK) thì phải tự wrap.

3. Performance critical: image processing real-time, video filter, ML inference, audio DSP. Tính toán nặng JS không kham, native (Metal/Vulkan/Core ML) cần.

4. Đụng platform-specific permission/hardware: Bluetooth Low Energy custom protocol, NFC ghi/đọc tag riêng, USB serial, secure element.

5. Existing native code legacy: team cũ có codebase Swift/Kotlin lớn → wrap thành module cho đỡ rewrite.

Trước khi commit viết:
- Search npm + GitHub + RN directory (reactnative.directory).
- Search Expo Modules — nhiều case Expo đã expose API platform mới.
- Issue trong lib hiện có thường được fix trong vài tuần — cân nhắc wait/contribute thay vì fork.

Build vs buy reality check: native module mất 1–2 tuần viết + maintain dài hạn. Lib có sẵn $0 + community support. Chỉ tự viết khi không thể tránh.

Expo Modules API là layer abstraction cao hơn TurboModule do Expo team viết. Cùng mục đích (gọi native từ JS) nhưng API gọn hơn nhiều, đặc biệt khi viết Swift/Kotlin idiomatic.

TurboModule (RN core):
- Spec TypeScript bắt buộc.
- Native code phải kế thừa class generated.
- Codegen step manual.
- Hỗ trợ Old + New Architecture.

Expo Modules:
- Định nghĩa module bằng DSL Swift/Kotlin:

swift
// ios/MyModule/MyModule.swift
import ExpoModulesCore

public class MyModule: Module {
  public func definition() -> ModuleDefinition {
    Name("MyModule")
    Function("greet") { (name: String) -> String in
      "Hello, \(name)!"
    }
    AsyncFunction("fetchData") { (url: String) async throws -> [String: Any] in
      // ...
    }
  }
}
kotlin
// android/src/main/java/expo/modules/mymodule/MyModule.kt
class MyModule : Module() {
  override fun definition() = ModuleDefinition {
    Name("MyModule")
    Function("greet") { name: String -> "Hello, $name!" }
  }
}

JS side đơn giản:

ts
import { requireNativeModule } from 'expo-modules-core'
const MyModule = requireNativeModule('MyModule')
MyModule.greet('World')

Lợi điểm Expo Modules:
- Không cần spec TypeScript riêng (TS type inference từ DSL).
- Hỗ trợ event emitter, lifecycle, view component (cho Fabric) tích hợp sẵn.
- Unit test dễ hơn (mock DSL gọn).
- Expo team duy trì compat với RN versions.

Nhược:
- Phụ thuộc expo-modules-core ngay cả trong bare workflow (cần npx install-expo-modules).
- Ít control low-level so với TurboModule.

Khi nào chọn:
- Đang dùng Expo (managed hoặc bare): mặc định Expo Modules.
- Lib RN community thuần, không Expo: TurboModule.
- Performance siêu critical (audio DSP, ML): TurboModule + JSI custom (kiểm soát layer thấp hơn).

Animated API (built-in RN core):
- Driver useNativeDriver: true đẩy animation lên native thread, smooth.
- Hạn chế: chỉ animate được layout/style không đụng tới layout (opacity, transform, backgroundColor); animate width/height/top phải useNativeDriver: false → JS thread, dễ jank.
- API verbose: Animated.timing, Animated.Value, interpolate chuỗi dài.

Reanimated 3 (recommend 2026):
- Worklet chạy trên UI thread qua JSI — animation độc lập với JS thread, không bao giờ bị block bởi render React.
- API hook-based gọn:

tsx
const offset = useSharedValue(0)
const style = useAnimatedStyle(() => ({
  transform: [{ translateX: withSpring(offset.value) }],
}))

<Animated.View style={style} />
<Pressable onPress={() => offset.value = 200}>...</Pressable>
  • Animate được mọi prop (width, height, scroll offset).
  • withSpring, withTiming, withSequence, withRepeat compose dễ.
  • useDerivedValue, useAnimatedReaction cho logic phức tạp.
  • Layout animation built-in: entering, exiting, layout props.

Quy tắc: Reanimated 3 cho mọi animation mới. Animated API chỉ cho legacy hoặc lib bắt buộc (ScrollView event onScroll cũ). Reanimated bundled trong Expo SDK 50+.

PanResponder (built-in):
- Mỗi gesture event đi qua bridge → JS xử lý → có lag.
- Không support gesture native (long-press riêng cho iOS, fling Android).
- Conflict với ScrollView/Modal hay xảy ra (event không propagate đúng).

react-native-gesture-handler (RNGH):
- Gesture detect và state machine chạy trên UI thread (native code).
- Cooperate native với scroll, pinch, rotate.
- API hook-based hợp với Reanimated:

tsx
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
import Animated, { useSharedValue, useAnimatedStyle } from 'react-native-reanimated'

const x = useSharedValue(0)
const pan = Gesture.Pan()
  .onChange((e) => { x.value += e.changeX })
  .onEnd(() => { x.value = withSpring(0) })

const style = useAnimatedStyle(() => ({
  transform: [{ translateX: x.value }],
}))

<GestureDetector gesture={pan}>
  <Animated.View style={style} />
</GestureDetector>

Lợi ích:
- 60+ FPS gesture, không lag.
- Compose multi-gesture: Gesture.Race(pan, pinch), Gesture.Simultaneous(...).
- Cancel/finalize lifecycle rõ ràng (.onBegin, .onUpdate, .onEnd, .onFinalize).
- Tích hợp react-navigation: swipe-back, drawer mở/đóng.

Setup:
- Cài react-native-gesture-handler.
- iOS: tự setup qua autolinking.
- Android: MainActivity.kt extends ReactActivity, override createReactActivityDelegate để add gesture handler.
- Wrap app trong <GestureHandlerRootView style={{ flex: 1 }}> ở root.

Expo managed (npx create-expo-app):
- Xcode/Android Studio không cần cài (build qua EAS Cloud).
- Native config qua app.json + Expo plugins, không động ios//android/ folder.
- Hot reload và OTA update mặc định.
- Hạn chế: chỉ dùng module trong Expo SDK + module có Expo plugin. Native module custom phải eject hoặc dùng dev client.

Expo bare workflow (npx expo prebuild từ managed, hoặc npx create-expo-app --template bare-minimum):
- Có folder ios/android/ thật, edit native code tự do.
- Vẫn dùng Expo modules ecosystem (expo-image, expo-router, expo-updates, ...).
- EAS Build vẫn hoạt động.
- "Best of both worlds" — recommend cho đa số dự án 2026.

Vanilla RN CLI (npx @react-native-community/cli@latest init):
- Pure RN, không Expo dependency.
- Toàn quyền kiểm soát, nhưng phải tự setup nhiều thứ (linking, config, splash screen, icons).
- Dùng khi: team đã có infrastructure native lớn, hoặc lib core cần feature pre-Expo.

Quyết định 2026:
- App mới, MVP → Expo managed, prebuild khi cần.
- App production scale, team mid-large → Expo bare.
- App enterprise có CI/CD native riêng, không muốn EAS lock-in → Vanilla CLI.

Expo SDK 52 (RN 0.76, cuối 2024) đặt New Architecture làm default — không còn lý do tránh Expo vì "performance" như trước. SDK <52 vẫn dùng Old Arch nếu chưa migrate.

Metro là bundler default của RN, viết bởi Meta. Tối ưu cho dev experience mobile (hot reload nhanh, source map cho native).

Khác Webpack:

MetroWebpack
TargetRN bundle (JS+assets cho mobile)Web bundle (JS+CSS+HTML)
EntryMột entry duy nhấtMulti-entry chunking
TransformBabel onlyBabel/SWC/esbuild loader
HMRFast Refresh tích hợpHMR plugin riêng
Tree shakingHạn chế (tốt hơn từ 0.73+)Mạnh, sideEffects flag
Code splittingKhông (single bundle)Built-in dynamic import
Source mapNative + JS combinedJS only

Config (metro.config.js):

js
const { getDefaultConfig } = require('expo/metro-config')

const config = getDefaultConfig(__dirname)
config.resolver.assetExts.push('lottie', 'glb') // custom asset
config.transformer.babelTransformerPath = require.resolve('react-native-svg-transformer')
module.exports = config

Cải tiến 2026:
- Metro 0.81+ tree-shaking ESM tốt hơn (giảm bundle size 10–20%).
- Lazy bundle loading — bundle splitting cho RN screens.
- Hermes-aware optimization (drop unused worklets).

Khi nào thay Metro:
- Hiện tại không có alternative production-ready.
- re.pack (project Callstack) port Webpack cho RN — dùng cho monorepo lớn cần code splitting cross-bundle, nhưng còn experimental.

Build local:
- iOS: cần macOS + Xcode license.
- Android: cần JDK + Android Studio + SDK.
- Build time: 5–15 phút mỗi lần.
- Code signing: tự manage cert/profile (iOS) và keystore (Android).
- Pros: zero cost, debug build nhanh hơn (cache local), không phụ thuộc internet.
- Cons: Mac-only cho iOS, manual setup, signing rủi ro nếu key mất.

EAS Build (Expo Application Services):
- Cloud build, không cần macOS local cho iOS.
- Profile config (eas.json) cho dev/preview/production.
- Code signing managed: EAS lưu cert/keystore + auto rotate.
- Pros: setup 1 lần dùng forever, share build với team qua EAS Update channel, tích hợp OTA, free tier 30 build/month.
- Cons: build thường 5–10 phút (cloud overhead), priority queue free tier có thể chờ; cost paid tier $99/month/team.

Decision matrix:
- Indie dev, không có Mac → EAS Build.
- Team có infrastructure CI riêng (Jenkins, GitHub Actions self-hosted Mac) → build local hoặc CI custom.
- Startup nhanh launch, ưu tiên dev velocity → EAS Build + EAS Update.
- Enterprise nghiêm ngặt về security (code không leave premises) → build local hoặc EAS Self-hosted.

Hybrid: dùng EAS cho preview/dev, local cho production release — kiểm soát signing key cuối cùng.

OTA (Over-The-Air) update cho phép push JS bundle mới mà không cần build/upload App Store/Play Store. User mở app → background fetch bundle → hot-swap.

Microsoft CodePush (legacy):
- Tool đầu tiên phổ biến (~2015).
- Deprecated: Microsoft thông báo retire CodePush tháng 3/2025. Migrate sang EAS Update hoặc tự host.

Expo Updates (expo-updates library):
- Built-in trong Expo bare/managed.
- Self-host: bundle + manifest serve qua S3/Cloudflare → app fetch.
- Free, control hoàn toàn, nhưng phải tự build pipeline.

EAS Update (recommend 2026):
- Managed service từ Expo team.
- Channel-based: production, staging, preview → release per channel.
- Rollback 1-click qua dashboard.
- Branch theo runtime version (đổi native code phải rebuild app).
- Tích hợp EAS Build → bundle build → publish update cùng pipeline.
- Free tier: 1000 MAU; paid $99/month team với unlimited.

Pattern triển khai:

ts
import * as Updates from 'expo-updates'

async function checkForUpdate() {
  const update = await Updates.checkForUpdateAsync()
  if (update.isAvailable) {
    await Updates.fetchUpdateAsync()
    await Updates.reloadAsync()
  }
}

Pitfall:
- OTA chỉ update JS bundle + assets. Native code (ios//android/) đổi → phải build mới + submit store.
- App Store policy: OTA cho phép, nhưng không được dùng để bypass review (vd thay đổi feature lớn → reject).
- Test rollback trên production trước khi push global.

@testing-library/react-native (RNTL) cung cấp API tương tự @testing-library/react (web) nhưng adapt cho RN component tree.

Setup:

bash
pnpm add -D jest @testing-library/react-native @testing-library/jest-native

jest.config.js:

js
module.exports = {
  preset: 'react-native',
  setupFilesAfterEnv: ['@testing-library/jest-native/extend-expect'],
  transformIgnorePatterns: [
    'node_modules/(?!(react-native|@react-native|@react-navigation|expo|@expo)/)',
  ],
}

Test ví dụ:

tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react-native'
import { LoginForm } from './LoginForm'

test('submit calls onLogin with email', async () => {
  const onLogin = jest.fn()
  render(<LoginForm onLogin={onLogin} />)

  fireEvent.changeText(screen.getByPlaceholderText('Email'), 'a@b.com')
  fireEvent.changeText(screen.getByPlaceholderText('Password'), 'pass1234')
  fireEvent.press(screen.getByRole('button', { name: /sign in/i }))

  await waitFor(() => expect(onLogin).toHaveBeenCalledWith('a@b.com'))
})

Best practices:
- Query bằng accessibility role/label (getByRole, getByLabelText) thay vì testID — tăng coverage a11y luôn.
- Mock native module qua jest.mock('react-native-mmkv', () => ({ MMKV: jest.fn() })).
- Mock navigation: jest.mock('@react-navigation/native') rồi mock useNavigation.
- Snapshot test ít dùng — dễ false negative khi UI legitimate đổi.

Coverage realistic: focus business logic + form validation + navigation flow. Skip pixel-perfect visual (dùng visual regression test thay).

Detox (Wix):
- E2E test framework cho RN/iOS/Android, viết bằng JS/TS.
- Cài đặt phức tạp: cần custom build (detox-cli), config simulator/emulator, sync với JS thread qua RN.
- Mạnh: tích hợp sâu RN (sync với animations, async ops), assertion mạnh.
- Yếu: setup khó, fail flaky khi animation hoặc timing không đoán được, chậm chạy 5–10 phút mỗi suite.

Maestro (Mobile.dev, recommend 2026):
- E2E framework đa nền tảng (iOS, Android, RN, native, Flutter), viết bằng YAML.
- Cài đặt 1 lệnh, không cần custom build.
- Mạnh: stability cao, retry tự động, syntax đơn giản, chạy nhanh.
- Yếu: ít control logic phức tạp (không phải JS), debug session khó hơn Detox.

Maestro test ví dụ (flow.yaml):

yaml
appId: com.myapp
---
- launchApp
- tapOn: "Sign in"
- inputText: "a@b.com"
- tapOn: "Password"
- inputText: "pass1234"
- tapOn: "Submit"
- assertVisible: "Welcome"

Quyết định 2026:
- Maestro là default. DX vượt trội, ít maintenance.
- Detox vẫn dùng khi: cần test logic phức tạp (intercept network, mock state), tích hợp sâu với JS test (chia sẻ helper với Jest).
- Cả hai: vài team chạy Maestro cho smoke tests + Detox cho edge case logic.

Maestro Cloud cho parallel run trên device farm — tích hợp CI dễ.

Flipper là tool debug cũ do Meta xây cho RN — desktop app có console, network inspector, layout inspector, plugin ecosystem (database, AsyncStorage). Deprecated từ RN 0.74+ vì:
- Khó maintain cross-platform (Java/Obj-C bridge).
- Chậm và crash thường xuyên trên Apple Silicon.
- Plugin ecosystem fragmented.

RN DevTools (default từ RN 0.76+):
- Web-based, mở qua browser từ Metro CLI (nhấn j trong terminal).
- Built trên Chrome DevTools Protocol — quen thuộc với web dev.
- Tính năng:
- Console: log JS, native warning, error stack với source map.
- Network: inspect fetch/axios request, response body, headers.
- React DevTools: component tree, props/state, profiler.
- Hermes Sources: breakpoint trong JS bundle, step-through debug.
- Memory: heap snapshot, allocation timeline.
- Performance: CPU profile, thread timeline.
- Tích hợp ngay với Hermes — không cần plugin riêng.

Cách dùng:

bash
pnpm start  # khởi Metro
# Trong terminal: nhấn j → mở RN DevTools trong Chrome/Edge

Hoặc shake device → "Open DevTools".

Hạn chế so với Flipper:
- Không có database inspector (tự dùng MMKV viewer hoặc DB tool ngoài).
- Không có layout inspector visual giống Flipper (dùng React DevTools "Inspect Element" thay).
- Plugin ecosystem mới, ít hơn Flipper cũ.

Migrate khỏi Flipper: RN 0.76+ tự động không setup Flipper. Project cũ: xóa Flipper-* pod (iOS), xóa Flipper deps trong gradle.

Snapshot test = render component, serialize tree thành text/JSON, lưu file .snap.

  • Chạy lại sau, so với snapshot cũ.
  • Khác → fail.
tsx
test('Button renders correctly', () => {
  const tree = render(<Button label="Submit" />).toJSON()
  expect(tree).toMatchSnapshot()
})

Hữu ích khi:
- Component thuần presentation (Avatar, Badge, Tag) — UI ít đổi, snapshot bắt regression vô tình.
- Lib component public — ngăn maintainer accidentally break public API shape.
- Test pure rendering không có business logic — verify "đầu ra" không thay đổi sau refactor.

Hại khi:
- False positive cao: UI legitimate đổi (đổi padding, đổi label) → snapshot fail → dev quen tay --updateSnapshot không nhìn diff → không thật sự verify gì.
- Diff không readable: snapshot dài 200 dòng → reviewer skip qua → không catch bug.
- Coverage giả: đạt 80% coverage qua snapshot không đảm bảo behavior đúng — chỉ đảm bảo render giống lần trước.
- Couple với implementation: đổi một class name → fail; refactor không ảnh hưởng UX → fail.

Quy tắc thực tế:
- Avoid snapshot cho integration test, screen-level test.
- Prefer behavior-driven test với getByRole, getByText từ RNTL.
- Nếu phải snapshot: inline snapshot (.toMatchInlineSnapshot()) — diff ngay trong code, dễ review hơn file .snap.
- Visual regression (Percy, Chromatic) tốt hơn snapshot cho UI test thực sự.

Industry trend 2026: snapshot test giảm dùng — focus vào behavior + visual regression + E2E.

react-native-permissions là lib chuẩn cho iOS/Android permission.

API thống nhất:

ts
import { check, request, PERMISSIONS, RESULTS } from 'react-native-permissions'

async function requestCamera() {
  const permission = Platform.select({
    ios: PERMISSIONS.IOS.CAMERA,
    android: PERMISSIONS.ANDROID.CAMERA,
  })!

  const status = await check(permission)

  switch (status) {
    case RESULTS.UNAVAILABLE:
      // device không có camera
      break
    case RESULTS.DENIED:
      // chưa được hỏi → request lần đầu
      const result = await request(permission)
      return result === RESULTS.GRANTED
    case RESULTS.GRANTED:
      return true
    case RESULTS.BLOCKED:
      // user từ chối permanent → mở Settings
      Linking.openSettings()
      break
  }
}

Setup config:
- iOS: Info.plist thêm key NSCameraUsageDescription, NSLocationWhenInUseUsageDescription, ... với mô tả lý do (App Store reject nếu thiếu hoặc generic).
- Android: AndroidManifest.xml thêm <uses-permission>. Android 6+ cần runtime request, lib tự handle.

Pattern UX 2026:
1. Pre-prompt: trước khi popup native, hiển thị custom modal explain why → tăng grant rate.
2. Just-in-time request: chỉ request khi feature cần (vd request camera khi user nhấn "Take photo", không request lúc app start).
3. Graceful fallback: nếu denied, app vẫn hoạt động không crash; chỉ hide feature liên quan.
4. Settings deeplink: với BLOCKED, deep link vào Settings app cho user enable manual.

iOS 14+ specific:
- PERMISSIONS.IOS.LOCATION_WHEN_IN_USE vs LOCATION_ALWAYS — request when-in-use trước, sau đó upgrade lên always khi cần background.
- App Tracking Transparency (PERMISSIONS.IOS.APP_TRACKING_TRANSPARENCY) — bắt buộc nếu dùng IDFA.

RN expose accessibility props từ iOS VoiceOver và Android TalkBack để screen reader mô tả UI cho user khiếm thị.

Props chính:
- accessible={true}: bật accessibility cho component (mặc định false cho View, true cho Text/Button).
- accessibilityLabel: text screen reader đọc. Override children text khi cần ngắn gọn hoặc rõ nghĩa hơn.
- accessibilityRole: vai trò UI (button, link, header, image, checkbox, radio, switch, ...). Screen reader đọc role + state (vd "checkbox, checked").
- accessibilityHint: hint thêm về hành động (vd "Submits the form").
- accessibilityState: { selected, disabled, checked, busy, expanded }.
- accessibilityValue: cho slider/progress bar ({ min, max, now, text }).

Ví dụ:

tsx
<Pressable
  onPress={handleLike}
  accessible
  accessibilityRole="button"
  accessibilityLabel={liked ? 'Unlike post' : 'Like post'}
  accessibilityState={{ selected: liked }}
  accessibilityHint="Toggles like status for this post"
>
  <Icon name={liked ? 'heart-filled' : 'heart-outline'} />
</Pressable>

Test screen reader:
- iOS: Settings → Accessibility → VoiceOver → bật. Triple-tap home button để toggle.
- Android: Settings → Accessibility → TalkBack → bật.
- Swipe để navigate, double-tap để activate, two-finger swipe để scroll.

Pitfall:
- Decorative icons không nên có accessibilityLabel → set accessible={false} hoặc accessibilityElementsHidden={true}.
- Group label: dùng accessibilityLabel ở parent View với importantForAccessibility="yes-exclude-descendants" (Android) hoặc accessibilityElementsHidden cho children (iOS).
- Dynamic text (count, time) — ensure label cập nhật theo state.
- Contrast: dùng useColorScheme + design system token.

Tools:
- eslint-plugin-react-native-a11y cho lint warnings.
- iOS Accessibility Inspector (Xcode) audit visual.

Library lựa chọn:
- react-i18next: mạnh nhất, ecosystem React lớn. Hỗ trợ namespace, plural, interpolation, lazy load translation file.
- i18n-js (cũ, FormatJS-style): nhẹ hơn, ít feature, vẫn dùng nhiều legacy.
- expo-localization: detect locale từ device, dùng kèm react-i18next.

Setup react-i18next:

ts
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
import * as Localization from 'expo-localization'
import en from './locales/en.json'
import vi from './locales/vi.json'

i18n.use(initReactI18next).init({
  resources: { en: { translation: en }, vi: { translation: vi } },
  lng: Localization.locale.split('-')[0],
  fallbackLng: 'en',
  interpolation: { escapeValue: false },
})

Component:

tsx
import { useTranslation } from 'react-i18next'

function Welcome() {
  const { t, i18n } = useTranslation()
  return (
    <>
      <Text>{t('welcome.title', { name: 'Linh' })}</Text>
      <Pressable onPress={() => i18n.changeLanguage('vi')}>
        <Text>VI</Text>
      </Pressable>
    </>
  )
}

RTL (Arabic, Hebrew):

ts
import { I18nManager } from 'react-native'
import * as Updates from 'expo-updates'

async function setRTL(rtl: boolean) {
  if (I18nManager.isRTL !== rtl) {
    I18nManager.allowRTL(rtl)
    I18nManager.forceRTL(rtl)
    await Updates.reloadAsync()  // RN cần reload để apply RTL
  }
}

Khi I18nManager.isRTL === true:
- Mọi flexbox tự đảo (row → row-reverse).
- paddingLeft/paddingRight đổi thành paddingStart/paddingEnd để symmetric.
- Icon directional (back arrow) phải tự flip với transform: [{ scaleX: -1 }].
- Test cẩn thận text alignment, image asset (vd checkmark cũng có thể cần mirror).

Pitfall:
- Số/ngày format: dùng Intl.NumberFormat / Intl.DateTimeFormat thay self-format.
- App restart sau đổi RTL — báo user trước.
- iOS Right-to-Left language: nếu user device ngôn ngữ RTL nhưng app chỉ support LTR, set I18nManager.forceRTL(false) để override.

Hai dịch vụ native cần biết:
- APNs (Apple Push Notification service): iOS push.
- FCM (Firebase Cloud Messaging): Android push, có thể proxy iOS qua APNs.

Option 1 — Direct (@react-native-firebase/messaging + APNs):
- Setup phức tạp: cần Firebase project, push cert/auth key APNs, token registration.
- Mạnh: full control, không lock vào Expo, miễn phí (chỉ trả cho FCM enterprise).
- Code:

ts
import messaging from '@react-native-firebase/messaging'

const token = await messaging().getToken() // gửi lên backend
messaging().onMessage(async (msg) => console.log('foreground', msg))
messaging().setBackgroundMessageHandler(async (msg) => console.log('background', msg))

Option 2 — Expo Notifications (recommend cho Expo project):
- Wrapper hoàn chỉnh quanh APNs + FCM.
- API thống nhất, không cần care platform khác biệt.
- Có thể dùng Expo push service (FCM/APNs proxy) miễn phí, hoặc dùng FCM/APNs trực tiếp.

ts
import * as Notifications from 'expo-notifications'

const { status } = await Notifications.requestPermissionsAsync()
const token = (await Notifications.getExpoPushTokenAsync()).data
// Gửi token lên backend, backend gọi Expo push API hoặc FCM

Notifications.addNotificationReceivedListener((notification) => {...})
Notifications.addNotificationResponseReceivedListener((response) => {
  // user tap notification → navigate vào screen
})

Backend send (Expo example):

ts
await fetch('https://exp.host/--/api/v2/push/send', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    to: expoPushToken,
    title: 'New message',
    body: 'Hello!',
    data: { screen: 'Chat', chatId: 42 },
  }),
})

Pitfall:
- iOS: cần aps-environment entitlement (development cho dev build, production cho TestFlight/App Store).
- Android 13+: cần permission POST_NOTIFICATIONS runtime.
- Foreground vs background handler khác nhau — test cả hai trạng thái.
- Token có thể expire (user reinstall app) — backend phải handle invalid token error gracefully.