Overview
CryptoTracker uses React’s built-in state management solutions without external libraries:
Context API for global state (filters)
Custom Hooks for data fetching and reusable logic
Component State for local UI state
This lightweight approach is perfect for small to medium apps, avoiding the complexity of Redux or similar libraries.
FilterContext Implementation
The FilterContext provides global filter state accessible throughout the app.
Context Structure
src/context/FilterContext.tsx:3-16
export interface Filter {
text : string ;
minPrice : number | null ;
maxPrice : number | null ;
}
type FilterContextType = {
filter : Filter ;
setFilter : React . Dispatch < React . SetStateAction < Filter >>;
};
const FilterContext = createContext < FilterContextType | undefined >( undefined );
Filter Interface
Context Type
The Filter interface defines the shape of filter state: interface Filter {
text : string ; // Search text for name/symbol
minPrice : number | null ; // Minimum price filter
maxPrice : number | null ; // Maximum price filter
}
Nullable price fields allow “no filter” state. The context provides both value and setter: type FilterContextType = {
filter : Filter ; // Current filter state
setFilter : React . Dispatch < React . SetStateAction < Filter >>;
};
This follows React’s useState pattern for familiarity.
FilterProvider Component
The provider wraps components that need access to filter state:
src/context/FilterContext.tsx:26-38
export const FilterProvider = ({ children } : { children : ReactNode }) => {
const [ filter , setFilter ] = useState < Filter >({
text: '' ,
minPrice: null ,
maxPrice: null ,
});
return (
< FilterContext.Provider value = { { filter , setFilter } } >
{ children }
</ FilterContext.Provider >
);
};
Initialize State
Creates local state with default filter values (empty text, no price limits)
Provide Context Value
Wraps children with context provider, passing filter and setFilter
Render Children
All child components can access the filter context
useFilter Hook
A custom hook provides safe access to the filter context:
src/context/FilterContext.tsx:50-57
export const useFilter = () => {
const ctx = useContext ( FilterContext );
if ( ! ctx ) throw new Error ( 'useFilter debe usarse dentro de FilterProvider' );
return ctx ;
};
The error check ensures the hook is only used within a FilterProvider, preventing runtime errors from undefined context.
Provider Setup in Navigation
The FilterProvider wraps the tab navigation to make filters available app-wide:
app/(tabs)/_layout.tsx:38-54
export default function TabLayout () {
const colorScheme = useColorScheme ();
return (
< 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 >
);
}
This placement ensures:
Filter state persists across tab switches
All screens within tabs can access filters
State resets when navigating away from the tab group
useCryptoData Hook
The useCryptoData hook encapsulates API data fetching logic:
src/hooks/useCryptoData.ts:17-60
export const useCryptoData = () => {
const [ data , setData ] = useState < CryptoApiResponse []>([]);
const [ loading , setLoading ] = useState ( true );
const [ error , setError ] = useState < string | null >( null );
useEffect (() => {
const fetchData = async () => {
try {
const res = await fetch ( 'https://api.coinlore.net/api/tickers/' );
const json = await res . json ();
const cryptos = json . data . map (
( item : any ) =>
new Crypto ( item . id , item . name , item . rank , item . symbol ,
item . price_usd , item . percent_change_24h )
);
setData ( cryptos );
} catch ( err ) {
console . error ( 'Error al obtener criptomonedas:' , err );
setError ( 'Error al obtener criptomonedas' );
} finally {
setLoading ( false );
}
};
fetchData ();
}, []);
return { data , loading , error };
};
Hook Breakdown
Three pieces of state track the async operation: const [ data , setData ] = useState < CryptoApiResponse []>([]);
const [ loading , setLoading ] = useState ( true );
const [ error , setError ] = useState < string | null >( null );
data: Array of crypto objects
loading: Boolean for loading state
error: Error message or null
The effect runs once on mount (empty dependency array): useEffect (() => {
const fetchData = async () => {
const res = await fetch ( 'https://api.coinlore.net/api/tickers/' );
const json = await res . json ();
// Process data...
};
fetchData ();
}, []);
Try/catch/finally pattern manages all states: try {
// Fetch and set data
} catch ( err ) {
console . error ( 'Error al obtener criptomonedas:' , err );
setError ( 'Error al obtener criptomonedas' );
} finally {
setLoading ( false );
}
Using the Hook
The hook provides a simple API for components:
src/screens/HomeScreen.tsx:17-19
const HomeScreen = () => {
const { data , loading , error } = useCryptoData ();
// Use data, loading, error in component
};
Loading State
Error State
Success State
if ( loading ) {
return < ActivityIndicator size = "large" color = "#0000ff" /> ;
}
if ( error ) {
return < Text style = { styles . errorText } > { error } </ Text > ;
}
return (
< ScrollView >
{ data . map ( crypto => < CryptoCard key = { crypto . id } crypto = { crypto } /> ) }
</ ScrollView >
);
State Sharing Between Components
The HomeScreen demonstrates combining multiple state sources:
src/screens/HomeScreen.tsx:17-39
const HomeScreen = () => {
const { data , loading , error } = useCryptoData ();
const { filter } = useFilter ();
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 ;
});
return (
< ScrollView >
{ filtered . map (( crypto ) => (
< CryptoCard key = { crypto . id } crypto = { crypto } />
)) }
</ ScrollView >
);
};
Data Flow Diagram
Filtering Logic
Text Matching
Check if search text matches name or symbol: const nameMatch = crypto . name . toLowerCase (). includes ( filter . text . toLowerCase ());
const symbolMatch = crypto . symbol . toLowerCase (). includes ( filter . text . toLowerCase ());
Price Range Filtering
Apply minimum and maximum price filters: const minOk = filter . minPrice === null || Number ( crypto . price_usd ) >= filter . minPrice ;
const maxOk = filter . maxPrice === null || Number ( crypto . price_usd ) <= filter . maxPrice ;
Null values are treated as “no filter”.
Combine Conditions
Return true only if all conditions pass: return ( nameMatch || symbolMatch ) && minOk && maxOk ;
Local Component State
The HomeScreen also manages local UI state for scroll behavior:
src/screens/HomeScreen.tsx:25-57
const [ scrollY , setScrollY ] = useState ( 0 );
const scrollViewRef = useRef < ScrollView >( null );
const handleScroll = ( event : any ) => {
setScrollY ( event . nativeEvent . contentOffset . y );
};
const scrollToTop = () => {
if ( scrollViewRef . current ) {
scrollViewRef . current . scrollTo ({ y: 0 , animated: true });
}
};
return (
< View >
< ScrollView
ref = { scrollViewRef }
onScroll = { handleScroll }
scrollEventThrottle = { 16 }
>
{ /* Content */ }
</ ScrollView >
{ scrollY > 200 && (
< TouchableOpacity onPress = { scrollToTop } >
< Text > ↑ </ Text >
</ TouchableOpacity >
) }
</ View >
);
Local state is perfect for UI-specific state that doesn’t need to be shared. Using context for everything would be overkill.
State Management Patterns
When to Use Context
Global App State User preferences, theme, authentication
Shared Across Routes Filters, cart items, notification settings
Deep Component Trees Avoid prop drilling through many levels
Infrequent Updates Context is less optimized for rapid updates
When to Use Custom Hooks
Data Fetching Encapsulate API calls and loading states
Reusable Logic Share logic across multiple components
Side Effects Abstract complex useEffect logic
Computed Values Derive values from other state
When to Use Component State
Local UI State Toggle switches, form inputs, modals
Temporary State Doesn’t need to persist across routes
High-Frequency Updates Scroll position, animation values
Single Component Not shared with other components
Best Practices
Keep state as close as possible to where it’s used: // Bad: Global state for local UI
const GlobalContext = createContext ();
// Good: Local state for local UI
const [ isOpen , setIsOpen ] = useState ( false );
Always validate context exists before using: export const useFilter = () => {
const ctx = useContext ( FilterContext );
if ( ! ctx ) throw new Error ( 'useFilter must be used within FilterProvider' );
return ctx ;
};
Use TypeScript interfaces for all state: interface Filter {
text : string ;
minPrice : number | null ;
maxPrice : number | null ;
}
const [ filter , setFilter ] = useState < Filter >({ ... });
Memoize Expensive Computations
Use useMemo for expensive filtering/sorting: const filtered = useMemo (
() => data . filter ( crypto => /* complex filter */ ),
[ data , filter ]
);
Keep data fetching separate from UI state: // Data fetching hook
const { data , loading } = useCryptoData ();
// UI state
const [ scrollY , setScrollY ] = useState ( 0 );
Complete State Architecture
Here’s how all state management pieces work together:
State Layers
Layer Source Scope Example Global Context API App-wide User filters, theme Feature Custom Hooks Screen/feature API data, loading state Local useState Component Scroll position, form inputs
Next Steps
Architecture Review the overall app structure
Navigation Learn about Expo Router navigation