How to make (a mini) Yik Yak with React Native

Hershy Bateea
8 min readOct 25, 2016

Part 2 of 2.

Check out Part 1 if you haven’t yet.

Now that we have the basic set-up for the application, we will allow users to navigate between the “All Posts” page and a specific post. In addition, we want users to be able to comment and like other posts.

Step 1: Set Up Navigation

To add navigation, we will first create a new file within our “MinYikYak” folder, call it PostPage.js, and cut all the code from index.ios.js into the new file. Index.ios.js should not be empty, and we will need to delete the last line of the PostPage.js and change the class’s name from MinYikYak to PostPage.

/* change */
...
export default class PostPage extends Component {
...
/* delete: AppRegistry.registerComponent('MinYikYak', () => MinYikYak); */

It is important that we still use the index.ios.js file, so we still must create a MinYikYak class. To create a class, we need to import the React and React Native elements that we will use in addition to using the AppRegistry to display the application. In addition, we want to import the “new” class we just created and display that within the index.ios.js file. The code should look as so:

import React, { Component } from 'react';
import {
AppRegistry,
StyleSheet,
NavigatorIOS,
} from 'react-native';
import PostPage from './PostPage';export default class MinYikYak extends Component {
render() {
return (
<PostPage />
);
}
}
AppRegistry.registerComponent('MinYikYak', () => MinYikYak);

Most importantly, to make the new page navigate, we must use the NavigatorIOS component. ** The differences between android and ios starts here, android users will want to use the regular Navigator component **. We need to add the NavigatorIOS component and the appropriate styles.

const styles = StyleSheet.create({
container: {
flex: 1
}
});
class MinYikYak extends Component {
render() {
return (
<NavigatorIOS
style={styles.container}
initialRoute={{
title: 'Mini Yik Yak',
component: PostPage
}}/>
);
}
}

The initialRoute prop simply states what the first component rendered will be. After refreshing, you should see a label on top that says Mini Yik Yak. We now have an application that can move to other pages, which we will revisit later.

Step 2: Setting Up Likes and Scroll

Now, let’s add the ability for users to like other posts. Whenever a user clicks an icon, we want to toggle the like (like if not liked, unlike if liked). Go to the PostPage folder and add a function toggleLike(key) that uses the key of a component to increase/decrease the number of likes the post has.

  toggleLike(key) {
var post = this.state.posts[key];
post.likes == 0 ? post.likes += 1 : post.likes -= 1;
this.setState({ posts: this.state.posts });
}
renderPost(key) {
return (
...
<Icon ... onPress={this.toggleLike.bind(this, key)}
...
);
}

If you are unfamiliar with the ? : notation, it is essentially the same as writing:

if (post.likes == 0) {
post.likes += 1;
} else {
post.likes -= 1;
}

The function sets the number of likes and sets the state. The icon triggers the function when it is pressed and passes the key parameter to the toggleLike. Remember that .bind(this) is used so that the function called uses this in the context of the class. You should notice that now you can like and unlike the post, and the icon changes color as well.

In addition, we want the user to be able to scroll after multiple posts are added. This requires few changes to the render() method. Import the ScrollView React component, change the container style to only include a padding: 15, and change the outside View in the render method to a ScrollView.

import {
...
ScrollView,
} from 'react-native';
...
render() {
var posts = Object.keys(this.state.posts).reverse();
return (
<ScrollView style={styles.container}>
...
</ScrollView>
);
}
...
const styles = ... {
container : {
padding: 15,
},
...
});

You should notice that now we can scroll through the page.

Step 3: Finish Navigation

Now, we want a user to be able to click on a post and see the comments, and comment on a post. To start, create a new file called PostResult.js and create the class and export it. Remember to import the PostResult class within the PostPage class, the Icon, the React and React-Native requirements, and copy the styles from PostPage into PostResult. PostResult should display the post and the comments below. We already have the code to display a post, so copy all the code within the renderPost function of PostPage and put that into the render function of PostResult. Your code should look like this:

render() {
return (
<View key={key} style={styles.rowContainer}>
<View style={styles.textContainer}>
<Text style={styles.time}>{timeSince(key)}</Text>
<Text style={styles.title}>
{this.state.posts[key].message}
</Text>
</View>
<View>
<Text>{this.state.posts[key].likes}</Text>
<Icon name="thumbs-up"
size={30}
color={this.state.posts[key].likes == 0 ? '#dddddd' : '#32cd32'}
onPress={this.toggleLike.bind(this, key)}/>
</View>
</View>
);
}

