Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

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-app
data/emojis.js
export const EMOJI_CATEGORIES = [
  {
    name: 'Smileys',
    emojis: ['????', '????', '????', '????', '????', '????', '????', '????', '????', '????', '????', '????'],
  },
  {
    name: 'Animals',
    emojis: ['????', '????', '????', '????', '????', '????', '????', '????', '????', '????', '????', '????'],
  },
  {
    name: 'Food',
    emojis: ['????', '????', '????', '????', '????', '????', '????', '????', '????', '????', '????', '????'],
  },
];
App.js
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

  • history uses filter + 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 grid FlatList forces React to fully rebuild the list on category change – important so the scroll position resets when switching categories.
  • The horizontal history list (horizontal on FlatList) and the vertical emoji grid (numColumns={{6}}) are two DIFFERENT configurations of the SAME component – showing how flexibly FlatList can be used for different layouts.
  • Rendering emojis as text has the advantage that size/color are controlled via fontSize like any other text – no image loading, no network request needed.

5. Outputs

Ausgabe
A large preview at the top ("❔" initially, text "Nothing selected yet"), below it (after the first selection) a horizontally scrollable "Recently used" strip, below that three tabs ("Smileys" active/underlined in blue), below that a 6-column emoji grid. Tapping an emoji enlarges it in the preview and inserts it at the front of the history.