Jasmine Wong
Jasmine Wong

Reputation: 91

react-native search bar and flatList issue

can someone please help me troubleshoot?

I am trying to search through a local JSON file and return the relevant data. My JSON file contains a restaurant name and restaurant type.

I don't understand where the issue may be coming from. When I comment out ListHeadComponent={this.renderHeader}, there is no error; however, nothing will display if I comment that out.

The issue I am getting is:

Invariant Violation: Invariant Violation: Tried to get frame for out of range index NaN

This error is located at:
    in VirtualizedList (at FlatList.js:662)
    in FlatList (at SearchScreen.js:75)
    in RCTView (at View.js:44)
    in SearchScreen (created by SceneView)
    in SceneView (at StackViewLayout.js:795)
    in RCTView (at View.js:44)
    in AnimatedComponent (at StackViewCard.js:69)
    in RCTView (at View.js:44)
    in AnimatedComponent (at screens.native.js:59)
    in Screen (at StackViewCard.js:57)
    in Card (at createPointerEventsContainer.js:27)
    in Container (at StackViewLayout.js:860)
    in RCTView (at View.js:44)
    in ScreenContainer (at StackViewLayout.js:311)
    in RCTView (at View.js:44)
    in AnimatedComponent (at StackViewLayout.js:307)
    in Handler (at StackViewLayout.js:300)
    in StackViewLayout (at withOrientation.js:30)
    in withOrientation (at StackView.js:79)
    in RCTView (at View.js:44)
    in Transitioner (at StackView.js:22)
    in StackView (created by Navigator)
    in Navigator (at createKeyboardAwareNavigator.js:12)
    in KeyboardAwareNavigator (created by SceneView)
    in SceneView (at createTabNavigator.js:39)
    in RCTView (at View.js:44)
    in RCTView (at View.js:44)
    in ResourceSavingScene (at createBottomTabNavigator.js:113)
    in RCTView (at View.js:44)
    in ScreenContainer (at createBottomTabNavigator.js:103)
    in RCTView (at View.js:44)
    in TabNavigationView (at createTabNavigator.js:197)
    in NavigationView (created by Navigator)
    in Navigator (created by SceneView)
    in SceneView (created by SwitchView)
    in SwitchView (created by Navigator)
    in Navigator (at createAppContainer.js:388)
    in NavigationContainer (at App.js:24)
    in RCTView (at View.js:44)
    in App (at withExpoRoot.js:22)
    in RootErrorBoundary (at withExpoRoot.js:21)
    in ExpoRootComponent (at renderApplication.js:34)
    in RCTView (at View.js:44)
    in RCTView (at View.js:44)
    in AppContainer (at renderApplication.js:33)

This error is located at:
    in NavigationContainer (at App.js:24)
    in RCTView (at View.js:44)
    in App (at withExpoRoot.js:22)
    in RootErrorBoundary (at withExpoRoot.js:21)
    in ExpoRootComponent (at renderApplication.js:34)
    in RCTView (at View.js:44)
    in RCTView (at View.js:44)
    in AppContainer (at renderApplication.js:33)

The code in my .js file is:

import React from 'react';
import { ExpoConfigView } from '@expo/samples';
import { View, Text, FlatList, ActivityIndicator} from 'react-native';
import { SearchBar } from 'react-native-elements';
import restaurantList from '../assets/files/search.json';

export default class SearchScreen extends React.Component {
static navigationOptions = {
    title: 'Search for Restaurants',
};
constructor() {
    super();
    this.state = {
        loading: false,
        data: {restaurantList},
        error: null,
    };
    this.arrayholder = [];
}

renderSeparator = () => {
    return (
      <View
        style={{
          height: 1,
          width: '86%',
          backgroundColor: '#CED0CE',
          marginLeft: '14%',
        }}
      />
    );
  };    

searchFilterFunction = text =>{
    this.setState({
        value: text,
    });

    const newData = this.arrayholder.filter(item => {
        const itemData = `${item.name.toUpperCase()} 
        ${item.type.toUpperCase()}`;
        const textData = text.toUpperCase();

        return itemData.indexOf(textData) > -1;
    })
    this.setState({
        data: newData,
    });
};

renderHeader = () => {
    return (
        <SearchBar
            placeholder="Type..."
            value={this.state.value}
            onChange={text => this.searchFilterFunction(text)}
            />
    );
};

render() {
    if (this.state.loading) {
        return (
          <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
            <ActivityIndicator />
          </View>
        );
      }
      return (
        <View>
          <FlatList
            data={this.state.data}
            renderItem={({ item }) => (
              <Text>{item.name} {item.type}</Text>
            )}
            ItemSeparatorComponent={this.renderSeparator}
            ListHeaderComponent={this.renderHeader}
          />
        </View>
      );
}
}

