% rn-senior-interview.tex — the senior RN cheat sheet: RN vs native vs Flutter vs KMP,
% when to drop to native, large-app architecture + brownfield, security, a11y, the questions.
% Source: own knowledge (no repo sources for React Native). Senior-interview level.
% Build ONLY with: tools/print/print-sheet.py <this>.tex --dry-run
% @source: hiot monorepo, docs/school/sheets/react-native/rn-senior-interview.tex — the SOURCE OF TRUTH; a copy anywhere else (e.g. artur.gurgul.pro) is regenerated from it, never edited
% @labels: area=react-native kind=interview level=senior platform=cross-platform new=no round=react-native-2026-09-24 topic=architecture,security,career
% @tags: rn-vs-flutter, kotlin-multiplatform, brownfield, rctreactnativefactory, monorepo, error-boundary, react-native-keychain, certificate-pinning, accessibility-props, app-attest, when-to-go-native
\documentclass[8pt]{extarticle}
\usepackage{printup-sheet}
\usepackage{array}
\usepackage{tabularx}

\tikzset{
  q/.style={box, font=\scriptsize, inner sep=1.5pt, minimum height=4.6mm, text width=37mm,
            draw=sheetGrey, fill=sheetGrey!8},
  res/.style={box, font=\scriptsize\bfseries, inner sep=1.5pt, minimum height=4.6mm, text width=25mm},
  lbl/.style={font=\tiny, text=black!75, inner sep=1pt, align=center},
}
\newcolumntype{L}{>{\raggedright\arraybackslash}X}

\begin{document}

