This project was initialized using Create React Native App. If you want to follow along, go ahead and install and initialize your own app, or explore this repo for the finished code.
Install dependencies with npm install
(or yarn
if you're into that), and run
the packager with npm start
(or yarn start
). Open it in the
Expo app on your phone to view it.
Follow the instructions at Create React Native App to get a new project started. This repo was also created using the CRNA template.
In order to have a functioning chat app, we need users to be associated with a username. In order to keep this simple, we're going to bypass any auth system and consider a user "logged in" if they have entered a username. We won't validate the username in any way either.
Go ahead and open up App.js
and add the following lines directly below the
line that says export default class App extends React.Component {
:
constructor(props) {
super(props);
this.state = {
username: '',
chats: [],
};
}
The state
object is basically a key-value database that lives with a particular
component through it's lifecycle. App.js
is the main page for our app, so the
lines we added initialized this object with an empty username and chat array.
But nothing in our app changed, since we never access this state
object
anywhere. So let's add some code that shows a different screen if the user isn't
logged in.
Add the following lines just below the line that says render() {
:
if (!this.state.username) return (
<View style={styles.container}>
<Text>You are not logged in.</Text>
</View>
);
Now if you refresh your app, you should be directed to a page that tells you
you aren't logged in. The render()
function is what a component runs in order
to generate a UI element from each component. Since we modified the render()
function of our main component, it's showing the view that we specified (since
this.state.username
will always be empty string).
Before we allow the user to actually register a username, let's clean up our
code a little bit. Even though our render()
function isn't particularly messy,
we can restructure a few things to make our intent even more clear:
_renderLogin() {
return (
<View style={styles.container}>
<Text>You are not logged in.</Text>
</View>
);
}
render() {
if (!this.state.username) return this._renderLogin();
return (
<View style={styles.container}>
<Text>Open up App.js to start working on your app!</Text>
...
);
}
We just created our first small abstraction! And our intent is very clear: if there is no username in our state, render the login page.
Let's take this a step further and create a separate component for the login
screen. First we need to modify our file structure in order to keep organized.
Create a new folder called screens
and a file within that folder called
Login.js
. Then populate that file with the code from
my Login screen.
We wrote our login screen, but nothing happened! In order for our new component
to show, we need to integrate it into our App.js
code. At the top of your
App.js
, add this line: import Login from './screens/Login';
. It's important
that you capitalize the Login
, since React won't work if it's lowercase.
Now we can use the Login
component in our app! Go ahead and modify our
_renderLogin()
function to be this:
_renderLogin() {
return (
<Login />
);
}
Woo, we just finished our first component! Unfortunately, it doesn't quite work
as expected. This is because we have a line in ./screens/Login.js
that reads:
onSubmitEditing={() => this.props.register(this.state.username)}
, but
this.props.register
doesn't exist.
In order to fix this, we can define this property in App.js
like this:
_renderLogin() {
return (
<Login register={ username => this.setState({ username }) } />
);
}
Now we pass a function down to the Login
component that updates the username
property in the state
. If this looks like gibberish to you, these articles on
ES6 Arrow Functions,
Object notation,
and setState()
may be useful.
Congrats, you've finished our simple login flow!
Now let's create the meat of our app, the chat screen. Rather than writing it in
App.js
, put it in our /screens
directory as Chat.js
. Go ahead and copy my
code from Chat-Starter.js
into your
screens/Chat.js
file. There are a lot of new concepts in this file, so try to
read through it. You may find these articles on
template strings,
Array.prototype.map()
,
Function.prototype.bind()
, and
TextInput
helpful. Feel free to post an issue if you don't understand anything, or would
do something different yourself 😃
You may have noticed that this screen also expects a few props
, namely
username
, chats
, sendMessage
, and logout
. It's time to add our chat
screen into App.js
, making sure to pass it these expected values.
First, import the component by adding import Chat from './screens/Chat';
, then
modify the render()
function in App.js
so that it looks like this:
render() {
if (!this.state.username) return this._renderLogin();
else return this._renderChat();
}
Look at how nice and succinct our render()
function is! Now we need to write
the _renderChat()
function:
_renderChat() {
return (
<Chat
username={this.state.username}
chats={this.state.chats}
sendMessage={this._sendMessage.bind(this)}
logout={() => this.setState({ username: '' })}
/>
);
}
Lastly, we need to write the _sendMessage()
function. For now, we'll just
push a value to the array in state
(using the
spread operator):
_sendMessage(message) {
this.setState({
chats: [
...this.state.chats,
{
id: this.state.chats.length,
username: this.state.username,
text: message,
},
]
});
}
Now everything should function! But did you spot the bug? Users will probably be
frustrated if their keyboard covers their textbox. Fortunately, Facebook
provides
a component
to fix this. In screens/Chat.js
, replace the outer <View>
with
<KeyboardAvoidingView behavior={'padding'} style={styles.wrap}>
. Don't forget
to replace the last </View>
with </KeyboardAvoidingView>
and to add
KeyboardAvoidingView
to the list of import
s at the top.
Alright, let's do a quick overview of everything our app does so far.
- The
state
object inApp.js
is initialized with a blank username and empty chats - The user is redirected to our
<Login/>
screen as long as the username is empty - The
<Login/>
screen is passed a function (via its props) calledregister
, which updates theApp.js
state to its argument - Once a user is "logged in" (has a username), they are redirected to the
<Chat/>
screen - The
<Chat/>
screen has assendMessage
function that updates theApp.js
state, pushing a new object to thechats
array - The
<Chat/>
screen is also passed a prop calledlogout
, which is a function that logs out the user by updating theApp.js
state with an empty username
Everything functions, but the design could use some work...
So far, we've created a couple of components (Login
and Chat
) that function
as full screens. But let's also create a Message
component that will be used
within our Chat
component.
Whoa!
Turns out components are composable, meaning they can be nested or used in arbitrary combinations.
Create a new directory called components
in this directory. The screens
directory contains the components that form full pages, whereas components
will contain smaller components that can be used to build the pages.
Within /components
, create a file called Message.js
, with the following
code:
import React from 'react';
import { Text, } from 'react-native';
export default (props) => (
<Text>{`${props.username}: ${props.text}`}</Text>
);
How is that a component if it doesn't use React.Component
as a base?
Well, all React components are just functions that return elements. By using
React.Component
as a base, you get lots of cool functionality (like state
,
and more). But any
function that return an element is a valid component. Components (like
Message
) that return purely based on their props are often referred to as
stateless functional components, pure components, presentational
components, or dumb components.
We can add our Message
component into our Chat
screen by adding
import Message from '../components/Message';
to our screens/Chat.js
file,
then changing:
<ScrollView style={styles.chats}>
{this.props.chats.map(x => <Text key={x.id}>{x.text}</Text>)}
</ScrollView>
to
<ScrollView style={styles.chats}>
{this.props.chats.map(x => <Message key={x.id} {...x} />)}
</ScrollView>
Now each message is rendered as a separate <Message/>
component! Feel free to
style your own Message
component, or use mine.
Now our app is done, but each user can only post messages to their own local
state
. Let's hook our app up to a backend so people can post messages to a
server that can be accessed by other people!
I already wrote a basic server for this project, and you can check out the code if interested. If you POST a valid object to any endpoint, it will add it to the chat database. If you GET any endpoint, it will return an array of chats. Follow the instructions in the backend's README to start the backend server (it runs on port 8080 by default, but feel free too change the port in server.js).
In order to get our app to communicate with our backend, we'll need to use some
HTTP calls with
fetch()
.
We could add these functions to App.js
, but it would be better to abstract
them out into a separate api
folder. So go ahead and create this directory as
well as a file called api.js
. Use my implementation, or feel
free to write your own. If you choose to use mine, you'll also need a Config
file at /constants/Config.js
.
Import our api calls into App.js
with import { getChats, postChat, } from './api/api';
, then we can use them to communicate between our app and the
backend. Modify our _sendMessage()
function to look like this:
_sendMessage(message) {
postChat({
username: this.state.username,
text: message,
});
}
Since we had previously abstracted out a function to send messages, swapping for a new implementation was easy! This allows us to add new messages to the server, but we need a way to get the messages as well. The standard way would be to use sockets, but we're going to use a technique called long polling (requesting every n seconds) since it's easier to see the effects.
We can declare our poll interval at the top of App.js
with a line that reads
const POLL_INTERVAL = 500;
. I used an interval of a half-second, but feel free
to use any value you want.
Then in App.js
, create a new function called componentWillMount()
defined as:
componentWillMount() {
this.interval = setInterval(() => {
getChats().then(chats => this.setState({ chats }))
}, POLL_INTERVAL)
}
componentWillMount()
is a special function that is called automatically by React when a component
first mounts. We set an interval that calls our getChats()
function every
POLL_INTERVAL
seconds. We store this interval as part of the component, so we
can clear the interval in componentWillUnmount()
once the component unmounts:
componentWillUnmount() {
this.interval && clearInterval(this.interval);
}
And now we're finished! Congrats on writing your first full app!
To wrap up, here's what our app does:
- The
state
object inApp.js
is initialized with a blank username and empty chats - The user is redirected to our
<Login/>
screen as long as the username is empty - The
<Login/>
screen is passed a function (via its props) calledregister
, which updates theApp.js
state to its argument - Once a user is "logged in" (has a username), they are redirected to the
<Chat/>
screen - The
<Chat/>
screen has assendMessage
function that updates theApp.js
state, pushing a new object to thechats
array - The
<Chat/>
screen is also passed a prop calledlogout
, which is a function that logs out the user by updating theApp.js
state with an empty username - We fetch a new
chats
array everyPOLL_INTERVAL
milliseconds - Every time the
chats
array in<Chat/>
is updated, wemap()
through the values and create a<Message/>
for each message - When the user sends a message, we use our
postMessage()
API call to send it to the backend
Hopefully this seminar/repo has helped you understand abstractions and why we component libraries like React are so popular.
Feel free to send a PR if you think something in this lesson can be improved!