Here is my json file

[
{ 
    "name": "Shake Shack",
    "type":"american"
},
{
    "name": "BonChon Chicken",
    "type": "korean"
},
{
    "name": "Starbucks",
    "type:": "cafe"
}
]

Upvotes: 2

Views: 5323

Answers (1)

Andrew
Andrew

Reputation: 28539

This is a really good start. However, there are a couple of issues with your code. So let's go through it and see where we can make some improvements.

Constructor

Firstly in your constructor you are making the data an object rather than array. FlatLists do not work with objects they work with arrays so this is going to immediately cause you problems. You really need to remove the {} from around the restaurantList.

constructor() {
    super();
    this.state = {
        loading: false,
        data: {restaurantList}, // You shouldn't have {} around the restaurantList
        error: null,
    };
    this.arrayholder = []; // we also don't need this
}

You should update your constructor to this

constructor (props) {
  super(props);

  this.state = {
    loading: false,
    data: restaurantList, // notice we have no {} around restaurantList
    error: null,
    value: ''
  };
}

renderHeader

In your renderHeader function you are using onChange rather than onChangeText. onChange returns an object, but you want the text that has been put into the search bar. You need to update your renderHeader function to be like this.

renderHeader = () => {
  return (
    <SearchBar
      placeholder="Type..."
      value={this.state.value}
      onChangeText={text => this.searchFilterFunction(text)} // now we are using the correct function to capture the text
    />
  );
};

searchFilterFunction

There are several issues with this function. Firstly you are looking at this.arrayholder which is empty. We don't actually need an additional array to hold the data as we can just use the restaurantList that we imported earlier. Secondly you are using indexOf on a string it is better to use includes.

searchFilterFunction = text => {
  this.setState({
    value: text
  });

  const newData = restaurantList.filter(item => {
    const itemData = `${item.name.toUpperCase()} ${item.type.toUpperCase()}`;
    const textData = text.toUpperCase();
    return itemData.includes(textData); // this will return true if our itemData contains the textData
  });

  this.setState({
    data: newData
  });
};

FlatList

In your FlatList you should use the extraData prop as this will allow the FlatList to update when the underlying data changes. You should also add a keyExtractor.

<FlatList
  keyExtractor={(item, index) => `${index}`}
  extraData={this.state} // <- add this prop
  data={this.state.data}
  renderItem={({ item }) => (
    <Text>{item.name} {item.type}</Text>
  )}
  ItemSeparatorComponent={this.renderSeparator}
  ListHeaderComponent={this.renderHeader}
/>

Putting it all together

So if we put this all together, adding a mock of data so that we can check it works. We should get something like this.

// mock the data as you didn't provide an example
const restaurantList = [
  {
    type: 'Italian',
    name: 'DiMaggio'
  },
  {
    type: 'Greek',
    name: 'Athena'
  }
];

export default class SearchScreen extends React.Component {
  static navigationOptions = {
    title: 'Search for Restaurants'
  };
  constructor (props) {
    super(props);

    this.state = {
      loading: false,
      data: restaurantList,
      error: null,
      value: ''
    };
  }

  renderSeparator = () => {
    return (
      <View
        style={{
          height: 1,
          width: '86%',
          backgroundColor: '#CED0CE',
          marginLeft: '14%'
        }}
      />
    );
  };

  searchFilterFunction = text => {
    this.setState({
      value: text
    });

    const newData = restaurantList.filter(item => {
      const itemData = `${item.name.toUpperCase()} ${item.type.toUpperCase()}`;
      const textData = text.toUpperCase();
      return itemData.includes(textData);
    });

    this.setState({
      data: newData
    });
  };

  renderHeader = () => {
    return (
      <SearchBar
        placeholder="Type..."
        value={this.state.value}
        onChangeText={text => this.searchFilterFunction(text)}
      />
    );
  };

  render () {
    if (this.state.loading) {
      return (
        <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
          <ActivityIndicator />
        </View>
      );
    } else {
      return (
        <View style={styles.container}>
          <FlatList
            keyExtractor={(item, index) => `${index}`}
            extraData={this.state}
            data={this.state.data}
            renderItem={({ item }) => (
              <Text>{item.name} {item.type}</Text>
            )}
            ItemSeparatorComponent={this.renderSeparator}
            ListHeaderComponent={this.renderHeader}
          />
        </View>
      );
    }
  }
}

Snack

You can see it working at the following snack https://snack.expo.io/@andypandy/flatlist-with-search

Upvotes: 2

Related Questions