Luyện Phỏng Vấn IT — 2000+ Câu Hỏi Phỏng Vấn IT Có Đáp Án 2026
React 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> là 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).
<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.
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 defaultValue và onChangeText cache giá trị vào ref ngoài, không bind vào React state.
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
flexDirectionlàcolumn(web làrow). Cần nhớ:<View>xếp children theo chiều dọc mặc định. flex: 1viết tắt choflexGrow: 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ó
autocho margin trong vài context cũ —marginLeft: 'auto'để đẩy phải hoạt động trong flex. alignSelf: stretchlà 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:
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.
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.
Platform.OS / Platform.select — khi nào dùng, khi nào tách file .ios.tsx / .android.tsx?Inline check (Platform.OS / Platform.select) dùng cho khác biệt nhỏ trong cùng một component:
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={...}>.
SafeAreaView built-in vs react-native-safe-area-context — vì sao team thường thay built-in?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:
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"trongAndroidManifest.xmlxử lý việc này tự động.
Pattern thực tế cross-platform:
<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 functionstyle={({ pressed }) => [...]}để áp style động. - Hỗ trợ
onLongPress,delayLongPress,hitSlop,pressRetentionOffset.
<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, proppresentationStylechỉ iOS.
<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:
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:
type RootStackParamList = {
Home: undefined
Detail: { productId: string; from?: 'home' | 'search' }
Profile: { userId: number }
}Trong screen component:
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:
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):
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:
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.
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ễ.
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:
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.
windowSize/maxToRenderPerBatch/getItemLayout ra sao?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.21nghĩ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:
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(defaultfalseiOS,trueAndroid): 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.properties → hermesEnabled=true.
- ios/Podfile → :hermes_enabled => true. Sau đó pod install.
Đo improvement:
1. Time-to-Interactive (TTI):
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:
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+:
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:
<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:
import { Image } from 'expo-image'
await Image.prefetch([url1, url2, url3]) // download trước, vào disk cacheGọ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:
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:
// 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
// ...
}
}
}// 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:
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:
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,withRepeatcompose dễ.useDerivedValue,useAnimatedReactioncho logic phức tạp.- Layout animation built-in:
entering,exiting,layoutprops.
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:
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/ và 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:
| Metro | Webpack | |
|---|---|---|
| Target | RN bundle (JS+assets cho mobile) | Web bundle (JS+CSS+HTML) |
| Entry | Một entry duy nhất | Multi-entry chunking |
| Transform | Babel only | Babel/SWC/esbuild loader |
| HMR | Fast Refresh tích hợp | HMR plugin riêng |
| Tree shaking | Hạn chế (tốt hơn từ 0.73+) | Mạnh, sideEffects flag |
| Code splitting | Không (single bundle) | Built-in dynamic import |
| Source map | Native + JS combined | JS only |
Config (metro.config.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 = configCả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:
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:
pnpm add -D jest @testing-library/react-native @testing-library/jest-nativejest.config.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ụ:
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):
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:
pnpm start # khởi Metro
# Trong terminal: nhấn j → mở RN DevTools trong Chrome/EdgeHoặ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.
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:
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ụ:
<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:
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:
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):
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:
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.
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):
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.
RN (2026): dùng JS/TS, render qua native UI components. Hot reload nhanh nhất, hệ sinh thái npm lớn nhất, share code với web React thuận lợi. Ecosystem có Expo, react-navigation, Reanimated 3, RN-Skia. New Architecture default → performance gần native với danh sách 60–120fps. Yếu hơn native ở: animation phức tạp custom render, integration sâu native API mới (App Clips, Live Activities iOS), build size.
Flutter (2026): dùng Dart, render qua Skia/Impeller engine — không gọi native UI mà tự vẽ pixel. UI identical trên cả hai platform; performance animation tốt nhất trong cross-platform. Kém hơn ở: file size lớn (~7MB baseline), Dart hệ sinh thái nhỏ hơn npm, accessibility chưa bằng RN.
Native (Swift/Kotlin): performance tốt nhất, integration tốt nhất với platform features mới, App Store/Play Store reject ít nhất. Nhược điểm: viết hai codebase, team chia đôi, ship feature chậm hơn 2x.
Quyết định 2026:
- Team JS có sẵn, web/mobile share code, MVP nhanh → RN + Expo.
- Cần UI brand đồng nhất pixel-perfect, animation phức tạp → Flutter.
- App nặng platform feature (AR, ML mới, system extension) → Native.
- Hybrid: native shell + RN module trong cho feature thay đổi nhanh.
Pattern phổ biến: không điều hướng giữa Login screen và App screens trong cùng một stack.
Thay vào đó render hai stack khác nhau dựa trên auth state.
function RootNavigator() {
const { user, isLoading } = useAuth()
if (isLoading) return <SplashScreen />
return (
<NavigationContainer>
{user ? <AppStack /> : <AuthStack />}
</NavigationContainer>
)
}Lợi ích:
- Khi user null → AppStack unmount hoàn toàn → không còn stale data, không cần manually navigation.reset().
- Login flow không "lưu" lịch sử trong AppStack → user không swipe-back từ Home về Login được (security).
- Logout = chỉ cần set user = null, navigation tự switch.
Pitfall: transition giữa AuthStack ↔ AppStack mặc định không animate (cây React unmount/remount toàn bộ). Để có animation:
- JS stack (@react-navigation/stack): screenOptions={{ animationTypeForReplace: 'pop' }} cho phép custom transition.
- Native stack (@react-navigation/native-stack): không có prop tương đương — dùng react-native-bootsplash hoặc fade overlay tự custom (Reanimated <Animated.View> ở root) để cover transition.
- Hoặc giữ wrapper component animate opacity quanh <NavigationContainer> con.
removeClippedSubviews — đo trade-off thế nào?removeClippedSubviews là prop của ScrollView/FlatList, mặc định false iOS và true Android. Khi bật, RN tách (detach) native view ra khỏi view hierarchy khi nó scroll khuất khỏi viewport, gắn lại khi scroll vào.
Lợi:
- Giảm số UIView/Android.View trong tree → giảm work cho compositor mỗi frame.
- List rất dài (>50 items mỗi viewport) thấy FPS scroll cải thiện 5–15%.
Trade-off / bug:
- Image trắng: Image sẽ unload khi clipped → khi scroll lại, ảnh phải reload từ cache. Nếu cache miss (vd remote URL không có disk cache), thấy flash trắng.
- Animation reset: Animated value gắn vào view bị clipped sẽ pause/reset.
- TextInput focus: input bị clipped có thể mất focus khi scroll.
- Layout glitch: trên Android cũ (<8.0), đôi khi item bị "ghost" hoặc layout shift sai.
Đo cụ thể:
1. React DevTools Profiler hoặc Systrace — đo render time mỗi commit.
2. So sánh peak memory giữa on/off → nếu chênh nhỏ, không đáng tắt feature ổn định.
3. Test scroll fast trên device thật + ảnh remote → nếu thấy flash, tắt và tìm cách khác (thay FlatList bằng FlashList).
Khuyến nghị: mặc định true Android cho list lớn; false cho list có TextInput hoặc animation chạy. Trên 2026 với New Architecture + Fabric, prop này ít cần thiết hơn vì Fabric quản lý view tree hiệu quả hơn.
RN DevTools (default từ RN 0.76+, thay Flipper):
- Console log, Network inspector, React DevTools (component tree, profiler).
- Inspect Hermes heap snapshot.
- Mở qua: lắc device → "Open DevTools", hoặc terminal j.
- Tốt cho: JS-side bottleneck, component re-render counts, memory leak JS.
Systrace (Android) + Hermes Sampling Profiler:
- Trace JS execution timeline → biết function nào tốn time.
- Setup: adb shell setprop debug.systrace 1 rồi capture qua Android Studio.
- Hermes profiler: Hermes.connect() → record → save .cpuprofile → mở Chrome DevTools "Performance" tab.
- Tốt cho: tìm slow function trong render path, frame drop khi scroll.
Xcode Instruments (iOS):
- Tool: Time Profiler, Allocations, Leaks, Energy Log.
- Attach process → record → drill xuống xem CPU breakdown.
- Tốt cho: native-side issue (image decoding, layout pass, GPU), memory growth.
Android Studio Profiler:
- CPU (Java/Kotlin call stack), Memory (Java heap + Native heap), Energy.
- Tốt cho: tìm leak Java, GC pressure.
Bonus: react-native-performance library tự collect metrics (TTI, render time per screen) → ship lên backend monitor production.
Workflow chuẩn:
1. Repro issue trên device thật (không simulator).
2. RN DevTools trước — xem JS-side suspect.
3. Nếu JS không tìm được, sang Instruments/Studio Profiler cho native-side.
4. So sánh metric Hermes on vs off để đánh giá engine impact.
Bridge cũ (Legacy):
- Gọi native = serialize args thành JSON → đẩy vào queue async → native deserialize → execute → trả result qua queue ngược lại.
- Async-only, batched mỗi 5–10 ms.
- JS không thể giữ tham chiếu trực tiếp tới object native — chỉ giao tiếp qua module ID + method name.
JSI (JavaScript Interface): C++ API exposed bởi engine (Hermes/JSC):
// C++ side — đăng ký function lên JS
jsi::Function::createFromHostFunction(runtime, propName, paramCount,
[](Runtime& rt, const Value& thisVal, const Value* args, size_t count) {
return jsi::Value(42)
})// JS side — gọi như function thường, sync
const result = global.myNativeFn() // = 42Khác biệt cốt lõi:
- Sync call: global.x() chạy ngay, return value ngay, không Promise.
- HostObject / HostFunction: native expose object/function trực tiếp lên global JS.
- Không serialize: truyền tham chiếu object qua C++ — Buffer, ArrayBuffer, String chia sẻ memory.
- Hỗ trợ tất cả engine: Hermes, JSC, V8 (custom build) đều implement JSI interface.
Hệ quả thực tế:
- Reanimated 3 chạy worklet trên UI thread, đọc shared value đồng bộ → animation 120fps.
- VisionCamera gửi frame buffer (camera preview) sang JS không serialize.
- Database adapter (WatermelonDB, Op-SQLite) query 1000 row 10× nhanh hơn AsyncStorage.
- TurboModules dùng JSI cho mọi method call.
UIManager cũ (Paper):
- React reconciler tính diff trên JS thread → gửi message "createView/updateView/removeView" qua bridge → UIManager native execute trong background queue → áp lên main thread.
- Async commit → có thể desync giữa state JS và UIView (vd state nói btn ẩn, UIView vẫn hiện trong vài frame).
- Concurrent React 18 không hoạt động đúng (commit không đồng bộ).
Fabric:
- Reconciler giờ chạy trong C++ (Hermes hoặc node-side), không qua bridge serialize.
- Layout (Yoga) + ShadowTree đặt trong C++, share memory với native renderer.
- Commit phase chạy đồng bộ với React reconciler — khi setState callback chạy, UIView đã updated.
- Hỗ trợ useLayoutEffect đúng nghĩa "trước paint".
- Concurrent React: Suspense, useTransition, automatic batching hoạt động đúng.
Khác biệt thực tế:
- Tap → setState → render: trên Paper có lag visible 16–32ms; trên Fabric đồng bộ trong cùng frame.
- Animation Reanimated UI thread giờ commit đúng order với React commit.
- Custom view native phải register qua Fabric component spec (Codegen) thay vì RCTViewManager.
Migrate cost: native components cũ (vd map view, chart custom) phải refactor sang <ComponentName>Fabric.tsx + .cpp spec. Đa số component thông dụng đã có version Fabric trong RN 0.76+.
NativeModules cũ:
- Register tất cả module ngay lúc app start → tốn memory/startup time dù chưa dùng.
- Mọi method call async (Promise/callback) qua bridge.
- API định nghĩa qua RCTBridgeModule (iOS Obj-C) / ReactContextBaseJavaModule (Android) — không type-safe, JS phải nhớ chính xác signature.
- Hệ quả: app có 100 module, dù dùng 10, vẫn pay cost startup cho 100.
TurboModules:
- Lazy load: chỉ load module khi gọi đến lần đầu (require('NativeFoo').foo() → load native binding tại runtime).
- Sync call qua JSI: getNumber() return number ngay, không Promise. (Async vẫn được khi cần I/O.)
- Type-safe: Codegen sinh code C++/Java/Obj-C từ TypeScript spec → JS gọi sai signature → compile error.
- Backward compat: API JS có thể giữ giống module cũ — migrate dần.
Spec file (TypeScript):
// NativeCalculator.ts
import { TurboModule, TurboModuleRegistry } from 'react-native'
export interface Spec extends TurboModule {
add(a: number, b: number): number
fetchUser(id: string): Promise<{ name: string; age: number }>
}
export default TurboModuleRegistry.getEnforcing<Spec>('Calculator')Codegen đọc spec → tự sinh:
- iOS: RCTCalculatorSpec.h/.mm (Obj-C/C++ glue).
- Android: CalculatorSpec.java/.kt.
Native implement chỉ cần extends spec → mọi điểm khớp.
Performance: TurboModule call ~100× nhanh hơn legacy module call vì không serialize, không queue.
Codegen là tool đọc TypeScript/Flow spec rồi sinh code native (C++, Obj-C++, Java/Kotlin) cho TurboModules và Fabric components. Mục đích: đảm bảo type-safety qua boundary JS ↔ native.
Vì sao cần:
- Bridge cũ: JS gọi native chỉ qua "module name + method name + JSON args". Sai tên/sai args → runtime error, không phát hiện compile.
- New Arch dùng JSI sync call → cần biết exact type signature ở compile time để engine generate vtable đúng. Codegen ghi nhận spec một lần, sinh ra glue code khớp tuyệt đối.
Workflow:
1. Author viết spec TypeScript: interface Spec extends TurboModule { add(a: number, b: number): number }.
2. Codegen chạy lúc build (pod install trên iOS, gradle task trên Android) → đọc tất cả Native*.ts trong project + lib.
3. Generate RCTCalculatorSpec.h/.mm, CalculatorSpec.java, CalculatorJSI.h.
4. Native code phải extends generated class → IDE báo lỗi nếu không match signature.
Cấu hình package.json:
"codegenConfig": {
"name": "RNCalculatorSpec",
"type": "modules",
"jsSrcsDir": "./src"
}Ai chịu trách nhiệm chạy Codegen:
- Lib author: ship spec trong package, user install lib → codegen chạy lúc build local.
- App author: viết spec local cho native module riêng → codegen chạy mỗi pod install/Gradle build.
Lợi ích kéo theo: docs auto từ spec, IDE autocomplete đầy đủ JS-side, refactor rename method được TS trace đầu đến cuối.
Bridgeless = bỏ hoàn toàn Bridge legacy. Mọi native interop chuyển sang JSI/Fabric/TurboModules.
Trước Bridgeless (RN 0.68–0.73, dù đã có New Arch):
- Bridge vẫn chạy song song để hỗ trợ legacy modules.
- JS thread vẫn share queue cũ với native.
- Vẫn có overhead dù nhiều phần đã chạy JSI.
Bridgeless (giới thiệu RN 0.74, default từ RN 0.76+):
- Không còn RCTBridge instance.
- App khởi động qua RCTHost (iOS) / ReactHost (Android) — manages JSI runtime, TurboModule registry, Fabric Surface.
- Mọi legacy NativeModule không TurboModule-compat sẽ không hoạt động → buộc migrate hoặc tìm replacement.
- Startup nhanh hơn 20–30% trên Android low-end.
- Memory footprint giảm vì không còn 2 bộ infrastructure song song.
Code thay đổi:
// AppDelegate.mm — iOS new-style
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.moduleName = @"MyApp";
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}App template Expo/RN CLI mới đã setup sẵn.
Pitfall: lib chưa migrate sẽ crash hoặc throw "TurboModuleRegistry: 'MyOldModule' could not be found." Lúc đó:
1. Update lib lên version mới nhất.
2. Nếu lib không còn maintain, fork và migrate sang TurboModule spec.
3. Tạm thời unstable_enableSyncTurboModule workaround (deprecated, sẽ remove).
Bước 1 — Native Module:
- Chuyển từ RCTBridgeModule (iOS) / ReactContextBaseJavaModule (Android) sang TurboModule spec.
- Viết file spec TypeScript NativeFoo.ts với interface Spec extends TurboModule.
- Chạy bundle exec pod install → Codegen sinh RNFooSpec.h/.mm.
- iOS: implement class kế thừa <RNFooSpec> protocol.
- Android: extends NativeFooSpec abstract class.
Bước 2 — Native Component (custom view):
- iOS: RCTViewManager → RCTComponentViewProtocol + Fabric component descriptor.
- Android: ViewManager → RCTViewManager (Fabric variant) + ViewProps C++ struct.
- Codegen type: "components" cho component spec.
Bước 3 — Update package.json:
"codegenConfig": {
"name": "RNFooSpec",
"type": "all",
"jsSrcsDir": "src"
}Bước 4 — Test compat:
- expo doctor hoặc npx react-native doctor báo issue.
- Build app với RCT_NEW_ARCH_ENABLED=1 (iOS) / newArchEnabled=true (Android gradle.properties).
- Test toàn bộ public API, đặc biệt method sync (vốn async ở bridge).
- Run trên cả Hermes và JSC.
Bước 5 — Document:
- README ghi version RN tối thiểu hỗ trợ.
- CHANGELOG cảnh báo breaking change (nếu API JS có thay đổi).
- Drop legacy support sau 2–3 minor version để clean codebase.
Resources: RN New Arch Working Group có spreadsheet tracking lib status (✅ supported, 🚧 in-progress, ❌ unsupported).
Hermes và New Architecture không phụ thuộc kỹ thuật bắt buộc — JSI hoạt động cả với JSC. Nhưng combo này được Meta đẩy mạnh vì cộng hưởng:
1. JSI implementation tốt nhất ở Hermes: Hermes được viết cùng thời với JSI, có tối ưu specific cho HostObject, ArrayBuffer share-memory. JSC vẫn implement JSI nhưng tốc độ TurboModule call thường chậm hơn measurable (vài % đến hơn 10% tùy workload — kiểm chứng bằng benchmark trên app cụ thể).
2. Bytecode + JSI = startup nhanh:
- Hermes load bytecode (hbc) ngay, không parse JS.
- JSI bind native module lazy, không upfront cost.
- Combo: TTI giảm 40–50% trên Android low-end vs JSC + legacy.
3. Memory:
- Hermes generational GC tốt cho object short-lived (mỗi animation frame).
- Fabric C++ ShadowTree không lookup qua bridge → ít cấp phát JS object.
- Tổng memory footprint giảm 20–30%.
4. Profiling tools:
- RN DevTools dựa trên Chrome DevTools Protocol qua Hermes inspector.
- Heap snapshot, CPU profile chỉ hoạt động với Hermes.
- JSC mode mất feature debugging hiện đại.
5. Future features:
- Static Hermes (đang phát triển 2025): bytecode pre-compiled với type info → AOT optimization. Roadmap chính thức của Meta — version chính xác RN tích hợp default có thể đổi.
- Concurrent React và Suspense workflow giả định Fabric + Hermes.
Khi nào không dùng combo:
- Lib critical chỉ work trên JSC (rất hiếm 2026).
- iOS-only app cần feature JSC engine cụ thể (ví dụ Safari Inspector).
Hermes default trong Expo từ SDK 49+; New Architecture + Bridgeless trở thành default từ Expo SDK 52 (RN 0.76, Q4 2024). RN CLI 0.76+ cũng default combo này.
Bước 1 — Spec TypeScript (src/specs/NativeCalculator.ts):
import { TurboModule, TurboModuleRegistry } from 'react-native'
export interface Spec extends TurboModule {
add(a: number, b: number): number
fetchUser(id: string): Promise<{ name: string }>
}
export default TurboModuleRegistry.getEnforcing<Spec>('Calculator')Bước 2 — package.json:
"codegenConfig": { "name": "RNCalculatorSpec", "type": "modules", "jsSrcsDir": "src/specs" }Bước 3 — Implementation Kotlin (android/src/main/java/com/calculator/CalculatorModule.kt):
package com.calculator
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.Promise
import com.calculator.NativeCalculatorSpec // generated by Codegen
class CalculatorModule(reactContext: ReactApplicationContext) :
NativeCalculatorSpec(reactContext) {
override fun getName() = NAME
override fun add(a: Double, b: Double): Double = a + b
override fun fetchUser(id: String, promise: Promise) {
// gọi API, DB, etc.
promise.resolve(mapOf("name" to "Alice"))
}
companion object { const val NAME = "Calculator" }
}Bước 4 — Package register (CalculatorPackage.kt):
class CalculatorPackage : TurboReactPackage() {
override fun getModule(name: String, ctx: ReactApplicationContext) =
if (name == CalculatorModule.NAME) CalculatorModule(ctx) else null
override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
mapOf(CalculatorModule.NAME to ReactModuleInfo(
CalculatorModule.NAME, CalculatorModule::class.java.name,
false, false, false, false, true))
}
}Bước 5 — Add to MainApplication:
override fun getPackages(): List<ReactPackage> = PackageList(this).packages.apply {
add(CalculatorPackage())
}Bước 6 — Sử dụng:
import Calculator from './src/specs/NativeCalculator'
console.log(Calculator.add(2, 3)) // 5 (sync)
console.log(await Calculator.fetchUser('42')) // { name: "Alice" }Build: pnpm android → Gradle chạy Codegen → biên dịch.
Spec TypeScript giống ví dụ Kotlin (NativeCalculator.ts).
Bước 1 — Header Obj-C bridging (ios/Calculator/RNCalculator.h):
#import <Foundation/Foundation.h>
#import "RNCalculatorSpec.h" // generated by Codegen
NS_ASSUME_NONNULL_BEGIN
@interface RNCalculator : NSObject <NativeCalculatorSpec>
@end
NS_ASSUME_NONNULL_ENDBước 2 — Implementation Swift (ios/Calculator/RNCalculator.swift):
import Foundation
@objc(RNCalculator)
class RNCalculator: NSObject, NativeCalculatorSpec {
func add(_ a: NSNumber, b: NSNumber) -> NSNumber {
return NSNumber(value: a.doubleValue + b.doubleValue)
}
func fetchUser(_ id: String, resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) {
resolve(["name": "Alice"])
}
static func moduleName() -> String { "Calculator" }
}Bước 3 — Bridging header (ios/RNCalculator-Bridging-Header.h):
#import <React/RCTBridgeModule.h>Bước 4 — Module entry Obj-C++ (ios/Calculator/RNCalculator.mm):
#import "RNCalculator.h"
#import "MyApp-Swift.h"
@implementation RNCalculator
RCT_EXPORT_MODULE(Calculator)
@endBước 5 — Calculator.podspec:
Pod::Spec.new do |s|
s.name = 'Calculator'
s.source_files = "Calculator/**/*.{swift,h,m,mm}"
s.dependency 'React-Core'
install_modules_dependencies(s)
endBước 6 — pod install trong ios/. Codegen chạy → RNCalculatorSpec.h/.mm generate. Build pnpm ios.
Pitfall:
- Swift class phải @objc để Codegen + Obj-C++ thấy.
- Method async dùng RCTPromiseResolveBlock/RCTPromiseRejectBlock.
- Param type: NSNumber cho number, NSString cho string, NSDictionary cho object — Swift Codegen tự bridge.
.ts) → Codegen flow chi tiết?Codegen là middleware giữa spec TypeScript và native binding code. Hiểu flow giúp debug khi build fail.
Input:
- Một hoặc nhiều file Native*.ts (theo convention) trong thư mục được khai báo codegenConfig.jsSrcsDir.
- Mỗi file phải export interface Spec extends TurboModule { ... } và export default TurboModuleRegistry.getEnforcing<Spec>('ModuleName').
Bước Codegen chạy:
1. Parse: đọc tất cả Native*.ts, dùng TypeScript compiler API extract type info.
2. Validate: kiểm tra type hợp lệ — chỉ accept string, number, boolean, Object, Array, Promise<T>, custom Object literal. Type khác (Date, Map) → error.
3. Generate:
- iOS: RN<Name>Spec.h (Obj-C protocol), RN<Name>Spec.mm (Obj-C++ wrapper với JSI bindings).
- Android: <Name>Spec.java (abstract class với @ReactMethod annotations) hoặc <Name>Spec.kt.
- C++: <Name>JSI.h (TurboModule descriptor cho JSI runtime).
4. Output location:
- iOS: ios/build/generated/ios/.
- Android: android/build/generated/source/codegen/.
- Tự động add vào Xcode project và Gradle source set.
Khi nào trigger:
- iOS: pod install mỗi khi spec file đổi.
- Android: Gradle task generateCodegenArtifactsFromSchema chạy mỗi build.
- Manual: node node_modules/react-native/scripts/generate-codegen-artifacts.js để debug.
Debug flow:
# In ra schema được parse
node node_modules/react-native/scripts/generate-codegen-artifacts.js \
--path . --outputPath ./codegen-debugKết quả ./codegen-debug/schema.json cho thấy Codegen hiểu spec ra sao — match với generated code.
Common errors:
- "Cannot read properties of undefined (reading 'name')": spec không export đúng default.
- "Unsupported type: Date": dùng string (ISO date) hoặc number (unix timestamp) thay.
- Build cache stale: cd ios && rm -rf build && pod install.
useSharedValue / useAnimatedStyle — chạy trên UI thread thế nào?Worklet = block JS code được compile thành standalone function chạy trên UI thread, không cần roundtrip về JS thread.
useSharedValue(initial) tạo object có .value → đọc/ghi từ cả JS thread và UI thread. Internal là object C++ shared memory.
useAnimatedStyle(() => {...}, deps) marker 'worklet' ngầm — function callback được transform thành worklet bởi react-native-reanimated/plugin (Babel plugin). Mỗi frame UI thread gọi worklet → style mới → áp lên Fabric ShadowTree → render.
const progress = useSharedValue(0)
// Worklet — chạy UI thread, đọc shared value sync
const style = useAnimatedStyle(() => {
return {
opacity: progress.value,
transform: [{ scale: 1 + progress.value * 0.5 }],
}
})
// JS thread set value → trigger worklet rerun on UI thread
progress.value = withSpring(1)Why fast:
- Không bridge serialize.
- Không chờ JS thread rảnh.
- 60–120 FPS guaranteed dù JS thread đang busy parse JSON 100MB.
Pitfall:
- Worklet không access được variable bên ngoài trừ khi capture qua dependency array — closure không hoạt động như JS thường.
- Console.log bên trong worklet không hiện trong JS console (UI thread riêng) — dùng runOnJS(console.log)(value) để log.
- Babel plugin react-native-reanimated/plugin phải là plugin cuối trong babel.config.js.
@shopify/react-native-skia mang Skia engine (2D graphics của Google, dùng trong Chrome/Flutter) vào RN. Cho phép vẽ canvas-style với GPU acceleration.
Use case:
1. Custom canvas drawing: logo animated, signature pad, drawing app.
<Canvas style={{ flex: 1 }}>
<Circle cx={100} cy={100} r={50} color="cyan" />
<Path path="M 0 0 L 100 100" color="red" />
</Canvas>2. Charts performance: Victory/D3 trong RN render qua SVG, lag với 1000+ data point. Skia render qua GPU, smooth với 10k+ point.
3. Blur backdrop / glassmorphism: built-in Blur filter, áp lên image hoặc layer trên — iOS-quality blur trên cả Android.
<Image image={img} fit="cover" />
<Blur blur={20} />4. Image filters / shaders: color tint, ColorMatrix, BlendMode (multiply, overlay, etc), runtime SkSL shader (custom GLSL-style code).
5. Particle/animation phức tạp: kết hợp Reanimated shared value + Skia animated value → 60fps animation 1000+ particle.
Lợi:
- Performance ngang Flutter cho graphics nặng.
- Cross-platform pixel-perfect (giống Flutter).
- GPU-accelerated.
Nhược:
- Bundle size +~3MB.
- Learning curve: Skia API ≠ Canvas web ≠ React Native.
- Debugger limited (canvas content không tương tác bằng inspector).
Khi nào KHÔNG dùng:
- UI thường (button, list, form) — overhead không xứng.
- Animation đơn giản — Reanimated transform đủ.
- App size critical (banking app yêu cầu APK <30MB).
App RN baseline ~25–40MB (iOS) / ~15–25MB (Android). Tối ưu xuống 10–20MB cho banking app, app SEA emerging market.
Android:
1. Hermes (default 0.70+): bytecode + smaller runtime → giảm 5–10MB so với JSC.
2. R8/ProGuard (default release): obfuscate + tree-shake unused Java code. Thêm rules tránh strip code dùng qua reflection:
-keep class com.facebook.react.** { *; }
-keep class com.swmansion.reanimated.** { *; }3. ABI splits (android/app/build.gradle):
splits {
abi {
enable true
reset()
include 'arm64-v8a', 'armeabi-v7a' // skip x86
universalApk false
}
}Mỗi APK chỉ chứa native lib cho 1 ABI → giảm 30–50% size. Play Store chấp nhận multi-APK.
4. App Bundle (.aab) (recommend): Google chia chunk theo device → user download chỉ phần cần. Giảm 20–30% so với universal APK.
5. Resource shrinking: shrinkResources true trong release. Bỏ asset không reference.
iOS:
1. Bitcode (deprecated từ Xcode 14): không cần config nữa.
2. App Thinning: App Store tự thinning per-device — chỉ download asset cho device đó. Active by default.
3. Strip debug symbols: Strip Linked Product = YES trong release build settings.
Cross-platform:
- Image: dùng WebP/AVIF thay PNG/JPEG. Tool:
expo-imagetự pick format. - Font subset: chỉ ship glyph cần. Tool
glyphhangerhoặc Google Webfonts subset API. - Lottie/JSON animation: minify, dedupe asset chung.
- Audit:
npx react-native-bundle-visualizerxem JS bundle.apkanalyzer(Android Studio) cho APK.
Target số 2026: một app feature trung bình nên fit trong 25MB Android, 40MB iOS install size.
1. Lưu sensitive data (token, password, biometric):
- iOS: Keychain — mã hóa hardware-backed, persistent qua app reinstall (option), bio-protected.
- Android: Keystore + EncryptedSharedPreferences.
- Lib: react-native-keychain hoặc expo-secure-store (cross-platform wrapper).
import * as SecureStore from 'expo-secure-store'
await SecureStore.setItemAsync('authToken', token, {
keychainAccessible: SecureStore.WHEN_UNLOCKED, // chỉ đọc khi device unlocked
})Đừng lưu token trong AsyncStorage hay MMKV không encryption — plain text trong file system, jailbreak/root device đọc được.
2. Certificate pinning: kiểm tra public key của server cert match expected → chống MitM (Man-in-the-Middle), proxy như Charles không inspect được traffic.
- Lib: react-native-ssl-pinning hoặc react-native-cert-pinner.
- Cấu hình:
import { fetch } from 'react-native-ssl-pinning'
fetch('https://api.example.com/me', {
method: 'GET',
sslPinning: { certs: ['cert1'] }, // file cert.cer trong assets
})- Pitfall: cert rotate → app cần update để pin cert mới, không OTA được. Pin public key thay cert (longer-lived) hoặc multi-pin với fallback.
3. Code obfuscation:
- Hermes bytecode đã hard hơn JS plain để reverse, nhưng vẫn decompile được.
- Tool: react-native-obfuscating-transformer (rename variable, dead code).
- Native code: ProGuard/R8 (Android), Strip Linked Product (iOS).
4. Jailbreak/Root detection:
- Lib: react-native-jail-monkey hoặc react-native-device-info.
- Check isJailBroken/isRooted → block app launch hoặc downgrade feature (vd disable banking transaction).
- Lưu ý: chỉ là deterrent, không tuyệt đối — sophisticated attacker có thể bypass.
5. App Transport Security (ATS) iOS:
- Default require HTTPS với TLS 1.2+. Không tắt cho production.
- Info.plist NSAllowsArbitraryLoads chỉ cho dev nếu cần.
6. Biometric auth:
- Lib: react-native-keychain có setGenericPassword(..., { accessible: BIOMETRY_CURRENT_SET }).
- iOS Face ID/Touch ID, Android Fingerprint/Face/Iris đều support.
- Workflow: user login lần đầu → save token + biometric flag → lần sau prompt biometric → release token từ Keychain.