Real Time Crypto Price Card List

Infinite-scrolling list of cryptocurrencies using Shopify's FlashList, with each item rendered as a customizable card showing price, trends, and a historical line graph. It fetches and paginates data from the CoinGecko API, handling api rate limiting, loading and error states.

react-nativeflashlistinfinite-scrollreact-querytypescriptdata-fetchinglist-viewcryptoperformancetanstack-queryfinancescreen

Preview

Installation

Step 1: Install Dependencies

npx expo install react-native-svg @shopify/flash-list

We will be using tanstack query for managing our queries

npm i @tanstack/react-query 

Step 2: Create a a file types/coinGecko.types.ts

types that model the JSON response structure from the CoinGecko API.

coinGecko.types.ts
/**
* Represents the currency-specific price data for various metrics.
*/
type CurrencyData = {
  aed: number;
  ars: number;
  aud: number;
  bch: number;
  bdt: number;
  bhd: number;
  bmd: number;
  bnb: number;
  brl: number;
  btc: number;
  cad: number;
  chf: number;
  clp: number;
  cny: number;
  czk: number;
  dkk: number;
  dot: number;
  eos: number;
  eth: number;
  eur: number;
  gbp: number;
  gel: number;
  hkd: number;
  huf: number;
  idr: number;
  ils: number;
  inr: number;
  jpy: number;
  krw: number;
  kwd: number;
  lkr: number;
  ltc: number;
  mmk: number;
  mxn: number;
  myr: number;
  ngn: number;
  nok: number;
  nzd: number;
  php: number;
  pkr: number;
  pln: number;
  rub: number;
  sar: number;
  sek: number;
  sgd: number;
  sol: number;
  thb: number;
  try: number;
  twd: number;
  uah: number;
  usd: number;
  vef: number;
  vnd: number;
  xag: number;
  xau: number;
  xdr: number;
  xlm: number;
  xrp: number;
  yfi: number;
  zar: number;
  bits: number;
  link: number;
  sats: number;
}

export type Currency = keyof CurrencyData;

export type TimeFrame = "24h" | "7d" | "30d" | "1y";

/**
 * Represents the currency-specific date data for ATH/ATL.
 */
type CurrencyDateData = {
  [currency: string]: string; // Allows any currency code as a key
}

/**
 * Represents links to code repositories.
 */
type ReposUrl = {
  github: string[];
  bitbucket: string[];
}

/**
 * Represents all external links related to the coin.
 */
type Links = {
  homepage: string[];
  whitepaper: string;
  blockchain_site: string[];
  official_forum_url: string[];
  chat_url: string[];
  announcement_url: string[];
  snapshot_url: string | null;
  twitter_screen_name: string;
  facebook_username: string;
  bitcointalk_thread_identifier: number | null;
  telegram_channel_identifier: string;
  subreddit_url: string;
  repos_url: ReposUrl;
}

/**
 * Represents different sizes of the coin's image.
 */
type Image = {
  thumb: string;
  small: string;
  large: string;
}

/**
 * Represents the detailed market data for the coin.
 */
