Building React Native Music (Part 2): SoundCloud API, Search and NavigatorIOS
(Part 1 available here.)
Live Data
First go to developers.soundcloud.com and register your app to obtain an API ID for your requests. Declare your unique ID as a global constant:
var SOUNDCLOUD_CLIENT_ID = ‘XXXXXX’;
We add a method to our BrowseTracksView component to return a request url to query SoundCloud’s uploaded tracks with the client id appended and then modify our fetch method to perform an async XHR request.
// List View For Browsing Songs
var BrowseTracksView = React.createClass({
getInitialState: function () {
return {
dataSource: new ListView.DataSource({
rowHasChanged: (row1, row2) => row1 !== row2,
})
};
},
componentDidMount: function() {
this.fetchData();
},
fetchData: function () {
// Return live data
fetch(this.fetchEndpoint)
.then((response) => response.json())
.then((responseData) => {
this.setState({
dataSource: this.state.dataSource.cloneWithRows(responseData)
})
.catch((error) => {
console.warn(error);
});
})
.done();
},
fetchEndpoint: 'http://api.soundcloud.com/tracks.json?client_id=' + SOUNDCLOUD_CLIENT_ID,
render: function () {
return (
<ListView
dataSource={this.state.dataSource}
renderRow={this.renderTrack}
style={styles.listView}/>
);
},
renderTrack: function (track) {
return (
<TrackCell track={track}/>
);
}
});
The Fetch method can be chained with processing, success and error callbacks. Here we call React’s setState method to signal the UI is ready for updating with our newly-fetched data.

Adding Search
Live data is nice and all, but as discerning music listeners we don’t want to listen to just any old tracks — we want to find some next-level beats! To make that possible let’s extend our app to include a new feature: Search.
In addition to rendering rows, the ListView API can take functions to render headers, section headers and footers. We are going to create a list header cell consisting of a search bar that will query SoundCloud for tracks matching out input. Modify BrowseTrackViews render function by adding a renderHeader attribute and then create a new method called renderSearchBar.
render: function () {
return (
<ListView
dataSource={this.state.dataSource}
renderRow={this.renderTrack}
renderHeader={this.renderSearchBar}
style={styles.listView}/>
);
},
renderSearchBar: function () {
return (
<View style={styles.searchCell}>
<TextInput onChange={this.onSearchChange} placeholder={'Search Here'} style={styles.searchContainer}/>
</View>
)
},We use the TextInput component here (don’t forget to add TextInput to your React imports) and give it an onChance callback. Use the onChange event to capture the input text and then pass it to BrowseTrackView’s newly modified fetchData method. We’ve also added a timeout to the search so the API doesn’t get hammered unnecessarily while the user is typing — this requires adding a mixin to our component.
At the top of the file import the react-timer-mixin module.
var TimerMixin = require(‘react-timer-mixin’);
Then we add it to BrowseTracksView before further modifying the component:
// List View For Browsing Songs
var BrowseTracksView = React.createClass({
mixins: [TimerMixin],
timeoutID: (null: any),
getInitialState: function () {
return {
dataSource: new ListView.DataSource({
rowHasChanged: (row1, row2) => row1 !== row2
})
};
},
componentDidMount: function() {
this.fetchData();
},
fetchData: function (query) {
var queryString = '';
if (query) {
queryString = '&q=' + query
}
// Return live data
fetch(this.fetchEndpoint + queryString)
.then((response) => response.json())
.then((responseData) => {
this.setState({
dataSource: this.state.dataSource.cloneWithRows(responseData)
});
}).catch((error) => {
console.warn(error);
})
.done();
},
fetchEndpoint: 'http://api.soundcloud.com/tracks.json?client_id=' + SOUNDCLOUD_CLIENT_ID,
onSearchChange: function (event) {
var q = event.nativeEvent.text.toLowerCase();
this.clearTimeout(this.timeoutID);
this.timeoutID = this.setTimeout(() => this.fetchData(q), 100);
},
render: function () {
return (
<ListView
dataSource={this.state.dataSource}
renderRow={this.renderTrack}
renderHeader={this.renderSearchBar}
style={styles.listView}/>
);
},
renderSearchBar: function () {
return (
<View style={styles.searchCell}>
<TextInput onChange={this.onSearchChange} placeholder={'Search Here'} style={styles.searchContainer}/>
</View>
)
},
renderTrack: function (track) {
return (
<TrackCell track={track}/>
);
}
});
Finally we add some styling attributes to give the searchContainer some layout info (if the height isn’t explicitly set here it will just collapse to zero.)
searchCell: {
flex: 1,
flexDirection: 'row'
},
searchContainer: {
height: 40,
width: 100,
flex: 1,
margin: 4,
padding: 4,
borderColor: 'gray',
color: 'black',
borderWidth: 1
},Try it out in the simulator by typing something awesome and checking out the results.

