Sorting and filtering lists

Now that you have learned the basics of FlatList components, including how to pass them data, let's add some controls to the list that you just implemented in the previous section. The FlatList component helps you render fixed-position content for list controls. You'll also see how to manipulate the data source, which ultimately drives what's rendered on the screen.

Before implementing list control components, it might be helpful to review the high-level structure of these components so that the code has more context. Here's an illustration of the component structure that you're going to implement:

Here's what each of these components is responsible for:

  • ListContainer: The overall container for the list; it follows the familiar React container pattern
  • List: A stateless component that passes the relevant pieces of state into the ListControls and the React Native ListView component
  • ListControls: A component that holds the various controls that change the state of the list
  • ListFilter: A control for filtering the item list
  • ListSort: A control for changing the sort order of the list
  • FlatList: The actual React Native component that renders items

 

In some cases, splitting apart the implementation of a list like this is overkill. However, I think that if your list needs controls in the first place, you're probably implementing something that will stand to benefit from having a well-thought-out component architecture.

Now, let's drill down into the implementation of this list, starting with the ListContainer component:

import React, { Component } from 'react';

import List from './List';

const mapItems = items =>
items.map((value, i) => ({ key: i.toString(), value }));

// Performs sorting and filtering on the given "data".
const filterAndSort = (data, text, asc) =>
data
.filter(
i =>
// Items that include the filter "text" are returned.
// Unless the "text" argument is an empty string,
// then everything is included.
text.length === 0 || i.includes(text)
)
.sort(
// Sorts either ascending or descending based on "asc".
asc
? (a, b) => (b > a ? -1 : a === b ? 0 : 1)
: (a, b) => (a > b ? -1 : a === b ? 0 : 1)
);

class ListContainer extends Component {
state = {
data: filterAndSort(
new Array(100).fill(null).map((v, i) => `Item ${i}`),
'',
true
),
asc: true,
filter: ''
};

render() {
return (
<List
data={mapItems(this.state.data)}
asc={this.state.asc}
onFilter={text => {
// Updates the "filter" state, the actualy filter text,
// and the "source" of the list. The "data" state is
// never actually touched - "filterAndSort()" doesn't
// mutate anything.
this.setState({
filter: text,
data: filterAndSort(this.state.data, text, this.state.asc)
});
}}
onSort={() => {
this.setState({
// Updates the "asc" state in order to change the
// order of the list. The same principles as used
// in the "onFilter()" handler are applied here,
// only with diferent arguments passed to
// "filterAndSort()"
asc: !this.state.asc,
data: filterAndSort(
this.state.data,
this.state.filter,
!this.state.asc
)
});
}}
/>
);
}
}

export default ListContainer;

If this seems like a bit much, it's because it is. This container component has a lot of state to handle. It also has some nontrivial behavior that it needs to make available to its children. If you look at it from the perspective of encapsulating state, it will be more approachable. Its job is to populate the list with state data and provide functions that operate on this state.

In an ideal world, child components of this container should be nice and simple since they don't have to directly interface with state. Let's take a look at the List component next:

import React from 'react';
import PropTypes from 'prop-types';
import { Text, FlatList } from 'react-native';

import styles from './styles';
import ListControls from './ListControls';

const List = ({ Controls, data, onFilter, onSort, asc }) => (
<FlatList
data={data}
ListHeaderComponent={<Controls {...{ onFilter, onSort, asc }} />}
renderItem={({ item }) => (
<Text style={styles.item}>{item.value}</Text>
)}
/>
);

List.propTypes = {
Controls: PropTypes.func.isRequired,
data: PropTypes.array.isRequired,
onFilter: PropTypes.func.isRequired,
onSort: PropTypes.func.isRequired,
asc: PropTypes.bool.isRequired
};

List.defaultProps = {
Controls: ListControls
};

export default List;

This component takes state from the ListContainer component as properties and renders a FlatList component. The main difference here, relative to the previous example, is the ListHeaderComponent property. This renders the controls for your list. What's especially useful about this property is that it renders the controls outside the scrollable list content, ensuring that the controls are always visible.

Also, notice that you're specifying your own ListControls component as a default value for the controls property. This makes it easy for others to pass in their own list controls. Let's take a look at the ListControls component next:

import React from 'react';
import PropTypes from 'prop-types';
import { View } from 'react-native';

import styles from './styles';
import ListFilter from './ListFilter';
import ListSort from './ListSort';

// Renders the "<ListFilter>" and "<ListSort>"
// components within a "<View>". The
// "styles.controls" style lays out the controls
// horizontally.
const ListControls = ({ onFilter, onSort, asc }) => (
<View style={styles.controls}>
<ListFilter onFilter={onFilter} />
<ListSort onSort={onSort} asc={asc} />
</View>
);

ListControls.propTypes = {
onFilter: PropTypes.func.isRequired,
onSort: PropTypes.func.isRequired,
asc: PropTypes.bool.isRequired
};

export default ListControls;

This component brings together the ListFilter and ListSort controls. So, if you were to add another list control, you would add it here. Let's take a look at the ListFilter implementation now:

import React from 'react';
import PropTypes from 'prop-types';
import { View, TextInput } from 'react-native';

import styles from './styles';

// Renders a "<TextInput>" component which allows the
// user to type in their filter text. This causes
// the "onFilter()" event handler to be called.
// This handler comes from "ListContainer" and changes
// the state of the list data source.
const ListFilter = ({ onFilter }) => (
<View>
<TextInput
autoFocus
placeholder="Search"
style={styles.filter}
onChangeText={onFilter}
/>
</View>
);

ListFilter.propTypes = {
onFilter: PropTypes.func.isRequired
};

export default ListFilter;

The filter control is a simple text input that filters the list of items as user types. The onChange function that handles this comes from the ListContainer component.

Let's look at the ListSort component next:

import React from 'react';
import PropTypes from 'prop-types';
import { Text } from 'react-native';

// The arrows to render based on the state of
// the "asc" property. Using a Map let's us
// stay declarative, rather than introducing
// logic into the JSX.
const arrows = new Map([[true, '▼'], [false, '▲']]);

// Renders the arrow text. When clicked, the
// "onSort()" function that's passed down from
// the container.
const ListSort = ({ onSort, asc }) => (
<Text onPress={onSort}>{arrows.get(asc)}</Text>
);

ListSort.propTypes = {
onSort: PropTypes.func.isRequired,
asc: PropTypes.bool.isRequired
};

export default ListSort;

 

Here's a look at the resulting list:

By default, the entire list is rendered in ascending order. You can see the placeholder text Search when the user hasn't provided anything yet. Let's see how this looks when you enter a filter and change the sort order:

This search includes items with a 1 in it, and sorts the results in descending order. Note that you can either change the order first or enter the filter first. Both the filter and the sort order are part of the ListContainer state.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.218.55.14