Lists, forms and input handling

Virtualise long lists with FlatList and SectionList, stabilise keys, add pull to refresh, and build forms that handle the keyboard and validation correctly.

FlatList that stays fast

import { useCallback } from 'react';
import { FlatList, Text, Pressable, StyleSheet, View } from 'react-native';

type Row = { id: string; title: string; subtitle: string };

export function TaskList({
  data,
  onOpen,
  onRefresh,
}: {
  data: readonly Row[];
  onOpen: (id: string) => void;
  onRefresh: () => void;
}) {
  const renderItem = useCallback(
    ({ item }: { item: Row }) => (
      <Pressable onPress={() => onOpen(item.id)} style={styles.row}>
        <Text style={styles.title}>{item.title}</Text>
        <Text style={styles.subtitle} numberOfLines={1}>{item.subtitle}</Text>
      </Pressable>
    ),
    [onOpen],
  );

  const keyExtractor = useCallback((item: Row) => item.id, []);

  return (
    <FlatList
      data={data}
      renderItem={renderItem}
      keyExtractor={keyExtractor}
      initialNumToRender={12}
      maxToRenderPerBatch={10}
      windowSize={7}
      removeClippedSubviews
      ItemSeparatorComponent={() => <View style={styles.sep} />}
      ListEmptyComponent={<Text style={styles.empty}>Nothing here yet</Text>}
      onRefresh={onRefresh}
      refreshing={false}
      contentContainerStyle={data.length === 0 ? styles.grow : undefined}
    />
  );
}

const styles = StyleSheet.create({
  row: { paddingVertical: 12 },
  title: { fontSize: 16, fontWeight: '600' },
  subtitle: { color: '#6b7280' },
  sep: { height: StyleSheet.hairlineWidth, backgroundColor: '#e5e7eb' },
  empty: { textAlign: 'center', padding: 32, color: '#9ca3af' },
  grow: { flexGrow: 1, justifyContent: 'center' },
});
  • keyExtractor must return a stable unique id. An index-based key makes rows jump and lose internal state after a delete or reorder.
  • Wrap renderItem in useCallback or every keystroke in a search box re-renders every visible row.
  • getItemLayout for fixed-height rows lets the list jump to an offset without measuring.
  • An empty list needs flexGrow: 1 on the content container for the empty component to centre.

Forms and the keyboard

import { Controller, useForm } from 'react-hook-form';

type FormValues = { email: string; age: string; accepted: boolean };

export function SignUpForm({ onSubmit }: { onSubmit: (values: FormValues) => void }) {
  const { control, handleSubmit, formState } = useForm<FormValues>({
    defaultValues: { email: '', age: '', accepted: false },
    mode: 'onBlur',
  });

  return (
    <KeyboardAvoidingView
      behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
      keyboardVerticalOffset={insets.top + 44}
      style={{ flex: 1 }}
    >
      <ScrollView keyboardShouldPersistTaps="handled" contentContainerStyle={{ padding: 16 }}>
        <Controller
          control={control}
          name="email"
          rules={{ required: 'Email is required', pattern: { value: /\S+@\S+\.\S+/, message: 'Invalid email' } }}
          render={({ field, fieldState }) => (
            <>
              <TextInput
                value={field.value}
                onChangeText={field.onChange}
                onBlur={field.onBlur}
                keyboardType="email-address"
                autoCapitalize="none"
                autoComplete="email"
                returnKeyType="next"
                style={[styles.input, fieldState.error && styles.inputError]}
              />
              {fieldState.error && <Text style={styles.error}>{fieldState.error.message}</Text>}
            </>
          )}
        />
        <Pressable
          disabled={!formState.isValid || formState.isSubmitting}
          onPress={handleSubmit(onSubmit)}
        >
          <Text>Create account</Text>
        </Pressable>
      </ScrollView>
    </KeyboardAvoidingView>
  );
}
ProblemFixWhy
Keyboard covers the fieldKeyboardAvoidingViewAndroid needs height, iOS padding
First tap only dismisses the keyboardkeyboardShouldPersistTaps="handled"Otherwise the tap is consumed
Android resizes the windowwindowSoftInputMode in the manifestAdjust resize or pan, not both
Autofill guesses wrongautoComplete and textContentTypeSet both for iOS and Android

Pressable and gesture feedback

import { Pressable, Text } from 'react-native';

export function Button({ label, onPress, disabled }: { label: string; onPress: () => void; disabled?: boolean }) {
  return (
    <Pressable
      onPress={onPress}
      disabled={disabled}
      hitSlop={8}
      android_ripple={{ color: 'rgba(0,0,0,0.12)' }}
      style={({ pressed }) => [
        { paddingVertical: 12, paddingHorizontal: 20, borderRadius: 10, opacity: disabled ? 0.4 : pressed ? 0.85 : 1 },
      ]}
    >
      <Text style={{ textAlign: 'center', fontWeight: '600' }}>{label}</Text>
    </Pressable>
  );
}
⚠️
Minimum tap target is 44 by 44 points on iOS and 48 by 48 density-independent pixels on Android. A small icon without hitSlop is technically clickable and practically unusable.

FAQ

Should I use FlatList or FlashList?
FlatList is built in and fine for most feeds. FlashList from Shopify recycles views more aggressively and is measurably faster for very long or image-heavy lists, at the cost of an extra dependency and stricter item sizing.
Why does my form state reset when I scroll?
The component unmounted — usually because a parent re-created it or the list recycled the row. Lift the form state above the scroll container, or use a form library whose state lives outside the render tree.

Flexbox layout and responsive design Networking, data fetching and offline support

Last refreshed 2026-09-18.