type MarketData = {
  current_price: CurrencyData;
  total_value_locked: null; // Or a specific type if it can have a value
  mcap_to_tvl_ratio: null;
  fdv_to_tvl_ratio: null;

  // All-time high price in various currencies
  ath: CurrencyData;
  // Percentage change from all-time high in various currencies
  ath_change_percentage: CurrencyData;
  // Date of all-time high in various currencies
  ath_date: CurrencyDateData;

  // All-time low price in various currencies
  atl: CurrencyData;
  // Percentage change from all-time low in various currencies
  atl_change_percentage: CurrencyData;
  // Date of all-time low in various currencies
  atl_date: CurrencyDateData;

  // Market capitalization in various currencies
  market_cap: CurrencyData;
  market_cap_rank: number;
  
  // Fully diluted valuation in various currencies
  fully_diluted_valuation: CurrencyData;
  market_cap_fdv_ratio: number;
  
  // Total volume in various currencies
  total_volume: CurrencyData;

  // High and low price in the last 24h in various currencies
  high_24h: CurrencyData;
  low_24h: CurrencyData;
  
  price_change_24h: number;
  price_change_percentage_24h: number;
  price_change_percentage_7d: number;
  price_change_percentage_14d: number;
  price_change_percentage_30d: number;
  price_change_percentage_60d: number;
  price_change_percentage_200d: number;
  price_change_percentage_1y: number;

  market_cap_change_24h: number;
  market_cap_change_percentage_24h: number;

  // Price changes over various time frames in different currencies
  price_change_24h_in_currency: CurrencyData;
  price_change_percentage_1h_in_currency: CurrencyData;
  price_change_percentage_24h_in_currency: CurrencyData;
  price_change_percentage_7d_in_currency: CurrencyData;
  price_change_percentage_14d_in_currency: CurrencyData;
  price_change_percentage_30d_in_currency: CurrencyData;
  price_change_percentage_60d_in_currency: CurrencyData;
  price_change_percentage_200d_in_currency: CurrencyData;
  price_change_percentage_1y_in_currency: CurrencyData;
  
  market_cap_change_24h_in_currency: CurrencyData;
  market_cap_change_percentage_24h_in_currency: CurrencyData;
  
  total_supply: number;
  max_supply: number | null;
  circulating_supply: number;
  last_updated: string;
}

/**
 * The main type for the full response from the /coins/{id} endpoint.
 */
export type CoinDetail = {
  id: string;
  symbol: string;
  name: string;
  web_slug: string;
  asset_platform_id: null; // Or a specific type
  platforms: { [key: string]: string };
  detail_platforms: { [key: string]: { decimal_place: number | null; contract_address: string } };
  block_time_in_minutes: number;
  hashing_algorithm: string;
  categories: string[];
  preview_listing: boolean;
  public_notice: null; // Or string
  additional_notices: any[];
  description: { [language_code: string]: string };
  links: Links;
  image: Image;
  country_origin: string;
  genesis_date: string; // ISO 8601 date string
  sentiment_votes_up_percentage: number;
  sentiment_votes_down_percentage: number;
  watchlist_portfolio_users: number;
  market_cap_rank: number;
  market_data: MarketData;
  status_updates: any[]; // Or a specific type if you know the structure
  last_updated: string; // ISO 8601 date string
}

Step 3: Add a utility throttledFetch.ts

An utility that wraps the standard fetch call to control the frequency of API requests, preventing rate-limit errors from api

throttledFetch.ts
// This queue will hold all pending requests.
// Each item will be a promise's resolve/reject functions and the fetch arguments.
const requestQueue: {
   url: string;
   options: RequestInit;
   resolve: (value: any) => void;
   reject: (reason?: any) => void;
}[] = [];

let isProcessing = false;
const RATE_LIMIT_INTERVAL = 2100; // 30 requests/min is 1 every 2s. We add a 100ms buffer.

/**
* Processes the request queue one by one, with a delay between each request.
*/
function processQueue() {
   if (isProcessing) return; // Don't start a new interval if one is already running
   isProcessing = true;

   const intervalId = setInterval(async () => {
   if (requestQueue.length === 0) {
       // If the queue is empty, stop the interval and mark as not processing.
       clearInterval(intervalId);
       isProcessing = false;
       return;
   }

   // Get the next request from the front of the queue.
   const nextRequest = requestQueue.shift();
   if (nextRequest) {
       try {
       const response = await fetch(nextRequest.url, nextRequest.options);
       if (!response.ok) {
           const errorData = await response.json();
           // Reject the specific promise for this request with the error.
           nextRequest.reject(new Error(errorData.error || 'API request failed'));
       } else {
           const data = await response.json();
           // Resolve the specific promise for this request with the data.
           nextRequest.resolve(data);
       }
       } catch (error) {
       nextRequest.reject(error);
       }
   }
   }, RATE_LIMIT_INTERVAL);
}

