Project Structure
CryptoTracker follows a modular architecture built with Expo and React Native, organizing code by feature and responsibility:
cryptotracker/
├── app/ # Expo Router file-based routing
│ ├── (tabs)/ # Tab navigation group
│ │ ├── _layout.tsx # Tab bar configuration
│ │ └── index.tsx # Home screen route
│ ├── crypto/ # Crypto detail routes
│ │ └── [id].tsx # Dynamic route for crypto details
│ ├── _layout.tsx # Root layout with theme
│ └── +not-found.tsx # 404 error page
├── src/
│ ├── components/ # Reusable UI components
│ │ ├── CryptoCard.tsx # Individual crypto display card
│ │ ├── HomeHeader.tsx # Header with search/filters
│ │ └── SearchBar.tsx # Search input component
│ ├── context/ # React Context providers
│ │ └── FilterContext.tsx # Global filter state
│ ├── hooks/ # Custom React hooks
│ │ └── useCryptoData.ts # API data fetching hook
│ ├── models/ # TypeScript interfaces & classes
│ │ └── Crypto.ts # Crypto data models
│ ├── screens/ # Full-page screen components
│ │ ├── HomeScreen.tsx # Main listing screen
│ │ └── CryptoDetailScreen.tsx
│ ├── services/ # API & external services
│ │ └── cryptoService.ts # CoinLore API integration
│ ├── utils/ # Utility functions
│ │ └── formatters.ts # Price/number formatting
│ └── constants/ # App-wide constants
│ └── Colors.ts # Theme color definitions
└── assets/ # Static assets (images, fonts)
The app/ directory defines your routes using Expo Router’s file-based routing, while src/ contains all business logic, components, and utilities.
Data Flow Architecture
The application follows a unidirectional data flow pattern:
Data Flow Steps
API Request
The useCryptoData hook initiates a fetch request to the CoinLore API when the component mounts: src/hooks/useCryptoData.ts:30-34
const fetchData = async () => {
const res = await fetch ( 'https://api.coinlore.net/api/tickers/' );
const json = await res . json ();
// Process data...
};
Data Transformation
Raw API responses are transformed into typed Crypto class instances: src/hooks/useCryptoData.ts:36-40
const cryptos = json . data . map (
( item : any ) =>
new Crypto ( item . id , item . name , item . rank ,
item . symbol , item . price_usd , item . percent_change_24h )
);
State Management
Data is stored in component state and made available to the UI: src/hooks/useCryptoData.ts:19-25
const [ data , setData ] = useState < CryptoApiResponse []>([]);
const [ loading , setLoading ] = useState ( true );
const [ error , setError ] = useState < string | null >( null );
Filtering
The HomeScreen combines API data with global filter state: src/screens/HomeScreen.tsx:31-39
const filtered = data . filter (( crypto ) => {
const nameMatch = crypto . name . toLowerCase (). includes ( filter . text . toLowerCase ());
const symbolMatch = crypto . symbol . toLowerCase (). includes ( filter . text . toLowerCase ());
const minOk = filter . minPrice === null || Number ( crypto . price_usd ) >= filter . minPrice ;
const maxOk = filter . maxPrice === null || Number ( crypto . price_usd ) <= filter . maxPrice ;
return ( nameMatch || symbolMatch ) && minOk && maxOk ;
});
Rendering
Filtered data is mapped to CryptoCard components for display: src/screens/HomeScreen.tsx:74-76
{ filtered . map (( crypto ) => (
< CryptoCard key = {crypto. id } crypto = { crypto } />
))}
Component Hierarchy
The component tree demonstrates how data and state flow through the application:
Root Layout (_layout.tsx)
The root layout provides theme configuration and font loading: function RootLayoutNav () {
const colorScheme = useColorScheme ();
return (
< ThemeProvider value = { colorScheme === 'dark' ? DarkTheme : DefaultTheme } >
< Stack >
< Stack.Screen name = "(tabs)" options = { { headerShown: false } } />
< Stack.Screen name = "modal" options = { { presentation: 'modal' } } />
</ Stack >
</ ThemeProvider >
);
}
Tab Layout ((tabs)/_layout.tsx)
The tab layout wraps the application in the FilterProvider context: app/(tabs)/_layout.tsx:38-54
< FilterProvider >
< Tabs
screenOptions = { {
tabBarActiveTintColor: Colors [ colorScheme ?? 'light' ]. tint ,
} }
>
< Tabs.Screen
name = "index"
options = { {
title: 'Inicio' ,
tabBarIcon : ({ color }) => < TabBarIcon name = "home" color = { color } /> ,
header : () => < HomeHeader /> ,
} }
/>
</ Tabs >
</ FilterProvider >
Home Screen (HomeScreen.tsx)
The home screen consumes both the API data hook and filter context: src/screens/HomeScreen.tsx:17-22
const HomeScreen = () => {
const { data , loading , error } = useCryptoData ();
const { filter } = useFilter ();
// ... filtering and rendering logic
};
Crypto Card (CryptoCard.tsx)
Individual cards receive crypto data as props and handle navigation: src/components/CryptoCard.tsx:21-40
const CryptoCard = ({ crypto } : Props ) => {
const router = useRouter ();
return (
< TouchableOpacity
onPress = { () =>
router . push ({
pathname: '/crypto/[id]' ,
params: { id: crypto . id },
})
}
>
{ /* Card UI */ }
</ TouchableOpacity >
);
};
Type Safety with TypeScript
The application leverages TypeScript for type safety throughout the data flow:
API Response Interface
Crypto Class
Filter Interface
src/models/Crypto.ts:9-26
export interface CryptoApiResponse {
id : string ;
symbol : string ;
name : string ;
rank : number ;
price_usd : string ;
percent_change_24h : string ;
percent_change_1h : string ;
percent_change_7d : string ;
market_cap_usd : string ;
volume24 : number ;
// ... additional fields
}
src/models/Crypto.ts:34-63
export class Crypto {
constructor (
public id : string ,
public name : string ,
public rank : number ,
public symbol : string ,
public price_usd : string ,
public percent_change_24h : string ,
) {}
get formattedPrice () : string {
return `$ ${ parseFloat ( this . price_usd ). toFixed ( 2 ) } ` ;
}
}
src/context/FilterContext.tsx:3-13
export interface Filter {
text : string ;
minPrice : number | null ;
maxPrice : number | null ;
}
type FilterContextType = {
filter : Filter ;
setFilter : React . Dispatch < React . SetStateAction < Filter >>;
};
Service Layer
The service layer abstracts API communication logic:
src/services/cryptoService.ts:8-21
export const getCryptoList = async (
start : number = 0 ,
limit : number = 100
) : Promise < CryptoApiResponse []> => {
const url = `https://api.coinlore.net/api/tickers/?start= ${ start } &limit= ${ limit } ` ;
const response = await fetch ( url );
if ( ! response . ok ) {
throw new Error ( `Error al obtener lista de criptos: ${ response . status } ` );
}
const json = await response . json ();
return json . data ;
};
The cryptoService provides paginated data fetching and detail lookup functionality, supporting future features like infinite scroll or search optimization.
Key Architectural Patterns
Separation of Concerns
Routing : Handled by Expo Router in app/ directory
UI Components : Reusable components in src/components/
Business Logic : Custom hooks in src/hooks/
Data Models : TypeScript classes/interfaces in src/models/
API Integration : Service functions in src/services/
Container/Presenter Pattern
Screens (HomeScreen, CryptoDetailScreen) act as containers that fetch data and manage state
Components (CryptoCard, SearchBar) are presentational, receiving data via props
Custom Hooks for Reusability
The useCryptoData hook encapsulates data fetching logic, making it reusable across multiple screens:
const { data , loading , error } = useCryptoData ();
This pattern allows any component to access crypto data without duplicating fetch logic.
Next Steps
Navigation Learn how Expo Router powers file-based routing
State Management Explore Context API and custom hooks