Skip to main content

Overview

CryptoTracker provides powerful filtering capabilities that allow users to search for cryptocurrencies by name or symbol, and filter by price range. The filtering system uses React Context for state management and implements debounced search for optimal performance.

FilterContext Architecture

The filtering system is built around the FilterContext, which provides centralized filter state management across the application.

Filter Interface

FilterContext.tsx
export interface Filter {
  text: string;           // Search text for name/symbol
  minPrice: number | null; // Minimum price filter
  maxPrice: number | null; // Maximum price filter
}

Context Setup

FilterContext.tsx
type FilterContextType = {
  filter: Filter;
  setFilter: React.Dispatch<React.SetStateAction<Filter>>;
};

const FilterContext = createContext<FilterContextType | undefined>(undefined);

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

useFilter Hook

The custom useFilter hook provides type-safe access to the filter context:
export const useFilter = () => {
  const ctx = useContext(FilterContext);
  
  if (!ctx) {
    throw new Error('useFilter debe usarse dentro de FilterProvider');
  }
  
  return ctx;
};
The hook throws an error if used outside of FilterProvider, ensuring proper context usage throughout the app.

SearchBar Component

The SearchBar component provides the UI for entering filter criteria with three input fields.

Implementation

SearchBar.tsx
const SearchBar = ({ onSearch }: Props) => {
  const [text, setText] = useState('');
  const [minPrice, setMinPrice] = useState<number | null>(null);
  const [maxPrice, setMaxPrice] = useState<number | null>(null);

  useEffect(() => {
    const timeout = setTimeout(() => {
      onSearch({
        text,
        minPrice: minPrice ? minPrice : null,
        maxPrice: maxPrice ? maxPrice : null,
      });
    }, 300);

    return () => clearTimeout(timeout);
  }, [text, minPrice, maxPrice]);

  return (
    <View style={styles.container}>
      <TextInput
        placeholder="Buscar criptomoneda..."
        value={text}
        onChangeText={setText}
        style={styles.input}
      />
      <TextInput
        placeholder="Precio mínimo"
        value={minPrice?.toString() ?? ''}
        onChangeText={(val) => {
          const parsed = parseFloat(val);
          setMinPrice(isNaN(parsed) ? null : parsed);
        }}
        style={styles.input}
        keyboardType="numeric"
      />
      <TextInput
        placeholder="Precio máximo"
        value={maxPrice?.toString() ?? ''}
        onChangeText={(val) => {
          const parsed = parseFloat(val);
          setMaxPrice(isNaN(parsed) ? null : parsed);
        }}
        style={styles.input}
        keyboardType="numeric"
      />
    </View>
  );
};
The SearchBar implements a 300ms debounce delay to prevent excessive re-renders and API calls while users are typing.
The useEffect hook sets up a timeout that triggers the onSearch callback only after the user stops typing for 300 milliseconds:
useEffect(() => {
  const timeout = setTimeout(() => {
    onSearch({ text, minPrice, maxPrice });
  }, 300);

  return () => clearTimeout(timeout); // Cleanup on unmount or value change
}, [text, minPrice, maxPrice]);

Input Styling

const styles = StyleSheet.create({
  container: {
    backgroundColor: '#fff',
    padding: 12,
    borderRadius: 12,
    marginBottom: 16,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 6,
    elevation: 4,
  },
  input: {
    fontSize: 16,
    color: '#333',
    borderWidth: 1,
    marginBottom: 5,
    marginTop: 5,
    borderColor: '#ddd',
    paddingHorizontal: 12,
    paddingVertical: 8,
    borderRadius: 8,
  },
});

Filter Logic in HomeScreen

The HomeScreen component applies the filter criteria to the cryptocurrency data:
HomeScreen.tsx
const HomeScreen = () => {
  const { data, loading, error } = useCryptoData();
  const { filter } = useFilter();

  // Apply filters to cryptocurrency data
  const filtered = data.filter((crypto) => {
    // Text search: match name OR symbol (case-insensitive)
    const nameMatch = crypto.name
      .toLowerCase()
      .includes(filter.text.toLowerCase());
    const symbolMatch = crypto.symbol
      .toLowerCase()
      .includes(filter.text.toLowerCase());

    // Price range filters
    const minOk = filter.minPrice === null || 
      Number(crypto.price_usd) >= filter.minPrice;
    const maxOk = filter.maxPrice === null || 
      Number(crypto.price_usd) <= filter.maxPrice;

    // All conditions must be satisfied
    return (nameMatch || symbolMatch) && minOk && maxOk;
  });

  return (
    <ScrollView>
      {filtered.map((crypto) => (
        <CryptoCard key={crypto.id} crypto={crypto} />
      ))}
    </ScrollView>
  );
};

How Filtering Works

The filter logic uses a multi-stage approach:
1

Text Matching

Checks if the search text appears in either the cryptocurrency name or symbol (case-insensitive).
const nameMatch = crypto.name.toLowerCase().includes(filter.text.toLowerCase());
const symbolMatch = crypto.symbol.toLowerCase().includes(filter.text.toLowerCase());
2

Minimum Price Check

If a minimum price is set, ensures the cryptocurrency price is greater than or equal to it.
const minOk = filter.minPrice === null || Number(crypto.price_usd) >= filter.minPrice;
3

Maximum Price Check

If a maximum price is set, ensures the cryptocurrency price is less than or equal to it.
const maxOk = filter.maxPrice === null || Number(crypto.price_usd) <= filter.maxPrice;
4

Combined Logic

A cryptocurrency is shown if it matches the text (name OR symbol) AND satisfies both price constraints.
return (nameMatch || symbolMatch) && minOk && maxOk;

Filter Examples

// User types "bit" in search box
filter = { text: 'bit', minPrice: null, maxPrice: null }

// Results: Bitcoin, BitTorrent, Bitshares, etc.

Performance Optimizations

Debouncing

The 300ms debounce prevents excessive filtering operations while typing:
  • Without debounce: Filter runs on every keystroke (typing “bitcoin” = 7 operations)
  • With debounce: Filter runs once after user stops typing (1 operation)

Null Handling

Price filters use null checks to skip unnecessary comparisons when no price range is set:
const minOk = filter.minPrice === null || Number(crypto.price_usd) >= filter.minPrice;
If minPrice is null, the expression short-circuits and returns true without evaluating the price comparison. Both search terms and cryptocurrency names/symbols are converted to lowercase once per iteration:
const lowerText = filter.text.toLowerCase();
const nameMatch = crypto.name.toLowerCase().includes(lowerText);

Key Features

  • Real-time Filtering: Results update as users type (with debouncing)
  • Multi-field Search: Matches both name and symbol fields
  • Price Range: Filter by minimum and/or maximum price
  • Context-based State: Centralized filter state accessible throughout the app
  • Type Safety: TypeScript interfaces ensure correct filter structure
  • Performance Optimized: Debounced inputs and efficient filter logic
  • Flexible Filters: Each filter criterion is optional and can be used independently

Build docs developers (and LLMs) love