Aleksei Danilov
Aleksei Danilov

Reputation: 645

TextInput lost focus after typing one symbol when searching

I have a FlatList

<View style={styles.container}>
    <FlatList data={this.state.restaurants} 
              renderItem={({ item }) => this.renderItem(item.restaurant)}
              keyExtractor={restaurant => restaurant.key}
              ListHeaderComponent={() => this.renderHeaderComponent()} 
              ItemSeparatorComponent={this.renderSeparator}/>
  </View>

And have TextInput in header it. I am using TextInput as search bar.

 renderHeaderComponent() {
    return(
      <View style={{ flexDirection: 'row', marginTop: 10, borderBottomColor: '#CED0CE', borderWidth: 1, borderColor: 'transparent' }}>
        <Icon name='search' size={30} style={{ marginLeft: 10, marginRight: 10 }}/>
        <TextInput
            style={{height: 40, flex: 1}}
            onChangeText={(text) => this.onChangeText(text)}
            placeholder='Type text for search'
            clearButtonMode='while-editing'
            value={this.state.searchText}
      />
      </View>
    );
  };

In onChangeMethod i filter my data.

 onChangeText(text) {
    const filteredRestaurants = _.filter(this.props.list, (restaurantObject) => {
      const restaurant = restaurantObject.restaurant;
      const result = restaurant.name.trim().toLowerCase().includes(text.trim().toLowerCase());
      return result;
    })
    this.setState({
      searchText: text,
      restaurants: filteredRestaurants
    });
  }

The problem is following. When I type one symbol in TextInput then focus is lost immediately from TextInput? How can I keep focus in TextInput while typing?

Upvotes: 7

Views: 3305

Answers (4)

sean_j_roberts
sean_j_roberts

Reputation: 498

I found another workaround for SectionList that seems to work so far, and I'll update this answer if I find it stops working. Rather than rendering my component in ListHeaderComponent I add a dummy section at the start of my data and then use a conditional in renderSectionHeader to render it out.

<SectionList
  sections={[{ title: 'header', data: [] }, ...sections]}
  renderSectionHeader={({ section }) => 
    section.title === 'header' ? (
      <MyListHeaderComponent />
    ) : (
      <DefaultSectionHeaderComponent />
    )
  }
/>

Having worked with some pretty hairy CollectionView screens in Swift/UIKit it's not that different from how we would handle a similar need in that environment so hopefully that means under the hood perf won't be an issue, but again I'll update this answer if that becomes the case.

Another option may be to just add a dummy item to your sections array so that it never becomes empty but I haven't tried that.

Upvotes: 1

Tomas Gonzalez
Tomas Gonzalez

Reputation: 188

I ran into this, and to solve it I wrapped the renderListHeader in a React.useMemo hook and passed the state hook as an item to the dependency array.

 renderListHeader = useMemo(() => (
  <View style={{ flexDirection: 'row', marginTop: 10, borderBottomColor: '#CED0CE', borderWidth: 1, borderColor: 'transparent' }}>
      <Icon name='search' size={30} style={{ marginLeft: 10, marginRight: 10 }}/>
      <TextInput
          style={{height: 40, flex: 1}}
          onChangeText={(text) => this.onChangeText(text)}
          placeholder='Type text for search'
          clearButtonMode='while-editing'
          value={this.state.searchText}
      />
  </View>
), [this.onChangeText])

Upvotes: 5

Sam
Sam

Reputation: 352

This is still an issue for SectionList as of react-native 0.61.5. The auto-bound method doesn't work since the ListHeaderComponent re-renders when data is becomes an empty array.

I used the following work-around :

  1. Move the text input code at the same level than the section list
  2. Using position absolute, position it at the place you want.
  3. Wrap it in an Animated.View
  4. Leverage Animated.event to translate Y the Animated.View

Code sample

const animatedScrollYValue = useRef(new Animated.Value(0)).current;
...
<View>
  <Animated.View style={{
    position: 'absolute',
    top: 142,
    left: 30,
    right: 30,
    zIndex: 1,
    transform: [{ translateY: Animated.multiply(animatedScrollYValue, new Animated.Value(-1)) }] }}>
    // Your text input
  </Animated.View>
  <Animated.SectionList
    scrollEventThrottle={1}
    onScroll={Animated.event([{ nativeEvent: { contentOffset: { y: animatedScrollYValue } } }], { useNativeDriver: true })}
    keyExtractor={(item) => item.id}
    ListHeaderComponent={// Whatever you want but make you include space for the absolute TextInput}
    sections={data}
    renderItem={renderItem}
    renderSectionHeader={renderHeader}
  />
</View>

Upvotes: 3

Ryan Turnbull
Ryan Turnbull

Reputation: 3934

You need to use an auto-bound method for this, as ListHeaderComponent is of type ReactClass, and your current method basically re-creates and re-binds its render every time the data updates, which is not what you want. This concept is further explained in this comment

Anyway, for your example, to fix your issues you should

1) Change your ListHeaderComponent prop to

ListHeaderComponent={this.renderListHeader}

2) Now you want to change your renderHeaderComponent method to be an auto-bound method, and by doing this a new render will not be instantiated every time you change data ( Or enter text into the `TextInput)

renderListHeader = () => (
  <View style={{ flexDirection: 'row', marginTop: 10, borderBottomColor: '#CED0CE', borderWidth: 1, borderColor: 'transparent' }}>
      <Icon name='search' size={30} style={{ marginLeft: 10, marginRight: 10 }}/>
      <TextInput
          style={{height: 40, flex: 1}}
          onChangeText={(text) => this.onChangeText(text)}
          placeholder='Type text for search'
          clearButtonMode='while-editing'
          value={this.state.searchText}
      />
  </View>
)

Upvotes: 8

Related Questions