/**
* A throttled version of fetch. Instead of fetching immediately, it adds
* the request to a queue that is processed at a rate-limit-friendly speed.
* @param url The URL to fetch.
* @param options The fetch options.
* @returns A promise that resolves/rejects when the request is eventually processed.
*/
export const throttledFetch = (url: string, options: RequestInit): Promise<any> => {
return new Promise((resolve, reject) => {
 // Add the new request to the end of the queue.
 requestQueue.push({ url, options, resolve, reject });
 // Start processing the queue if it's not already running.
 processQueue();
});
};

Step 4: Create a file hooks/useCryptoData.ts

Provides a collection of custom TanStack Query hooks to fetch, cache, and manage cryptocurrency market data, price history, and details from the CoinGecko API.

useCryptoData.ts
import { useQuery, useInfiniteQuery } from '@tanstack/react-query';
import { throttledFetch } from '@/lib/apiClient';
import { CoinDetail } from '@/types/coinGecko.types';
import { Currency, TimeFrame } from "@/types/coinGecko.types";

export type Roi = {
times: number;
currency: string;
percentage: number;
}

// types
export type Coin = {
id: string;
symbol: string;
name: string;
image: string;
current_price: number;
market_cap: number;
market_cap_rank: number;
fully_diluted_valuation: number | null;
total_volume: number;
high_24h: number;
low_24h: number;
price_change_24h: number;
price_change_percentage_24h: number;
market_cap_change_24h: number;
market_cap_change_percentage_24h: number;
circulating_supply: number;
total_supply: number | null;
max_supply: number | null;
ath: number;
ath_change_percentage: number;
ath_date: string;
atl: number;
atl_change_percentage: number;
atl_date: string;
roi: Roi | null;
last_updated: string;
price_change_percentage_24h_in_currency: number;
price_change_percentage_7d_in_currency: number;
price_change_percentage_30d_in_currency: number;
price_change_percentage_1y_in_currency: number;
}

// Type for the full API response object
export type CoinApiResponse = {
data: Coin[];
page: number;
pageSize: number;
totalItems: number;
totalPages: number;
}

const COINGECKO_API_KEY = 'your-api-key';

/**
* Fetches a paginated list of all coins from the CoinGecko API.
*
* @param page - The page number to fetch (default: 1)
* @param pageSize - The number of coins per page (default: 10)
* @param timeFrame - The time frame for price change percentage (default: '24h'). 
*                    Accepts '24h', '7d', '30d' or 1y.
* @returns A Promise that resolves to an array of Coin objects.
*/
const fetchAllCoins = async (page: number = 1, pageSize: number = 10, timeFrame: TimeFrame = '24h', currency: Currency): Promise<Coin[]> => {
const url = `https://api.coingecko.com/api/v3/coins/markets?vs_currency=${currency}&price_change_percentage=${timeFrame}&order=market_cap_desc&per_page=${pageSize}&page=${page}`;

const options = {
method: 'GET',
headers: {
  'x-cg-demo-api-key': COINGECKO_API_KEY,
},
};

try {
const response = await fetch(url, options);

if (!response.ok) {
  // If the response is not OK, try to read it as text first.
  const errorText = await response.text();
  console.error("API Error Response Text:", errorText);

  if (errorText.toLowerCase().includes('throttled')) {
    throw new Error('Rate limit exceeded. Please wait a moment.');
  }
  
  // If it's something else, try to parse it as JSON, or just throw the text.
  try {
      const errorJson = JSON.parse(errorText);
      throw new Error(errorJson.error || 'An unknown API error occurred');
  } catch (e) {
      throw new Error(`API returned status ${response.status}: ${errorText}`);
  }
}

return response.json();

} catch (error) {
// This will catch network errors (e.g., no internet)
console.error("Fetch operation failed:", error);
throw error; // Re-throw the error for TanStack Query to handle
}
};