\sheettitle{React Native — the senior's cheat sheet}{react-native · memo}

\oneliner{A senior answers in \textbf{trade-offs}: RN = one React/TS codebase driving
\textbf{real native views}, native code where the OS or the frame budget demands it — and a
big app kept \textbf{modular, observable, secure, accessible}.}

\vspace{2pt}
{\scriptsize
\noindent\begin{tabularx}{\linewidth}{@{}>{\bfseries\raggedright\arraybackslash}p{13mm}LLLL@{}}
\toprule
 & \textbf{React Native} & \textbf{Fully native} (Swift / Kotlin) & \textbf{Flutter} & \textbf{KMP / Compose MP} \\
\midrule
Rendering & JS (Hermes) describes UI; Fabric mounts \textbf{real} UIKit / Android views &
  UIKit · SwiftUI · Views · Compose & Dart AOT; own engine \textbf{paints every pixel}
  (Impeller); platform look emulated & shared Kotlin logic + native UI; Compose MP draws
  its own UI on iOS \\
Perf ceiling & native views; limits = one JS thread + crossings; hot paths $\to$
  worklets / native & highest: direct GPU + OS & steady frames, own renderer;
  embedded platform views cost & native binary (Kotlin/Native); UI as native \\
Sharing & UI + logic (+ React skills, some web) & none — two codebases &
  UI + logic, also web/desktop & logic (data, domain, network); UI optional \\
Native APIs & Turbo / Expo Modules (Swift/Kotlin); big lib ecosystem & day one &
  platform channels / Pigeon / FFI & iOS via Obj-C interop; Swift-only APIs need a
  wrapper \\
Ship fixes & \textbf{OTA} JS updates (EAS Update) per native runtime & store only &
  store (OTA only via 3rd-party Shorebird) & store only \\
Hiring & huge React pool; seniors with native depth rare & two specialist teams &
  smaller Dart pool, fast ramp & Kotlin devs; iOS team must accept Kotlin \\
Wins when & React org, content/commerce, fast iteration, brownfield &
  OS-first, perf-critical, deepest polish & pixel-identical brand UI, many targets &
  native UX + shared logic, incremental \\
\bottomrule
\end{tabularx}}

\begin{multicols}{2}

\section{Picture — RN, native, or both?}
\noindent\begin{tikzpicture}[sheet]
  \node[q] (q1) at (0,2.1) {OS-first core? (widgets, watch, bg BLE)};
  \node[q] (q2) at (0,1.4) {Large existing native app?};
  \node[q] (q3) at (0,0.7) {Hot path = heavy GPU / realtime?};
  \node[q] (q4) at (0,0) {React/TS team, 2 platforms, OTA?};
  \node[res, draw=sheetBlue, fill=sheetBlue!10] (o1) at (4.3,2.1) {NATIVE \mdseries(+RN screens)};
  \node[res, draw=sheetOrange, fill=sheetOrange!12] (o2) at (4.3,1.4) {BOTH \mdseries brownfield RN};
  \node[res, draw=sheetOrange, fill=sheetOrange!12] (o3) at (4.3,0.7) {RN \mdseries+ native module};
  \node[res, draw=sheetGreen, fill=sheetGreen!12] (o4) at (4.3,0) {RN \mdseries(Expo)};
  \node[res, draw=sheetBlue, fill=sheetBlue!10] (o5) at (4.3,-0.7) {NATIVE \mdseries(KMP logic)};
  \foreach \a/\b in {q1/o1,q2/o2,q3/o3,q4/o4} \draw[flow, draw=sheetGreen!60!black] (\a) -- node[lbl, above]{yes} (\b);
  \foreach \a/\b in {q1/q2,q2/q3,q3/q4} \draw[flow] (\a) -- node[lbl, right]{no} (\b);
  \draw[flow] (q4.south) |- node[lbl, pos=0.3, right]{no} (o5.west);
\end{tikzpicture}

\section{When to drop to native}
\begin{itemize}\raggedright
  \item \textbf{GPU / realtime} (Metal, AR, video): a native Fabric view wrapped for JS; 2D
        can stay in JS via Skia. \textbf{Frames}: camera/audio on native or worklet threads,
        never JS per frame.
  \item \textbf{BLE + background}: iOS may suspend the JS runtime; background modes, state
        restoration, \texttt{BGTaskScheduler} are native — keep the stack native, emit events.
  \item \textbf{OS-first targets}: WidgetKit (SwiftUI, own process), Live Activities, App
        Intents, watchOS, extensions, CarPlay — native targets sharing data via App Groups.
\end{itemize}

\section{Architecting a large RN app}
\begin{itemize}\raggedright
  \item \textbf{Monorepo} (workspaces + Nx/Turborepo): \texttt{apps/mobile},
        \texttt{packages/ui}, \texttt{features/*} each with a public \texttt{index}; deep
        imports banned by lint. \textbf{Design system}: tokens $\to$ primitives $\to$
        components, a11y built in, Storybook.
  \item \textbf{State by kind}: server cache (TanStack Query) · client (Zustand / Redux
        Toolkit) · form · nav · persisted (MMKV). Classic bug: server data copied into a
        global store.
  \item \textbf{Errors}: per-screen \texttt{ErrorBoundary} + global JS handler + native
        crash SDK; upload \textbf{source maps + dSYMs per build and per OTA}. One typed
        analytics/log facade, PII scrubbed. \textbf{Flags}: staged rollout + kill switches,
        OTA code included.
  \item \textbf{Brownfield (iOS)}: JS \texttt{AppRegistry.registerComponent('Cart',…)}; a
        \texttt{UIViewController} hosts it — legacy \texttt{RCTRootView}, current
        \texttt{RCTReactNativeFactory} $\to$ \texttt{rootViewFactory.view(withModuleName:)}.
        \textbf{One} shared runtime; native owns navigation; data via
        \texttt{initialProperties} + modules/events; release ships \texttt{main.jsbundle}.
\end{itemize}

\section{Security — the bundle is public}
\begin{itemize}\raggedright
  \item JS or \textbf{Hermes bytecode} is extractable and decompilable; \texttt{.env} values
        are baked in. \textbf{No secrets on the client} — short-lived, scoped tokens only.
  \item Tokens in \textbf{Keychain / Keystore} (\texttt{react-native-keychain},
        \texttt{expo-secure-store}), never AsyncStorage (plaintext).
  \item \textbf{Pinning} in the native HTTP layer (TrustKit, OkHttp
        \texttt{CertificatePinner}); pin SPKI + a backup; a stale native pin needs a
        \emph{store} release.
  \item Jailbreak/root checks are heuristics Frida bypasses — cost, not protection; prefer
        server-verified App Attest / Play Integrity. Minify + bytecode $\neq$ obfuscation.
\end{itemize}

\section{Accessibility props}
\begin{itemize}\raggedright
  \item \texttt{accessible} merges children into \textbf{one} element (Pressable default) —
        nested buttons become unreachable.
  \item \texttt{accessibilityLabel} · \texttt{Hint} · \texttt{Role} (button, header, switch,
        adjustable…) · \texttt{State} \{disabled, selected, checked, busy, expanded\} ·
        \texttt{Value} · \texttt{accessibilityActions} for swipe-only gestures; ARIA aliases
        (\texttt{role}, \texttt{aria-label}) since 0.71. Keep font scaling; cap with
        \texttt{maxFontSizeMultiplier}.
\end{itemize}

\section{Interview traps}
\begin{itemize}\raggedright
  \trap{``RN is slow because JS'' — the cost is \emph{crossings} + a \emph{busy JS thread};
        name the thread that dropped frames, in a \textbf{release} build.}
  \trap{Error boundaries miss event-handler and async errors.}
\end{itemize}

\section{Remember}
\emph{Native views, JS brain · cross rarely · no secrets shipped · state by kind.}

\section{Questions that separate seniors}
\begin{enumerate}\raggedright
  \item Why was the bridge slow? — async JSON batches over one queue, no sync calls, all
        modules init at start; JSI = direct C++ refs, lazy TurboModules.
  \item \texttt{useCallback} everywhere? — no: pays only when identity matters (memo'd
        child, effect deps, \texttt{renderItem}); profile first.
  \item OTA vs native code? — an update targets a \texttt{runtimeVersion} (native
        fingerprint); native change $\Rightarrow$ new binary.
  \item Janky list? — release + Perf Monitor (JS or UI fps?); stable keys, memo'd rows,
        \texttt{getItemLayout}, sized images, then FlashList.
  \item Animation while JS is busy? — native driver (transform/opacity) or Reanimated
        worklets on the UI thread.
  \item What did Fabric change? — shared C++ shadow tree + Yoga, sync layout, concurrent React.
  \item Why Hermes? — bytecode compiled at build: faster start, less memory.
  \item Crash only in release? — symbolicate (source maps + dSYM); suspect R8/minify,
        \texttt{\_\_DEV\_\_} branches.
  \item When \emph{not} RN? — OS-first extensions, GPU-heavy core, an all-native team.
\end{enumerate}

\end{multicols}

\noindent{\footnotesize\color{sheetGrey}\textit{Related:} rn-testing-typescript ·
ios-client-system-design · ios-system-design-deep-dives · keychain-secure-enclave ·
accessibility-localization · \texttt{docs/memos/kmp-*}, \texttt{android-*}}

\end{document}