How do we get the PostResult page to display? We need to add the ability to navigate to the new component when the post is pressed. We first must make a function that can navigate from the PostPage to the PostResult component. The NavigatorIOS component that we used in index.ios.js passes a prop to the PostPage component called navigator. When we push onto this function, we move the given component on top of the current component and the new one is displayed. Create a nextPage(key) function within PostPage:

  nextPage(key) {
this.props.navigator.push({
title: 'Post',
component: PostResult
});
}

Now we need to trigger the nextPage function. To do this, we need to make each post in the PostPage a button by surrounding it with a TouchableOpacity component inside the renderPost function. First import the TouchableOpacity component, and add it to the renderPost method with its onPress method calling nextPage:

import {
...
TouchableOpacity,
} from 'react-native';
renderPost(key) {
return (
<TouchableOpacity key={key} onPress={this.nextPage.bind(this, key)}>
<View key={key} style={styles.rowContainer}>
...
</View>
</TouchableOpacity>
);
}

Now when you make a new post and click the post, it should…error? But… when we look at the first line of the error, it comes from the render function of PostResult which means that we are one step closer to rendering the function! The problem with the PostResults should be pretty easy to find, there is no variable called key. So how can we get the variable from one parent component into a child component? Use props! If you are familiar with React, passing props to another component might look like this:

/* Passing prop from PostPage.js */
<PostResult key={key} />
/* Receiving prop in PostResult.js */
this.props.key

However, since we are using the navigator, receiving props looks the same, but passing them is a little different:

  nextPage(key) {
this.props.navigator.push({
title: 'Post',
component: PostResult,
passProps: {time: key,
post: this.state.posts[key],
timeSince: timeSince,
toggleLike: this.toggleLike.bind(this)}

});
}

In this, we are saying that — in the PostResult component — you can access the key through this.props.time, the post through this.props.post, and the timeSince function through this.props.timeSince. Notice how we use .bind(this) for toggleLike and not for timeSince, this is because timeSince is not within the class and does not use class variables using “this”. We must go back to the PostResult.js file to change the code that we added in earlier.

render() {
return (
<View style={styles.wrapper} >
<ScrollView contentContainerStyle={styles.wrapper}>
<View style={styles.rowContainer}>
<View style={styles.textContainer}>
<Text style={styles.time}>{this.props.timeSince(this.props.time)}</Text>
<Text style={styles.title}>
{this.props.post.message}
</Text>
</View>
<View>
<Text>{this.props.post.likes}</Text>
<Icon name="thumbs-up"
size={30}
color={this.props.post.likes == 0 ? '#dddddd' : '#32cd32'}
onPress={this.props.toggleLike.bind(this, this.props.time)}/>
</View>
</View>
</ScrollView>
</View>
);
}
...
const styles = ... {
...
wrapper: {
flex: 1
},

...
});

We added the ScrollView to this code because the comments must be scrolled through as more are added. Other styling and props issues are also fixed in this process. You might notice that pressing the like button on the PostResult page does not update the like in the page itself. This is an issue with NavigatorIOS, and is fixed when using the regular Navigator component, which is not as convenient to use. A simple fix to this problem is to add the following:

/* In PostPage.js */
toggleLike(key, result) {
var post = this.state.posts[key];
post.likes == 0 ? post.likes += 1 : post.likes -= 1;
this.setState({ posts: this.state.posts });
if (result) {
this.props.navigator.replace({
title: 'Post',
component: PostResult,
passProps: {time: key,
post: this.state.posts[key],
timeSince: timeSince,
toggleLike: this.toggleLike.bind(this)}
});
}

}
renderPost(key) {
...
<Icon ...
onPress={this.toggleLike.bind(this, key, false)}/>
...
}
/* In PostResult.js */
render() {
...
<Icon ...
onPress={this.props.toggleLike.bind(this, this.props.time, true)}/>
...
}

This code checks if the like button was pressed in the PostResult or the PostPage component. If it was in the PostResult, it will re-render the page, otherwise not. This is a short fix to the problem, but using Navigator provides a better solution.

Step 4: Finish Comments

We will now add the input text that’s tied to the bottom of the screen allowing users to post comments. Using the same process that we did for the postInput, we must create a constructor in PostResult with a state variable that holds the comment string. In addition, we must add a TextInput component outside of the ScrollView component. This is because we don’t want the input to scroll out of view, we want it to stay fixed at the bottom.