/**
* Fetches the OHLC price history for a single coin from the official CoinGecko API.
* @param coinId - The string ID of the coin (e.g., "bitcoin")
* @param days - The number of days for the history (e.g., 7)
*/
const fetchCoinOhlc = async (coinId: string, days: number = 7, currency: Currency): Promise<number[]> => {
// Construct the URL using the coinId string directly
const url = `https://api.coingecko.com/api/v3/coins/${coinId}/ohlc?vs_currency=${currency}&days=${days}`;

const options = {
    method: 'GET',
    headers: {
    'x-cg-demo-api-key': COINGECKO_API_KEY,
    },
};

const response = await fetch(url, options);
if (!response.ok) {
const errorData = await response.json();
console.error("CoinGecko OHLC API Error:", errorData);
throw new Error(errorData.error || 'Failed to fetch OHLC data');
}

// The new data structure is an array of arrays: [timestamp, open, high, low, close]
const data: [number, number, number, number, number][] = await throttledFetch(url, options); //throttledFetch is you using free version of the api, added due to rate limits 

// We need to extract the 5th element (index 4), which is the closing price.
return data.map(pricePoint => pricePoint[4]);
};



const fetchSingleCoin = async (coinIdentifier: string): Promise<CoinDetail> => {
const url = `https://api.coingecko.com/api/v3/coins/${coinIdentifier}`;
const options = {
    method: 'GET',
    headers: { 'x-cg-demo-api-key': COINGECKO_API_KEY, },
};

const response = await fetch(url, options);
if (!response.ok) {
    const errorData = await response.json();
    throw new Error(errorData.error || 'Failed to fetch coin details');
}

// The function now returns the full, strongly-typed object.
return response.json();
};

// Hooks 

export function useInfiniteCoinsQuery(timeFrame: TimeFrame, currency: Currency) {
return useInfiniteQuery({
    queryKey: ['allCoins'],
    queryFn: ({ pageParam = 1 }) => fetchAllCoins(pageParam, 10, timeFrame, currency),
    initialPageParam: 1,
    getNextPageParam: (lastPage, allPages) => {
    // If the last page had items, request the next page.
    // If the last page was empty, return undefined to stop fetching.
    return lastPage.length > 0 ? allPages.length + 1 : undefined;
    },
});
}

// Hook (used inside each card)
export function useCoinPriceHistoryQuery(coinId: string, timeFrame: TimeFrame, currency: Currency) {
let days: number;
switch (timeFrame) {
    case '24h': days = 1; break;
    case '7d': days = 7; break;
    case '30d': days = 30; break;
    case '1y': days = 365; break;
    default: days = 7;
}
return useQuery({
    queryKey: ['coinOhlc', coinId],
    queryFn: () => fetchCoinOhlc(coinId, days, currency),
    staleTime: 1000 * 60 * 5, // you can remove and add like refresh control on demand
    refetchInterval: 1000 * 60 * 5, // you can remove and add like refresh control on demand
    enabled: !!coinId,
    retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), //this is to slow down if you are using free version of the api
    retry: 2,
});
}

// Hook to fetch a single coin by name/symbol
export function useSingleCoinQuery(coinIdentifier: string) {
return useQuery<CoinDetail, Error>({
    queryKey: ['singleCoin', coinIdentifier.toLowerCase()],
    queryFn: () => fetchSingleCoin(coinIdentifier),
    staleTime: 1000 * 60 * 2, // you can remove and add like refresh control on demand
    refetchInterval: 1000 * 60 * 1, // you can remove and add like refresh control on demand
    enabled: !!coinIdentifier, // Only fetch if coinIdentifier is provided
    retry: 2,
});
}

Step 5: Create a line graph component for the charts LineGraph.tsx

uses React-native-svg to plot path

