Criando um component <Select/> com opção de Search e Paginação para ReactNative-NativeBase

Alexandre Justen Filho - Feb 16 - - Dev Community

Se você trabalha com React Native utilizando o NativeBase e muitas outras bibliotecas, e está precisando de um componente que lhe dê a opção de pesquisa e paginação, provavelmente está decepcionado! Pois nativamente você não tem essa opção. No entanto, criei esse componente e gostaria de compartilhar com vocês. Então, vamos lá.

Nativamente o component do NativeBase vai ser assim:

Image description

Um componente simples que pode servir para muitos casos, mas não para todos! O meu componente se comporta de forma diferente:

Image description

Podemos observar que ele conta com a opção de pesquisa e também, ao chegar perto do final da lista, ele já faz a paginação, carregando um indicador de carregamento e, posteriormente, adicionando os itens da próxima página na minha lista. Por que não trazer todos os dados sem paginação? Basicamente, porque podemos ter centenas de milhares de dados e isso causaria problemas com a memória.
Esse componente foi desenvolvido para ser utilizado em vários locais da minha aplicação, então ele não é específico para uma API ou um objeto só. Por isso, você pode utilizá-lo adaptando mínimos detalhes para que ele funcione na sua aplicação.

import React, { useEffect, useState } from 'react';
import { Box, Modal, Button, VStack, Text, View } from 'native-base';
import SearchComponent from './search-fields.utils';
import { FlatList } from 'native-base';
import { HStack } from 'native-base';
import { TouchableOpacity } from 'react-native';
import { ActivityIndicator } from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome5';
import { useTheme } from 'native-base';

interface Props {
    apiUrl: any;
    searchName: string;
    valueName: string;
    placeholder?: string;
    isRequired?: boolean;
    value?: any;
    onChange: (value: any) => void;
}

