การสร้างตารางการ์ดแบบตอบสนอง
ใช้การตัดบรรทัดของ Flexbox และความกว้างแบบเปอร์เซ็นต์เพื่อสร้างตารางการ์ดสองคอลัมน์ที่แสดงผลได้เหมาะสมทั้งบนโทรศัพท์ขนาดเล็กและแท็บเล็ตขนาดใหญ่
การสร้างตารางการ์ดแบบตอบสนอง เป็นบทเรียน React Native Academy ฟรีบน CoddyKit นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน React Native Academy และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส React Native Academy มีบทเรียนทั้งหมด 4 บทเรียน
บางส่วนของบทเรียนนี้ยังไม่ได้รับการแปล และแสดงเป็นภาษาอังกฤษ
Card Grid Architecture Overview
A card grid is one of the most common UI patterns in mobile apps — from product catalogs to photo galleries and app dashboards. In React Native, you build it with a combination of flexDirection: 'row', flexWrap: 'wrap', width percentages, and the gap property. The challenge is making the grid look correct on small iPhones (320–375pt wide), large Android phones (392–430pt), and tablets (768pt+). By computing card dimensions from the screen width, the grid adapts to every device size automatically.
// The pattern: percentage-width cards in a wrapping row
import { View, useWindowDimensions } from 'react-native';
const COLUMNS = 2;
const GAP = 12;
const PADDING = 16;
export default function CardGrid({ items }) {
const { width } = useWindowDimensions();
const cardWidth = (width - PADDING * 2 - GAP * (COLUMNS - 1)) / COLUMNS;
return (
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: GAP, padding: PADDING }}>
{items.map(item => <Card key={item.id} width={cardWidth} data={item} />)}
</View>
);
}Calculating Card Width Precisely
To fit exactly N columns with consistent gutters, calculate card width as: (containerWidth - horizontalPadding * 2 - gap * (N - 1)) / N. For two columns with 16pt padding on each side and 12pt gap: (screenWidth - 32 - 12) / 2. This formula ensures the cards and gutters sum exactly to the container width, producing a pixel-perfect grid without rounding artifacts. Using useWindowDimensions means this recalculates when the screen width changes (e.g., device rotation or iPad split view).
import { useWindowDimensions } from 'react-native';
function useGridLayout({ columns = 2, padding = 16, gap = 12 } = {}) {
const { width } = useWindowDimensions();
const cardWidth = (width - padding * 2 - gap * (columns - 1)) / columns;
return { cardWidth, numColumns: columns };
}
// Usage:
function MyGrid() {
const { cardWidth } = useGridLayout({ columns: 2, padding: 16, gap: 12 });
// cardWidth updates automatically on rotation
return <>{/* render cards with width: cardWidth */}</>;
}Building the Card Component
Each grid card is a self-contained component with an image, title, and optional subtitle. Use borderRadius for rounded corners, a shadow or elevation for depth, and overflow: 'hidden' to clip the image to the card shape. The image height is set with aspectRatio so it scales proportionally to the card width. Accept the card width as a prop from the grid parent — the card doesn't need to know about the screen; it just fills whatever width it's given.
import { View, Text, Image, StyleSheet } from 'react-native';
export default function ProductCard({ item, width }) {
return (
<View style={[styles.card, { width }]}>
<Image
source={{ uri: item.imageUri }}
style={styles.image}
/>
<View style={styles.info}>
<Text style={styles.title} numberOfLines={2}>{item.name}</Text>
<Text style={styles.price}>${item.price.toFixed(2)}</Text>
</View>
</View>
);
}
const styles = StyleSheet.create({
card: { backgroundColor: '#fff', borderRadius: 14, overflow: 'hidden', elevation: 3, shadowColor: '#000', shadowOffset: {width:0,height:2}, shadowOpacity:0.08, shadowRadius:8 },
image: { width: '100%', aspectRatio: 1, backgroundColor: '#f0f0f0' },
info: { padding: 10 },
title: { fontSize: 13, fontWeight: '600', color: '#222', lineHeight: 18 },
price: { fontSize: 15, fontWeight: 'bold', color: '#4f86f7', marginTop: 4 },
});FlatList with numColumns for Grids
For large datasets, use FlatList with the numColumns prop instead of a wrapping row. FlatList virtualizes rendering — only cards visible on screen are in memory, making scrolling smooth with thousands of items. Set numColumns={2} and provide a columnWrapperStyle for consistent row spacing. The key prop from keyExtractor is critical for performance. The downside: changing numColumns at runtime requires unmounting and remounting the list — use the same number of columns for the full session.
import { FlatList, useWindowDimensions } from 'react-native';
import ProductCard from './ProductCard';
export default function ProductGrid({ products }) {
const { width } = useWindowDimensions();
const cardWidth = (width - 32 - 12) / 2;
return (
<FlatList
data={products}
numColumns={2}
keyExtractor={item => item.id.toString()}
renderItem={({ item }) => (
<ProductCard item={item} width={cardWidth} />
)}
columnWrapperStyle={{ gap: 12, paddingHorizontal: 16 }}
contentContainerStyle={{ paddingVertical: 16, gap: 12 }}
showsVerticalScrollIndicator={false}
/>
);
}Handling Odd Item Counts (Last Row)
When your grid has an odd number of items, the last row will have a card without a matching sibling. By default, this lone card stretches to fill the full row width (because of Flexbox stretch). To keep the orphan card the same width as other cards, give each card a fixed calculated width rather than relying on Flexbox stretching. In FlatList, you can also add a transparent spacer item to pad the last row. Another option is ListFooterComponent with a fixed height to prevent the last card from appearing stretched near the bottom of the screen.
import { FlatList, View } from 'react-native';
import ProductCard from './ProductCard';
// Add a transparent placeholder if odd count
function padToEven(data) {
if (data.length % 2 !== 0) {
return [...data, { id: '__placeholder__', _placeholder: true }];
}
return data;
}
export default function EvenGrid({ products, cardWidth }) {
const paddedData = padToEven(products);
return (
<FlatList
data={paddedData}
numColumns={2}
keyExtractor={item => item.id}
renderItem={({ item }) => (
item._placeholder
? <View style={{ width: cardWidth }} />
: <ProductCard item={item} width={cardWidth} />
)}
columnWrapperStyle={{ gap: 12, paddingHorizontal: 16 }}
/>
);
}Category Header Above the Grid
Real app grids often show a category header — a title, a 'See all' link, and optional horizontal filter pills — above the product cards. The most flexible approach is to use FlatList's ListHeaderComponent prop, which renders above the scrollable content and scrolls with it. This keeps the header inside the virtualized scroll without needing a separate ScrollView wrapper. For multiple categories with their own grids, use SectionList with section headers and a custom renderSectionHeader.
import { FlatList, View, Text, TouchableOpacity, StyleSheet } from 'react-native';
function GridHeader({ title, onSeeAll }) {
return (
<View style={styles.header}>
<Text style={styles.title}>{title}</Text>
<TouchableOpacity onPress={onSeeAll}>
<Text style={styles.seeAll}>See all</Text>
</TouchableOpacity>
</View>
);
}
// Use as:
<FlatList
ListHeaderComponent={<GridHeader title='Popular' onSeeAll={() => {}} />}
data={products}
numColumns={2}
// ...
/>
const styles = StyleSheet.create({
header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 16, paddingVertical: 12 },
title: { fontSize: 20, fontWeight: 'bold', color: '#111' },
seeAll: { fontSize: 14, color: '#4f86f7' },
});Adaptive Columns for Tablets
For a grid that shows 2 columns on phones and 3 or 4 on tablets, compute the number of columns dynamically from the screen width. Because FlatList's numColumns cannot change after mounting, the safest approach is to include the column count in the FlatList's key prop: key={numColumns.toString()}. When the column count changes (e.g., device rotated into landscape on a tablet), FlatList unmounts and remounts with the new layout. This is a known limitation — accept the brief remount for a correct adaptive grid.
import { FlatList, useWindowDimensions } from 'react-native';
import ProductCard from './ProductCard';
export default function AdaptiveGrid({ products }) {
const { width } = useWindowDimensions();
const numColumns = width >= 768 ? 3 : 2;
const gap = 12, padding = 16;
const cardWidth = (width - padding * 2 - gap * (numColumns - 1)) / numColumns;
return (
<FlatList
key={numColumns.toString()} // remount on column change
data={products}
numColumns={numColumns}
keyExtractor={item => item.id}
renderItem={({ item }) => <ProductCard item={item} width={cardWidth} />}
columnWrapperStyle={{ gap, paddingHorizontal: padding }}
contentContainerStyle={{ paddingVertical: 16, gap }}
/>
);
}Loading Skeleton Grid
While product data loads from an API, show a skeleton grid of placeholder cards to prevent an empty white screen. Use a fixed array of placeholder items (e.g., 6 items) and render skeleton cards in the same grid layout. Animate them with a pulsing opacity using the Animated API for a polished shimmer effect. Once real data arrives, replace the skeleton array with actual items. This transition from skeleton to real content should be instant — no fade — to avoid visual confusion about whether the content changed.
import { View, StyleSheet } from 'react-native';
function SkeletonCard({ width }) {
return (
<View style={[styles.card, { width }]}>
<View style={styles.imgSkeleton} />
<View style={styles.info}>
<View style={styles.titleSkeleton} />
<View style={styles.priceSkeleton} />
</View>
</View>
);
}
const PLACEHOLDER_COUNT = 6;
export default function SkeletonGrid({ cardWidth }) {
return (
<View style={styles.grid}>
{Array.from({ length: PLACEHOLDER_COUNT }).map((_, i) => (
<SkeletonCard key={i} width={cardWidth} />
))}
</View>
);
}
const styles = StyleSheet.create({
grid: { flexDirection: 'row', flexWrap: 'wrap', gap: 12, padding: 16 },
card: { borderRadius: 14, overflow: 'hidden', backgroundColor: '#fff', elevation: 2 },
imgSkeleton: { width: '100%', aspectRatio: 1, backgroundColor: '#e0e0e0' },
info: { padding: 10, gap: 6 },
titleSkeleton: { height: 14, backgroundColor: '#e0e0e0', borderRadius: 4, width: '80%' },
priceSkeleton: { height: 14, backgroundColor: '#e0e0e0', borderRadius: 4, width: '40%' },
});Pull-to-Refresh in the Grid
Add pull-to-refresh to the grid by passing a RefreshControl to FlatList's refreshControl prop. This renders the native platform refresh indicator (spinner on iOS, pull animation on Android). Set the refreshing prop to a boolean state variable and onRefresh to a function that fetches new data and sets refreshing back to false when done. Always reset refreshing to false in both the success and error paths of your API call to prevent the spinner spinning forever.
import { FlatList, RefreshControl } from 'react-native';
import { useState, useCallback } from 'react';
export default function RefreshableGrid({ initialProducts, fetchProducts }) {
const [products, setProducts] = useState(initialProducts);
const [refreshing, setRefreshing] = useState(false);
const onRefresh = useCallback(async () => {
setRefreshing(true);
try {
const fresh = await fetchProducts();
setProducts(fresh);
} finally {
setRefreshing(false); // always reset
}
}, [fetchProducts]);
return (
<FlatList
data={products}
numColumns={2}
keyExtractor={item => item.id}
renderItem={({ item }) => <>{/* ProductCard */}</>}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor='#4f86f7' />
}
/>
);
}Empty State for the Grid
When the product list is empty (no search results or empty category), show an empty state instead of a blank screen. Use FlatList's ListEmptyComponent prop to render a custom component when the data array is empty. A good empty state includes an icon or illustration, a clear headline ('No results found'), and optionally a call-to-action button ('Clear filters' or 'Browse all'). This dramatically improves usability compared to showing nothing and leaving the user confused about whether data is loading or missing.
import { FlatList, View, Text, TouchableOpacity, StyleSheet } from 'react-native';
function EmptyState({ onReset }) {
return (
<View style={styles.empty}>
<Text style={styles.icon}>🔍</Text>
<Text style={styles.title}>No products found</Text>
<Text style={styles.subtitle}>Try different filters or search terms</Text>
<TouchableOpacity style={styles.btn} onPress={onReset}>
<Text style={styles.btnText}>Clear filters</Text>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
empty: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 32 },
icon: { fontSize: 56, marginBottom: 16 },
title: { fontSize: 20, fontWeight: 'bold', color: '#333' },
subtitle: { fontSize: 14, color: '#999', textAlign: 'center', marginTop: 8 },
btn: { marginTop: 20, paddingHorizontal: 24, paddingVertical: 12, backgroundColor: '#4f86f7', borderRadius: 24 },
btnText: { color: '#fff', fontWeight: '600' },
});Grid Item Long-Press Actions
Adding a long-press context menu to grid cards enhances their usefulness — users can hold a card to see options like 'Add to Wishlist', 'Share', or 'Remove'. Use the onLongPress prop on the card's TouchableOpacity or Pressable wrapper. Show the action sheet with React Native's built-in ActionSheetIOS on iOS or a custom bottom sheet modal that works cross-platform. Pass the item's data to the long-press handler so the action sheet knows which item to act on.
import { TouchableOpacity, ActionSheetIOS, Platform, Alert } from 'react-native';
function GridCard({ item, width, onDelete, onShare }) {
function handleLongPress() {
if (Platform.OS === 'ios') {
ActionSheetIOS.showActionSheetWithOptions(
{
options: ['Cancel', 'Share', 'Remove'],
cancelButtonIndex: 0,
destructiveButtonIndex: 2,
},
(index) => {
if (index === 1) onShare(item);
if (index === 2) onDelete(item.id);
}
);
} else {
Alert.alert(item.name, 'Choose an action', [
{ text: 'Share', onPress: () => onShare(item) },
{ text: 'Remove', style: 'destructive', onPress: () => onDelete(item.id) },
{ text: 'Cancel', style: 'cancel' },
]);
}
}
return (
<TouchableOpacity
onPress={() => {/* navigate to detail */}}
onLongPress={handleLongPress}
delayLongPress={400}
style={{ width }}
>
{/* card content */}
</TouchableOpacity>
);
}Quick Check
Test your understanding of React Native Mobile Development concepts from this lesson.
Lesson Recap
In this lesson you learned: FlatList with numColumns virtualizes grid rendering for large datasets, cardWidth calculation ensures a perfect fit across all screen sizes, and ListEmptyComponent and ListHeaderComponent add polish with empty states and category headers. Next up we explore passing data between components with props.
คำถามที่พบบ่อย
บทเรียน “การสร้างตารางการ์ดแบบตอบสนอง” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “การสร้างตารางการ์ดแบบตอบสนอง” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส React Native Academy ให้อัปเกรดเป็น CoddyKit PRO คอร์ส React Native Academy มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “การสร้างตารางการ์ดแบบตอบสนอง”
ใช้การตัดบรรทัดของ Flexbox และความกว้างแบบเปอร์เซ็นต์เพื่อสร้างตารางการ์ดสองคอลัมน์ที่แสดงผลได้เหมาะสมทั้งบนโทรศัพท์ขนาดเล็กและแท็บเล็ตขนาดใหญ่ คุณปฏิบัติ React Native Academy ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน React Native Academy หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน React Native Academy บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน
บทเรียน “การสร้างตารางการ์ดแบบตอบสนอง” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน React Native Academy นี้ได้ไหม
ได้ บทเรียน React Native Academy ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- StyleSheet.create และรูปแบบแบบอินไลน์
- ทิศทาง Flexbox, JustifyContent และ AlignItems
- flex, flexGrow และการปรับขนาดตามหน้าจอ
- การสร้างตารางการ์ดแบบตอบสนอง