LineGraph.tsx
import React, { useMemo } from "react";
import { View } from "react-native";
import Svg, { Path, Defs, LinearGradient, Stop, Circle } from "react-native-svg";

type LineGraphProps = {
data: number[];
width: number;
height: number;
color?: string;
};
const VERTICAL_PADDING = 4;
const HORIZONTAL_PADDING = 2;

/**
* Generates an SVG path string for a line graph based on the provided data points.
*
* - Each data point is mapped to an (x, y) coordinate:
*    - x is spaced evenly across the width (minus horizontal padding).
*    - y is scaled so the min value is at the bottom and the max at the top, with vertical padding.
* - The path starts with 'M' (move to) for the first point, then 'L' (line to) for each subsequent point.
*
* @param data - Array of y-values (numbers) to plot.
* @param width - Width of the SVG area.
* @param height - Height of the SVG area.
* @returns SVG path string for the line graph.
*/
const createPath = (data: number[], width: number, height: number): string => {
if (!data || data.length < 2) {
	return `M 0,${height} L ${width},${height}`;
}

const min = Math.min(...data);
const max = Math.max(...data);
const range = max - min || 1; // Prevent division by zero

return data.reduce((acc, point, i) => {
	const x = (i / (data.length - 1)) * (width - 2 * HORIZONTAL_PADDING);
	// The y value is now offset by VERTICAL_PADDING at both ends
	const y =
		VERTICAL_PADDING + (height - 2 * VERTICAL_PADDING - ((point - min) / range) * (height - 2 * VERTICAL_PADDING));
	return i === 0 ? `M ${x},${y}` : `${acc} L ${x},${y}`;
}, "");
};

const getLastPointCoordinates = (data: number[], width: number, height: number) => {
if (!data || data.length === 0) {
	return { x: width, y: height };
}
const min = Math.min(...data);
const max = Math.max(...data);
const range = max - min || 1;

const lastPoint = data[data.length - 1];
const x = width - 2 * HORIZONTAL_PADDING;
const y =
	VERTICAL_PADDING + (height - 2 * VERTICAL_PADDING) - ((lastPoint - min) / range) * (height - 2 * VERTICAL_PADDING);

return { x, y };
};

export const LineGraph = ({ data, width, height, color = "#C9FE71" }: LineGraphProps) => {
const linePath = useMemo(() => createPath(data, width, height), [data, width, height]);

// The path for the gradient area is also memoized.
const areaPath = useMemo(
	() => `${linePath} L ${width - 2 * HORIZONTAL_PADDING},${height} L ${HORIZONTAL_PADDING},${height} Z`,
	[linePath, width, height]
);

const endPoint = useMemo(() => getLastPointCoordinates(data, width, height), [data, width, height]);

return (
	<View style={{ width, height }}>
		<Svg width={width} height={height}>
			<Defs>
				<LinearGradient id="gradient" x1="50%" y1="0%" x2="50%" y2="100%">
					<Stop offset="0%" stopColor={color} stopOpacity={0.3} />
					<Stop offset="100%" stopColor={color} stopOpacity={0} />
				</LinearGradient>
			</Defs>
			<Path d={areaPath} fill="url(#gradient)" />
			<Path
				d={linePath}
				fill="transparent"
				stroke={color}
				strokeWidth={2.5}
				strokeLinejoin="round"
				strokeLinecap="round"
			/>
			<Circle
				cx={endPoint.x}
				cy={endPoint.y}
				// animatedProps={animatedGlowProps}
				r={5}
				fill={color}
				opacity={0.5}
			/>
			<Circle cx={endPoint.x} cy={endPoint.y} r={2} fill={color} />
		</Svg>
	</View>
);
};

Step 6: Create file CryptoCard.tsx

Import LineGraph, useCryptoData hook and types created above please check imports it may not be same as yours thank you

