Component Patterns Best Practices
A condensed summary of the 25 most important best practices drawn from every page in this section.
Search across all documentation pages
A condensed summary of the 25 most important best practices drawn from every page in this section.
Name the screen vs presenter split: The screen (container) owns data, navigation, and side effects; the presenter renders props only - if you cannot test UI without mocking fetch, the boundary is wrong.
Co-locate by feature, not by type: Keep features/orders/screens, hooks, and components together - a global components/ dump becomes a graveyard of one-off wrappers.
One public export per feature: Expose OrdersScreen from features/orders/index.ts - route files import one symbol; internals stay private to the folder.
Extract UI hooks before the third useState: Modal open state, disclosure toggles, and filter chips belong in useDisclosure / useFilters - not twenty useState calls at the top of a screen.
Keep data hooks separate from UI hooks: useOrdersQuery fetches; useOrderFilters shapes UI - mixing them makes hooks untestable and over-fetches on unrelated toggles.
Use compound components for multi-part UI: Tabs, accordions, and field groups share implicit state via context - prop-drilling selectedIndex through four layers is a maintenance tax.
Expose intent methods on compound APIs: Export selectTab(id) instead of raw setIndex - screens should not bypass validation or analytics hooks wired inside the compound root.
Reach for render props when children need data the parent owns: List empty states and row skeletons are classic render-prop slots - avoid cloning the parent just to read its state.
Use Expo Router <Slot /> for layout shells: Slot patterns flatten deep navigation trees without wrapper hell - the layout owns chrome; leaf routes own content.
Container/presenter naming is documentation: Suffix OrdersScreen + OrdersView (or OrdersPresenter) so reviewers instantly know which file may call fetch or router.push.
Test presenters with plain props: Presenters should render from a props object with zero providers - snapshot and interaction tests run in milliseconds without MSW.
Headless primitives return behavior, not styles: useDisclosure exports triggerProps and contentProps bags - pre-styled output in the primitive couples every consumer to your design tokens.
Mirror visual state in accessibility props: Selected tabs and expanded accordions must set accessibilityState and accessibilityRole - VoiceOver does not read your CSS.
Memoize accessibility prop bags: Rebuilding { onPress, accessibilityRole, … } inline every render defeats memo on list rows - useMemo keyed on open / selected.
Never call hooks inside render props: children={() => useFoo()} violates the Rules of Hooks - extract a named child component or call the hook in the parent.
Use polymorphic as with generics: BoxProps<E extends ElementType> preserves href vs onPress typing - bare ElementType erases the host contract.
Prefer asChild over nested touchables: Merging props onto Expo Router Link avoids invalid Pressable > Link trees and fixes focus order on web targets.
Enforce a single child for asChild: Children.only fails fast when designers wrap two nodes - document the constraint in Storybook examples.
Chain onPress handlers deliberately: Parent and child both fire by default - document precedence when navigation should swallow the parent handler.
Split god screens at 300 lines, not 2,000: Extract presenters and hooks incrementally - waiting until a file is unmaintainable guarantees a risky big-bang refactor.
Move FlatList renderItem to a memoized row component: Inline 200-line renderItem closures re-create every parent render and block virtualization wins.
Validate route params at the container: God screens branch on twelve raw string params - parse useLocalSearchParams with Zod in the screen container before passing typed props down.
Keep platform switches in presenters or tokens: Repeated Platform.OS ternaries through JSX signal a missing presenter split or design-system variant.
Reserve global stores for cross-route state: Bottom-sheet open state for one screen does not belong in Zustand - local state or a feature-scoped context is enough.
Unit-test headless hooks, snapshot one skin: Test useDisclosure behavior in isolation; integration-test a single styled reference component - not every consumer markup tree.
Tabs.List, Tabs.Panel) that must stay in sync.Pressable, which breaks link semantics and nesting rules.*View component with typed props.router.push in the screen file until the view stabilizes.app/ routes stay thin - import FeatureScreen from features/ and render it.Stack versions: This page was written for React 19.2.3, React Native 0.86.0, and Expo SDK 57 (
expo~57.0.4).
Reviewed by Chris St. John·Last updated Jul 16, 2026