Quase todo app tem uma lista em que dá para pesquisar. Vamos construir uma em etapas, do menor componente até uma lista que continua rápida com milhares de linhas.
## Comece pequeno
Uma linha é um Text dentro de um Pressable:
import { Pressable, Text } from 'react-native';
export function Row({ title, onPress }: { title: string; onPress: () => void }) {
return (
<Pressable onPress={onPress}>
<Text>{title}</Text>
</Pressable>
);
}Primeiro, crie o app com o Expo:
npx create-expo-app@latest searchable-list
cd searchable-list && npx expo start## Espere a digitação parar
Filtrar a cada tecla é desperdício. Um hook genérico devolve o valor só depois que ele fica parado por um instante:
import { useEffect, useState } from 'react';
export function useDebounced<T>(value: T, delay = 250): T {
const [settled, setSettled] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setSettled(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return settled;
}## A lista
A tela guarda seu estado num reducer, filtra com useMemo e desenha com uma FlatList de linhas memoizadas, então digitar só redesenha o que mudou:
import { memo, useCallback, useMemo, useReducer } from 'react';
import { FlatList, StyleSheet, TextInput, View } from 'react-native';
import { Row } from './Row';
import { useDebounced } from './useDebounced';
type Item = { id: string; title: string };
type State = { query: string; selected: Set<string> };
type Action =
| { type: 'query'; query: string }
| { type: 'toggle'; id: string };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'query':
return { ...state, query: action.query };
case 'toggle': {
const selected = new Set(state.selected);
selected.has(action.id) ? selected.delete(action.id) : selected.add(action.id);
return { ...state, selected };
}
}
}
const MemoRow = memo(Row);
export function SearchScreen({ items }: { items: Item[] }) {
const [state, dispatch] = useReducer(reducer, { query: '', selected: new Set<string>() });
const query = useDebounced(state.query.trim().toLowerCase());
const visible = useMemo(
() => (query ? items.filter((item) => item.title.toLowerCase().includes(query)) : items),
[items, query],
);
const renderItem = useCallback(
({ item }: { item: Item }) => (
<MemoRow title={item.title} onPress={() => dispatch({ type: 'toggle', id: item.id })} />
),
[],
);
return (
<View style={styles.screen}>
<TextInput
placeholder="Pesquisar"
value={state.query}
onChangeText={(text) => dispatch({ type: 'query', query: text })}
style={styles.search}
/>
<FlatList data={visible} keyExtractor={(item) => item.id} renderItem={renderItem} initialNumToRender={20} />
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, padding: 16 },
search: { borderWidth: 1, borderColor: '#d0d7de', borderRadius: 6, padding: 12, marginBottom: 12 },
});## Uma configuração
No Android, o teclado não deve cobrir a lista:
{
"expo": {
"name": "Searchable List",
"android": { "softwareKeyboardLayoutMode": "pan" }
}
}Pronto: um hook, um reducer e uma linha memoizada. A documentação do React Native sobre FlatList vai além.