Skip to main content

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);
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.

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>
  );
};
1

Initialize State

Creates local state with default filter values (empty text, no price limits)
2

Provide Context Value

Wraps children with context provider, passing filter and setFilter
3

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();
}, []);
Raw API data is transformed into Crypto class instances:
const cryptos = json.data.map(
  (item: any) =>
    new Crypto(item.id, item.name, item.rank, 
               item.symbol, item.price_usd, item.percent_change_24h)
);
This provides type safety and computed properties like formattedPrice.
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
};
if (loading) {
  return <ActivityIndicator size="large" color="#0000ff" />;
}

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

1

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());
2

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”.
3

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>({ ... });
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

LayerSourceScopeExample
GlobalContext APIApp-wideUser filters, theme
FeatureCustom HooksScreen/featureAPI data, loading state
LocaluseStateComponentScroll position, form inputs

Next Steps

Architecture

Review the overall app structure

Navigation

Learn about Expo Router navigation

Build docs developers (and LLMs) love