React Native Land_

// construindo apps mobile melhores

# Uma lista pesquisável em React Native

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:

Row.tsxtsx
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:

useDebounced.tsts
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:

SearchScreen.tsxtsx
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:

app.jsonjson
{
  "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.