CryptoCard.tsx
import React, { useMemo } from "react";
import { View, Text, StyleSheet, ActivityIndicator, Image } from "react-native";
import { useCoinPriceHistoryQuery, Coin } from "@/hooks/useCryptoData";
import { LineGraph } from "./LineGraph";
import { Currency, TimeFrame } from "@/types/coinGecko.types";

type CryptoCardProps = {
coin: Coin;
timeFrame?: TimeFrame;
currency?: Currency
positiveColor?: string;
negativeColor?: string;
cardBgColor?: string;
symbolColor?: string;
nameColor?: string;
priceColor?: string;
loadingColor?: string;
timeFrameColor?: string;
};

export const CryptoCard = ({
coin,
timeFrame = "24h",
currency = "usd",
positiveColor = "#C9FE71",
negativeColor = "#FF6B6B",
cardBgColor = "#1E1E1E",
symbolColor = "#FFFFFF",
nameColor = "#8A8A8A",
priceColor = "#FFFFFF",
loadingColor = "#C9FE71",
timeFrameColor = "#2C2F2F",
}: CryptoCardProps) => {
const { data: history, isLoading, isError } = useCoinPriceHistoryQuery(coin.id, timeFrame, currency);

const displayData = useMemo(() => {
    if (!coin) {
        // Return default values if coin data is not yet loaded
        return {
            price: 0,
            percentageChange: 0,
            isPositive: true,
        };
    }

    // Select the correct percentage change based on the timeFrame prop
    let percentageChange: number;
    switch (timeFrame) {
        case '24h':
            percentageChange = coin.price_change_percentage_24h_in_currency;
            break;
        case '7d':
            percentageChange = coin.price_change_percentage_7d_in_currency;
            break;
        case '30d':
            percentageChange = coin.price_change_percentage_30d_in_currency;
            break;
        case '1y':
            percentageChange = coin.price_change_percentage_1y_in_currency;
            break;
        default:
            percentageChange = coin.price_change_percentage_24h;
    }

    return {
        price: coin.current_price,
        percentageChange: percentageChange || 0, // Fallback to 0 if the value is null/undefined
        isPositive: (percentageChange || 0) >= 0,
    };

}, [coin, timeFrame]);

const formattedPrice = useMemo(() => {
	const price = displayData.price;

	const options: Intl.NumberFormatOptions = {
		style: "currency",
		currency: currency.toUpperCase(),
	};

	if (price >= 100000) {
		options.minimumFractionDigits = 0;
		options.maximumFractionDigits = 0;
	} else if (price >= 1) {
		options.minimumFractionDigits = 2;
		options.maximumFractionDigits = 2;
	} else if (price > 0) {
		// 1. Calculate the number of leading zeros after the decimal.
		// Math.log10(0.000013689) is approx -4.86. Flipping and flooring gives 4.
		const numberOfLeadingZeros = Math.floor(-Math.log10(price));
		// 2. Set the maximum fraction digits to be the number of zeros plus the 2 significant digits you want.
		options.maximumFractionDigits = numberOfLeadingZeros + 2;
		// We still set a minimum to avoid showing just "$0.00" for slightly larger fractions.
		options.minimumFractionDigits = 2;

	} else {
		// For a price of exactly 0, use standard formatting.
		options.minimumFractionDigits = 2;
		options.maximumFractionDigits = 2;
	}

	return new Intl.NumberFormat("en-US", options).format(price);

}, [displayData.price, currency])

const percentageColor = displayData.isPositive ? positiveColor : negativeColor;
const percentageBackgroundColor = displayData.isPositive ? `${positiveColor}26` : `${negativeColor}26`;

return (
	<View style={[styles.card, { backgroundColor: cardBgColor }]}>
		<View style={styles.header}>
			<View style={styles.titleContainer}>
				<Image source={{ uri: coin.image }} style={styles.icon} />
				<View style={styles.textInfoContainer}>
					<Text style={[styles.symbol, { color: symbolColor }]} numberOfLines={1}>
						{coin.symbol.toUpperCase()}
					</Text>
					<Text style={[styles.name, { color: nameColor }]} numberOfLines={1}>
						{coin.name}
					</Text>
				</View>
			</View>

			<View style={{ flexDirection: "row", gap: 8 }}>
				<View style={[styles.changeContainer, { backgroundColor: timeFrameColor }]}>
					<Text style={[styles.timeFrameText, { color: nameColor }]}>{timeFrame}</Text>
				</View>
				<View style={[styles.changeContainer, { backgroundColor: percentageBackgroundColor }]}>
					<Text style={[styles.changeText, { color: percentageColor }]}>
						{displayData.isPositive ? "+" : ""}
						{displayData.percentageChange.toFixed(2)} %
					</Text>
				</View>
			</View>
		</View>

		<View style={styles.footer}>
			<Text style={[styles.price, { color: priceColor }]}>{formattedPrice}</Text>
			<View style={styles.graphContainer}>
				{isLoading ? (
					<ActivityIndicator color={loadingColor} />
				) : isError || !history ? (
					<Text style={[styles.errorText, { color: negativeColor }]}>Chart unavailable</Text>
				) : (
					<LineGraph data={history} color={percentageColor} width={150} height={50} />
				)}
			</View>
		</View>
	</View>
);
};

