How to add a select element in React Native

TL;DR: The React Native Picker component is the equivalent of the HTML select element.

In my last post, I used fetch to download a list of movie titles. However, I only displayed the list by using a debugging tool. My app user will want to see this list, and even better, may want to select something off the list. The commonly used web widget which does this is the HTML <select> element. In React Native, the corresponding widget is a Picker.

You can add a Picker to your render method very easily, like this:

import { Picker } from 'react-native';
...
render() {
    return <View>
        <Picker>
        </Picker>
    </View>;
}

That’s a start, but it doesn’t display my data, and it doesn’t look very interesting. We’ve got a picker with no functionality which looks like this:

Notice the little upside-down triangle in the upper right corner, which indicates the Picker has loaded.

In order to populate the Picker with data, we need to get access to the list of movies inside the render method. Fortunately, I already used setState to make the movie data persist in my app’s state (see previous post). That means I can use it in render, like this:

render() {
    let items = [];
    var length = this.state.dataSource.length;
    for (var i = 0; i < length; i++) {
        var item = this.state.dataSource[i];
        // Really, really important to not put quotes around these braces:
        items.push(<Picker.Item label={item.title} value={item.title} key={item.id} />);
    }
    return <View>
        <Picker>
        </Picker>
    </View>;
}

Uh oh, wait, as soon as I added this code, I saw the red screen of death in the emulator. The error message read “TypeError: null is not an object (evaluating ‘this.state.dataSource’). This error is located at: in HelloWorldApp (at renderApplication.js:40)…”

The problem is that I tried to reference this.state.dataSource before it had been set. The render method might be called before fetch runs. We don’t know when our fetch statement will be called, or even if it is successful. We need to add code in render which handles the case where this data is not yet available.

Here’s the new render code which skips doing anything with dataSource if it is not found:

render() {
    if (this.state && !this.state.isLoading) {
        let items = [];
        var length = this.state.dataSource.length;
        for (var i = 0; i < length; i++) {
            var item = this.state.dataSource[i];
            items.push(<Picker.Item label={item.title} value={item.title} key={item.id} />);
        }
        return <View>
            <Picker>
            </Picker>
        </View>;
    } else {
        return ( <View></View>);
    }
}

That fixes the error, but the Picker is still empty. But in anticipation of needing a list of movies, I’ve already got a list of Picker.Item objects loaded into an array using JSX (items.push(<Picker.Item....). It turns out to be very easy to add these to the Picker. Just add items in braces inside the Picker tags, like this:

<Picker>
{items}
</Picker>

As soon as this is done, like magic, the Picker reflects the changes. A dropdown appears with the text Star Wars selected by default.

Now you can click the dropdown, and try to select a different movie, but what you’ll find is that the dropdown is stuck at Star Wars – you can’t select anything else!

For a finishing touch, let’s make it possible for the user to select a different movie. The Picker documentation tells us how to do this by adding the selectedValue attribute and the onValueChange methods like this:

selectedValue={this.state.movie}
onValueChange={(itemValue, itemIndex) =>
    this.setState({ movie: itemValue })
}

After adding this I can select any movie, and that movie stays selected! Here’s my final code:

import React, { Component } from 'react';
import { View, Picker } from 'react-native';

export default class HelloWorldApp extends Component {
    componentDidMount() {
        return fetch('https://facebook.github.io/react-native/movies.json')
            .then((response) => response.json())
            .then((responseJson) => {
                console.log(responseJson);
                this.setState({
                    isLoading: false,
                    dataSource: responseJson.movies,
                }, function () {

                });

            })
            .catch((error) => {
                console.error(error);
            });
    }

    render() {
        if (this.state && !this.state.isLoading) {
            let items = [];
            var length = this.state.dataSource.length;
            for (var i = 0; i < length; i++) {
                var item = this.state.dataSource[i];
                // Really, really important to not put quotes around these braces:
                items.push(<Picker.Item label={item.title} value={item.title} key={item.id} />);
            }
            return <View>
                <Picker selectedValue={this.state.movie}
                    onValueChange={(itemValue, itemIndex) =>
                        this.setState({ movie: itemValue })
                    }>
                    {items}
                </Picker>
            </View>;
        } else {
            return (<View></View>);
        }
    }
}

Got comments? Send them to me in an email at fullstackdev@fullstackoasis.com. If you found this interesting, go ahead and click the subscribe button above. I write a new post about once a week.

Leave a Reply

Your email address will not be published. Required fields are marked *