function SelectPaginate(props: Props) {
    const { colors } = useTheme();
    const [modalVisible, setModalVisible] = React.useState(false);
    const [selectValue, setSelectValue] = useState('');
    const [data, setData] = useState<any[]>([]);
    const [loading, setLoading] = useState(false);
    const [page, setPage] = useState(1);
    const [searchQuery, setSearchQuery] = useState<string>('');
    const [value, setValue] = useState();
    const perPage = 10;

    const onPress = (value: any) => {
        setSelectValue(value?.[props.valueName]);
        setModalVisible(false);
        props.onChange(value);
    };

    const handleSearch = value => {
        setSearchQuery(value);
    };

    useEffect(() => {
        if (props.value !== undefined && props.value !== null) {
            setSelectValue(props.value?.[props.valueName]);
        }
    }, [props.value]);

    useEffect(() => {
        if (data.length === 0) {
            load()
        } else {
            null;
        }
    }, [data]);

    useEffect(() => {
        setPage(1)
        setData([]);
    }, [searchQuery]);

    function areArraysEqual(arr1, arr2) {
        if (arr1.length !== arr2.length) {
            return false;
        }

        for (let i = 0; i < arr1.length; i++) {
            if (arr1[i] !== arr2[i]) {
                return false;
            }
        }

        return true;
    }

    async function loadApi() {
        if (loading) return;

        setLoading(true);
        try {
            const response = await axios.get(`${props.apiUrl}`, {
                params: {
                    page: page - 1,
                    size: perPage,
                    sort: 'id,DESC',
                    cacheBuster: new Date().getTime(),
                },
            });
            if (!areArraysEqual(data, response.data)) {
                setData([...data, ...response.data]);
                setPage(page+1);
            } else {
                return;
            }
        } catch (error) {
            console.log(error);
            return null;
        } finally {
            setLoading(false);
        }
    }

    async function loadApiSearch() {

        try {
            const response = await axios.get(`${props.apiUrl}`, {
                params: {
                    page: 0,
                    size: perPage,
                    sort: 'id,DESC',
                    [props.searchName]: searchQuery,
                    cacheBuster: new Date().getTime(),
                },
            });

            if (!areArraysEqual(data, response.data)) {
                setData([...data, ...response.data]);
                setPage(page+1);
            } else {
                return ;
            }
        } catch (error) {
            console.log(error);
            return null;
        } finally {
            setLoading(false);
        }
    }

    async function load(){
        if (uniqueData.length < 10 && uniqueData.length >0) {
            return;
        }
            if (searchQuery === '' || searchQuery === undefined || searchQuery === null) {

                loadApi();

            } else {
                setLoading(true);
                loadApiSearch();
            }

    }

    function FooterList({ Load }) {
        if (!Load) return null;
        return (
            <View>
                <ActivityIndicator size={25} color="#121212" />
            </View>
        );
    }
    function ListItem({ data }) {
        return (
            <Box pl={['0', '4']} pr={['0', '5']} py="2">
                <TouchableOpacity
                    onPress={() => {
                        onPress(data);
                    }}>
                    <HStack space={[2, 3]} justifyContent="space-between">
                        <VStack>
                            <Text
                                _dark={{
                                    color: 'warmGray.50',
                                }}
                                color="coolGray.800"
                                fontSize={15}
                                mt={2}>
                                {data?.[props.valueName]}
                            </Text>
                        </VStack>
                    </HStack>
                </TouchableOpacity>
            </Box>
        );
    }
    function TextComponent() {
        if (selectValue === null || selectValue === '') {
            return <Text style={{ color: 'grey' }}>{props.placeholder}</Text>;
        } else {
            return <Text style={{ color: 'white' }}>{selectValue}</Text>;
        }
    }
    const uniqueData = Array.from(new Set(data.map(item => item.id)))
    .map(id => data.find(item => item.id === id));
    return (
        <>
            <Modal isOpen={modalVisible} onClose={() => setModalVisible(false)} avoidKeyboard justifyContent="flex-end" bottom="0" size="lg">
                <Modal.Content>
                    <Modal.CloseButton />
                    <Modal.Header>
                        <SearchComponent onSearch={handleSearch} placeholder={props.placeholder} colorText={'grey'} />
                    </Modal.Header>
                    <FlatList
                        height="500"
                        data={uniqueData}
                        renderItem={({ item }) => <ListItem data={item} />}
                        keyExtractor={item => item.id}
                        onEndReached={load}
                        onEndReachedThreshold={0.1}
                        ListFooterComponent={<FooterList Load={loading} />}
                        ml={2}
                    />
                </Modal.Content>
            </Modal>
            <VStack space={8} alignItems="center" >
                <Button
                    onPress={() => {
                        setModalVisible(!modalVisible);
                    }}
                    style={{ borderWidth: 1, borderColor: colors.white50, width: '100%'}} h={12} >
                    <TextComponent />
                    <Icon size={18} name={'chevron-down'} style={{ textAlign: 'right', marginLeft: '94%', marginTop: '-6%', color: 'gray' }} />
                </Button>
            </VStack>
        </>
    );
}

export default SelectPaginate;

Enter fullscreen mode Exit fullscreen mode

Você também vai precisar do componente de pesquisa, que é importado:

import React, { useEffect, useState } from 'react';
import {  Input,  Box } from 'native-base';

function SearchComponent({ onSearch, placeholder ,colorText ='white'}) {
    const [value, setValue] = useState("");

    useEffect(() => {
      const timer = setTimeout(() => {
        onSearch(value);
      }, 500);

      return () => {
        clearTimeout(timer);
      };
    }, [value, onSearch]);

    const handleChange = (text) => {
      setValue(text);
    };

  return (
    <Box alignItems="center">
      <Input
      color={colorText}
      value={value}
      onChangeText={handleChange}
      placeholder={placeholder}
      w="40"
    />
    </Box>
  );
}

export default SearchComponent;

Enter fullscreen mode Exit fullscreen mode

Lembre-se de adaptar esses componentes para a sua aplicação, como a requisição para a API e o objeto. Tente entender a interface criada para passar os valores corretos. Em caso de dúvidas, basta comentar neste post que eu ajudo você.

. . . . . .