constructor(props) {
super(props);
this.state = {
commentString: '',
}
}
onCommentTextChanged(event) {
this.setState({ commentString: event.nativeEvent.text });
}

render() {
...
</ScrollView>
<View style={{flexDirection : 'row', bottom : 0}} >
<TextInput
value={this.state.commentString}
placeholder="Comment"
style={styles.searchInput}
onChange={this.onCommentTextChanged.bind(this)}/>
<TouchableHighlight style={styles.button}
underlayColor='green'>
<Text style={styles.buttonText}>Reply</Text>
</TouchableHighlight>
</View>
...
}

As you can see this is almost the same code that we used in the PostPage.js file. We used an inline-styling technique for the outer view and we’ll see why in just a second. Reloading the page, we can see that everything works well, but at the top going to Hardware->Keyboard->Toggle Software Keyboard, we can see that the input actually gets covered when the comment is clicked on. Since this part is not important to the overall effect, I added the code below, and you can understand what is happening.

import {
...
Dimensions,
Keyboard,

} from 'react-native';
...
constructor(props) {
super(props);
this.state = {
visibleHeight: Dimensions.get('window').height,
commentString: '',
}
this.height = this.state.visibleHeight;
}
componentWillMount () {
Keyboard.addListener('keyboardWillShow', this.keyboardWillShow.bind(this))
Keyboard.addListener('keyboardWillHide', this.keyboardWillHide.bind(this))
}
keyboardWillShow (e) {
let newSize = Dimensions.get('window').height - e.endCoordinates.height
this.setState({visibleHeight: newSize})
}
keyboardWillHide (e) {
this.setState({visibleHeight: Dimensions.get('window').height})
}

render() {
return (
...
</ScrollView>
<View style={{flexDirection : 'row', bottom : this.height - this.state.visibleHeight}} >
...
);
}

Now, typing in the comment section with the toggled keyboard should still work perfectly. We are almost done! All we have to do now is add a function that allows a user to add a comment. However, updating the comment in PostResult.js by doing:

this.props.post.comments.push(this.state.commentString);

would work perfectly, but it wouldn’t update the state of the posts that is in the PostPage.js file. How can we update a parent post through a child component? Break this problem down into parts. How can we update the comment in the PostPage.js file. We must add a function that takes in a key, and a comment and updates the state of the component after adding the comment to the post.

/* In PostPage.js inside the PostPage class */
loadComment(key, comment) {
this.state.posts[key].comments.push(comment);
this.setState({ posts : this.state.posts });
}

How can we call this function through the child component? Pass it through props! Edit the props now:

toggleLike(key, result) {
...
passProps: {...
loadComment: this.loadComment.bind(this)}
});
}
}
nextPage(key) {
...
passProps: {...
loadComment: this.loadComment.bind(this)}
});
}

Now move to PostResult.js and add the the following code to load the comments.

loadComment() {
this.props.loadComment(this.props.time, this.state.commentString);
this.setState({ commentString: '' });
}

render() {
return (...
<TouchableHighlight style={styles.button}
underlayColor='green'
onPress={this.loadComment.bind(this)}>
<Text style={styles.buttonText}>Reply</Text>
</TouchableHighlight>
);
}

Now after adding a comment it disappears! Nothing happened?! That’s because, just like the posts, we have to map the comments to a function that will display all of them. So finally,

renderComments(key) {
return (
<View key={key} style={styles.rowContainer}>
<View style={styles.textContainer}>
<Text style={styles.title}>{key}</Text>
</View>
<View style={styles.separator}/>
</View>
)
}

render() {
return (
<View style={styles.wrapper} >
<ScrollView contentContainerStyle={styles.wrapper}>
<View style={styles.rowContainer}>
...
</View>
<View style={styles.separator}/>
{this.props.post.comments.map(this.renderComments.bind(this))}
</ScrollView>
...
);
}
...
const styles = ... {
...
separator: {
height: 1,
backgroundColor: 'black'
},
)};

The separator style can be used to make the definition between the post and comments more defined. We have now completed the Mini Yik Yak application! You can like, unlike, comment and navigate within the app. Unfortunately, after refreshing the app, the whole screen turns blank and posts don’t persist. This can be fixed by adding a backend. Popular backends for React applications include Firebase, Node, and Meteor as they are lightweight and easily set up. Have fun yakking!

--

--

Hershy Bateea

Hello, I’m Hershal Bhatia, currently a Software Engineer at Amazon – AWS where I am building out a new service. Check out more at www.hershalb.com