Why This Decision Matters Early
React Native projects tend to start fast. A screen here, a card there, a button copied from the previous screen with one prop changed. Six weeks in, the same card component exists in nine different files — each with slightly different padding, a slightly different shadow, a slightly different border radius. Changing the design means hunting down every copy.
The fix is not discipline. It is structure. When there is a clear folder for every type of component, the right decision becomes the obvious one.
Three Folders — Everything Fits Somewhere
components/
ui/ ← no business logic, pure display
Card.tsx
Button.tsx
Badge.tsx
Avatar.tsx
layout/ ← structure shared across screens
ScreenWrapper.tsx
Header.tsx
TabBar.tsx
features/ ← specific to one domain
listings/
ListingCard.tsx
ListingGrid.tsx
profile/
ProfileHeader.tsx
ProfileStats.tsx
ui/ — components that know nothing about your application. They receive data via props and render it. No API calls, no navigation, no business logic. A Card is just a styled container. A Button is just a pressable element with a label. They work anywhere.
layout/ — components that define the structure of a screen. A ScreenWrapper handles safe area insets and scroll behaviour consistently. A Header renders the same back button and title pattern across every screen. These components don't know what content surrounds them.
features/ — components tied to a specific domain. A ListingCard knows about property listings. A ProfileHeader knows about user profiles. They live close to the feature they belong to and can use data-fetching hooks, navigation, or application state.
The One Rule
If a component appears on more than one screen, it belongs in ui/.
That's it. Apply it consistently and the structure stays clean without any active effort.
What a Reusable Card Component Looks Like in TypeScript
In React Native there is no className. Flexibility comes from accepting a style prop typed as StyleProp<ViewStyle> — which allows any valid React Native style, including arrays of styles, to be passed in and merged at the call site.
import { View, StyleSheet, StyleProp, ViewStyle } from 'react-native';
interface CardProps {
children: React.ReactNode;
style?: StyleProp<ViewStyle>;
}
export function Card({ children, style }: CardProps) {
return (
<View style={[styles.card, style]}>
{children}
</View>
);
}
const styles = StyleSheet.create({
card: {
borderRadius: 12,
padding: 16,
backgroundColor: '#fff',
shadowColor: '#000',
shadowOpacity: 0.08,
shadowRadius: 8,
shadowOffset: { width: 0, height: 2 },
elevation: 2,
},
});
Now the same Card works across every screen. You override spacing or background at the call site by passing a style prop — no new props required inside the component.
// Listing screen
<Card style={{ marginBottom: 12 }}>
<ListingContent listing={listing} />
</Card>
// Profile screen
<Card style={{ backgroundColor: '#f9f9f9' }}>
<ProfileStats user={user} />
</Card>
One component. Two completely different screens. Zero new props inside Card.
Keep Prop Interfaces Focused
A component accumulating conditional props is a component doing too much. The moment you find yourself adding showBadge, variant, and onLongPress to the same component, it needs to be split.
// ❌ Too many responsibilities in one component
<ListingCard
listing={listing}
showFavourite
showBadge
variant="featured"
onPress={handlePress}
onLongPress={handleLongPress}
/>
// ✓ Each piece has one job
<ListingCard listing={listing} onPress={handlePress} />
<FavouriteBadge listingId={listing.id} />
<FeaturedLabel />
Each piece is independently testable, independently reusable, and independently updatable. Changing the favourite behaviour doesn't require touching ListingCard at all.
Co-locate TypeScript Interfaces With the Component
Keep the props interface in the same file as the component. A separate types/ folder for component props adds indirection without benefit — when you update the component, you have to update two files instead of one.
// ListingCard.tsx
interface ListingCardProps {
listing: {
id: string;
title: string;
price: number;
imageUrl: string;
location: string;
};
onPress: (id: string) => void;
}
export function ListingCard({ listing, onPress }: ListingCardProps) {
return (
<Pressable onPress={() => onPress(listing.id)}>
<Card>
<ListingImage uri={listing.imageUrl} />
<ListingDetails listing={listing} />
</Card>
</Pressable>
);
}
The interface is right next to the component. If the listing shape changes, the interface is the first thing you see when you open the file.
When to Extract a Component
The heuristic is the same regardless of framework: if you are about to copy and paste JSX, stop and ask whether it should be a component instead.
Extract it when:
- The same JSX appears on more than one screen
- The block has a clear, nameable responsibility
- You want to test or update it in isolation
Leave it inline when:
- It only appears in one place and is unlikely to be reused
- Extracting it requires so many props that it becomes harder to read than the original
- The component is a one-liner that adds nothing by being named
The Payoff at Week Eight
None of this feels significant on day one. The folder structure looks like overhead.
Week eight is when it pays off. The designer changes the card shadow across the entire app. In a well-structured project, you update Card.tsx once — every screen updates. In an unstructured project, you spend a morning finding every place a card was copied and hoping you didn't miss the one in the onboarding flow.
Build components like you are building a library — even when you are the only one using it. Future you, two months from now, will not remember which card was the "correct" one. The folder structure makes that question irrelevant.
If you are starting a React Native project and want to get the architecture right before writing the first screen, get in touch. The right structure at the start is far cheaper than refactoring it later.