const styles = StyleSheet.create({
card: {
	borderRadius: 24,
	padding: 20,
	marginVertical: 8,
},
header: {
	flexDirection: "row",
	justifyContent: "space-between",
	alignItems: "flex-start",
	marginBottom: 22,
},
titleContainer: {
	flexDirection: "row",
	alignItems: "center",
	flex: 1,
},
icon: {
	width: 40,
	height: 40,
	borderRadius: 20,
	marginRight: 12,
},
timeFrameText: {
	fontWeight: "600",
	fontSize: 14,
},
textInfoContainer: {
	flex: 1,
},
symbol: {
	fontSize: 20,
	fontWeight: "600",
},
name: {
	fontSize: 15,
},
changeContainer: {
	borderRadius: 12,
	paddingHorizontal: 10,
	paddingVertical: 6,
},
changeText: {
	fontWeight: "600",
	fontSize: 14,
},
footer: {
	flexDirection: "row",
	justifyContent: "space-between",
	alignItems: "flex-end",
},
price: {
	fontSize: 26,
	fontWeight: "700",
},
graphContainer: {
	width: 150,
	height: 50,
	justifyContent: "center",
	alignItems: "center",
},
errorText: {
	fontSize: 12,
},
});

Usage

CoinListPage.tsx
import React, { useMemo } from 'react';
import { SafeAreaView, StyleSheet, View, Text, ActivityIndicator } from 'react-native';
import { FlashList } from '@shopify/flash-list';
import { useInfiniteCoinsQuery, Coin } from '../hooks/useCryptoData';
import { CryptoCard } from '@/components/ui/CryptoCard';
import { Currency, TimeFrame } from '@/types/coinGecko.types';
import { useAppColors } from '@/hooks/useAppColors';

type ErrorComponentProps = {
error?: Error | null;
}

const CURRENCY: Currency = "usd"; //Change this to your liking
const TIME_FRAME: TimeFrame = "24h"; //Change this to your liking