Navigation
Sooner or later most apps need a UI for navigating between discrete screens —in iOS this is commonly handled via a navigation stack. An app starts on a single root view and when a user taps on an item to look at it’s details, a new screen is pushed on top of the view stack. The navigation header provides a consistent back button in the upper-left corner of the screen that pops the current view off the stack revealing the view below.
React Native allows access to the NavigationController via the NavigatorIOS component. After importing the component, we replace our BrowseTracksView with the NavigatorIOS component and then specify BrowseTracksView as the root component for our navigation. This will allow for seamless navigation between our list of track search results and individual detail views of tracks. Notice that we are keeping the Now Playing footer outside of the Navigator since it is a global UI element that should persist regardless of what screen we are currently viewing.
var ReactNativeMusic = React.createClass({
getInitialState: function () {
return {
nowPlaying: null
}
},
render: function() {
return (
<View style={styles.appContainer}>
<NavigatorIOS style={styles.navContainer}
barTintColor='#F5FCFF'
initialRoute={{
title: 'React Native Music',
component: BrowseTracksView
}} />
<NowPlayingFooterView nowPlaying={this.props.nowPlaying}/>
</View>
);
}
});Sure enough our tracks list is now wrapped by a navigation header. To turn our track cells into touchable navigation links, we wrap the TrackCell component with a TouchableHighlight and specify a press handler. A new component we are about to build, TrackScreen, is pushed onto the stack and passed the targeted track object as a property.
var TrackCell = React.createClass({
render: function () {
return (
<TouchableOpacity onPress={() => this.selectTrack(this.props.track)} style={styles.rightContainer}>
<View style={styles.trackCell}>
<Image
source={{uri: this.props.track.artwork_url}}
style={styles.thumbnail} />
<View style={styles.rightContainer}>
<Text style={styles.trackTitle}>{this.props.track.title}</Text>
<Text style={styles.trackArtist}>{this.props.track.user.username}</Text>
</View>
</View>
</TouchableOpacity>
);
},
selectTrack: function (track) {
this.props.navigator.push({
title: track.title,
component: TrackScreen,
passProps: {track}
});
}
});We also need to pass BrowseTrackView’s navigator property to the cell by including it in a property on the renderTrack method.
...,
renderTrack: function (track) {
return (
<TrackCell navigator={this.props.navigator} track={track}/>
);
},
...
Now let’s set up a basic details view within the TrackScreen component.
var TrackScreen = React.createClass({
render: function () {
var largeImageUrl = this.props.track.artwork_url ? this.props.track.artwork_url.replace('-large', '-t300x300') : '';
return (
<View style={styles.trackScreen}>
<Image
style={styles.largeArtwork}
source={{uri: largeImageUrl}}>
</Image>
<Text style={styles.trackTitle}>{this.props.track.title}</Text>
<Text style={styles.trackArtist}>{this.props.track.user.username}</Text>
<View style={styles.buttonRow}>
<TouchableHighlight style={styles.playButton}>
<Text style={styles.playButtonText}>Play Track</Text>
</TouchableHighlight>
</View>
</View>
);
}
});var styles = StyleSheet.create({
...
buttonRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'stretch',
},
largeArtwork: {
width: 300,
height: 300
},
playButton: {
backgroundColor: '#4472B9',
margin: 4,
padding: 4,
borderRadius: 4,
flex: 1
},
playButtonText: {
color: 'white',
fontSize: 20
},
trackScreen: {
flex: 1,
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
trackTitle: {
fontSize: 20,
marginBottom: 8,
textAlign: 'center',
},
trackArtist: {
fontSize: 12,
marginBottom: 6,
textAlign: 'center',
}, ...Voila! Tap on any of our track results to see our shiny new details view.
