Emoji Picker App in React Native
Emoji Picker App
~11 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026
An emoji picker (as seen in chat apps) combines category tabs, a filtered grid, and a "recently used" history – a compact but complete example of categorized grid views.
1. Description
Emojis are plain Unicode CHARACTERS, not images – they render as Text like any other string. The app organizes a fixed emoji list by category, displays it in a grid (numColumns), and remembers recently selected emojis in a separate list.
2. Short example
<TouchableOpacity onPress={() => onSelect('????')}>
<Text style={{ fontSize: 28 }}>????</Text>
</TouchableOpacity>3. Complete project: an emoji picker with history
npx create-expo-app emoji-picker-app
cd emoji-picker-appexport const EMOJI_CATEGORIES = [
{
name: 'Smileys',
emojis: ['????', '????', '????', '????', '????', '????', '????', '????', '????', '????', '????', '????'],
},
{
name: 'Animals',
emojis: ['????', '????', '????', '????', '????', '????', '????', '????', '????', '????', '????', '????'],
},
{
name: 'Food',
emojis: ['????', '????', '????', '????', '????', '????', '????', '????', '????', '????', '????', '????'],
},
];import { useState } from 'react';
import { View, Text, TouchableOpacity, FlatList, StyleSheet } from 'react-native';
import { EMOJI_CATEGORIES } from './data/emojis';
const MAX_HISTORY = 12;
export default function App() {
const [categoryIndex, setCategoryIndex] = useState(0);
const [selected, setSelected] = useState(null);
const [history, setHistory] = useState([]);
function selectEmoji(emoji) {
setSelected(emoji);
setHistory((list) => {
const withoutDuplicate = list.filter((e) => e !== emoji);
return [emoji, ...withoutDuplicate].slice(0, MAX_HISTORY);
});
}
const currentCategory = EMOJI_CATEGORIES[categoryIndex];
return (
<View style={styles.container}>
<View style={styles.preview}>
<Text style={styles.previewEmoji}>{selected ?? '❔'}</Text>
<Text style={styles.previewText}>
{selected ? 'Selected emoji' : 'Nothing selected yet'}
</Text>
</View>
{history.length > 0 && (
<View style={styles.historySection}>
<Text style={styles.historyTitle}>Recently used</Text>
<FlatList
horizontal
data={history}
keyExtractor={(item, index) => `${item}-${index}`}
showsHorizontalScrollIndicator={false}
renderItem={({ item }) => (
<TouchableOpacity onPress={() => selectEmoji(item)} style={styles.historyCell}>
<Text style={styles.emojiText}>{item}</Text>
</TouchableOpacity>
)}
/>
</View>
)}
<View style={styles.tabRow}>
{EMOJI_CATEGORIES.map((category, index) => (
<TouchableOpacity
key={category.name}
style={[styles.tab, index === categoryIndex && styles.tabActive]}
onPress={() => setCategoryIndex(index)}
>
<Text style={[styles.tabText, index === categoryIndex && styles.tabTextActive]}>
{category.name}
</Text>
</TouchableOpacity>
))}
</View>
<FlatList
key={categoryIndex}
data={currentCategory.emojis}
numColumns={6}
keyExtractor={(item) => item}
renderItem={({ item }) => (
<TouchableOpacity style={styles.gridCell} onPress={() => selectEmoji(item)}>
<Text style={styles.emojiText}>{item}</Text>
</TouchableOpacity>
)}
/>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, paddingTop: 60, paddingHorizontal: 16 },
preview: { alignItems: 'center', marginBottom: 16 },
previewEmoji: { fontSize: 48 },
previewText: { color: '#6b7280', marginTop: 4 },
historySection: { marginBottom: 16 },
historyTitle: { color: '#6b7280', marginBottom: 6, fontSize: 13 },
historyCell: { marginRight: 12 },
tabRow: { flexDirection: 'row', marginBottom: 8 },
tab: { flex: 1, paddingVertical: 8, borderBottomWidth: 2, borderBottomColor: 'transparent', alignItems: 'center' },
tabActive: { borderBottomColor: '#2563eb' },
tabText: { color: '#6b7280' },
tabTextActive: { color: '#2563eb', fontWeight: 'bold' },
gridCell: { flex: 1, aspectRatio: 1, alignItems: 'center', justifyContent: 'center' },
emojiText: { fontSize: 28 },
});4. Explanation
historyusesfilter+slice(0, MAX_HISTORY)instead of a simple append – so an emoji ALREADY in the history moves to the FRONT when tapped again instead of duplicating, and the list never grows past 12 entries.key={{categoryIndex}}on the gridFlatListforces React to fully rebuild the list on category change – important so the scroll position resets when switching categories.- The horizontal history list (
horizontalonFlatList) and the vertical emoji grid (numColumns={{6}}) are two DIFFERENT configurations of the SAME component – showing how flexiblyFlatListcan be used for different layouts. - Rendering emojis as text has the advantage that size/color are controlled via
fontSizelike any other text – no image loading, no network request needed.