export default function CoinListPage() {
const colors = useAppColors();
const {
  data,
  fetchNextPage,
  hasNextPage,
  isFetchingNextPage,
  isLoading,
  isError,
  error
} = useInfiniteCoinsQuery(TIME_FRAME, CURRENCY);

const coins = useMemo(() => {
  if (!data?.pages) return [];
  
  // 1. Flatten all the pages into a single array of coins.
  const allCoins = data.pages.flat();
  
  // 2. Create a Set of seen IDs to track duplicates.
  const seenIds = new Set<string>();
  
  // 3. Filter the flattened array, only keeping the first occurrence of each coin.
  return allCoins.filter(coin => {
    if (seenIds.has(coin.id)) {
      // If we've already seen this coin ID, it's a duplicate. Skip it.
      return false;
    } else {
      // If it's the first time we've seen this ID, add it to the set and keep it.
      seenIds.add(coin.id);
      return true;
    }
  });
}, [data]);



if (isLoading) {
  return <View style={styles.center}><ActivityIndicator size="large" /></View>;
}
if (isError) {
  return <ErrorComponent error={error} />;
}


return (
  <SafeAreaView style={styles.container}>
    <Text style={[styles.title, { color: colors.Neutral900 }]}>Coin List {CURRENCY}, {TIME_FRAME}</Text>
    <FlashList
      data={coins}
      renderItem={({ item }) => <CryptoCard coin={item} currency={CURRENCY} timeFrame={TIME_FRAME} />}
      keyExtractor={(item: Coin) => item.id.toString()}
      estimatedItemSize={150}
      onEndReached={() => {
        if (hasNextPage) fetchNextPage();
      }}
      onEndReachedThreshold={0.5}
      ListFooterComponent={isFetchingNextPage ? <ActivityIndicator style={{ margin: 20 }} /> : null}
      contentContainerStyle={styles.listContent}
    />
  </SafeAreaView>
);
};

function ErrorComponent({ error }: ErrorComponentProps) {
return (
  <View style={[styles.center, { padding: 24 }]}>
    <View style={styles.errorContainer}>
      <Text style={styles.errorTitle}>
        Oops!
      </Text>
      <Text style={styles.errorMessage}>
        Failed to load coins.
      </Text>
      <Text style={styles.errorDetails}>
        {error?.message || "Unknown error"}
      </Text>
    </View>
  </View>
);
}

const styles = StyleSheet.create({
container: {
  flex: 1,
  backgroundColor: '#000000',
},
listContent: {
  paddingHorizontal: 16,
},
title: {
  	fontSize: 24,
  	fontWeight: "bold",
  	marginBottom: 30,
  	textAlign: "center",
  }, 
center: {
  flex: 1,
  justifyContent: 'center',
  alignItems: 'center',
  backgroundColor: '#000000',
},
errorContainer: {
  backgroundColor: "#1E1E1E",
  borderRadius: 24,
  padding: 28,
  alignItems: "center",
  maxWidth: 340,
},
errorTitle: {
  color: "#FF6B6B",
  fontSize: 22,
  fontWeight: "700",
  marginBottom: 10,
  textAlign: "center",
},
errorMessage: {
  color: "#fff",
  fontSize: 16,
  marginBottom: 8,
  textAlign: "center",
},
errorDetails: {
  color: "#FF6B6B",
  fontSize: 14,
  textAlign: "center",
},
errorText: {
  color: 'white',
  fontSize: 16,
},
});

Props

PropTypeDefaultRequiredDescription
coinCoinYesThe coin data object containing details like ID, name, symbol, image, and price.
timeFrame"24h" | "7d" | "30d" | "1y"'24h'NoThe time period for the price history graph and percentage change calculation.
currencyCurrency'usd'NoThe currency for displaying prices and market data.
positiveColorstring'#C9FE71'NoThe color for the line graph and percentage text when the price change is positive.
negativeColorstring'#FF6B6B'NoThe color for the line graph and percentage text when the price change is negative.
cardBgColorstring'#1E1E1E'NoThe background color of the card component.
symbolColorstring'#FFFFFF'NoThe color of the coin's symbol text (e.g., 'BTC').
nameColorstring'#8A8A8A'NoThe color of the coin's full name text (e.g., 'Bitcoin').
priceColorstring'#FFFFFF'NoThe color of the main price display text.
loadingColorstring'#C9FE71'NoThe color of the ActivityIndicator shown while the line graph is loading.
timeFrameColorstring'#2C2F2F'NoThe background color of the time frame indicator badge.