├── .watchmanconfig ├── assets ├── icon.png └── splash.png ├── babel.config.js ├── .gitignore ├── app.json ├── package.json ├── Header.js ├── App.js └── README.md /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevakallio/react-native-chat-tutorial/HEAD/assets/icon.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevakallio/react-native-chat-tutorial/HEAD/assets/splash.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | .expo/* 3 | npm-debug.* 4 | *.jks 5 | *.p12 6 | *.key 7 | *.mobileprovision 8 | *.orig.* 9 | web-build/ 10 | web-report/ 11 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "ReactNativeChatDemo", 4 | "slug": "ReactNativeChatDemo", 5 | "privacy": "public", 6 | "sdkVersion": "33.0.0", 7 | "platforms": [ 8 | "ios", 9 | "android", 10 | "web" 11 | ], 12 | "version": "1.0.0", 13 | "orientation": "portrait", 14 | "icon": "./assets/icon.png", 15 | "splash": { 16 | "image": "./assets/splash.png", 17 | "resizeMode": "contain", 18 | "backgroundColor": "#ffffff" 19 | }, 20 | "updates": { 21 | "fallbackToCacheTimeout": 0 22 | }, 23 | "assetBundlePatterns": [ 24 | "**/*" 25 | ], 26 | "ios": { 27 | "supportsTablet": true 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ReactNativeChatDemo", 3 | "version": "0.2.0", 4 | "private": true, 5 | "main": "node_modules/expo/AppEntry.js", 6 | "scripts": { 7 | "start": "expo start", 8 | "android": "expo start --android", 9 | "ios": "expo start --ios", 10 | "web": "expo start --web", 11 | "eject": "expo eject" 12 | }, 13 | "dependencies": { 14 | "expo": "^33.0.0", 15 | "react": "16.8.3", 16 | "react-dom": "^16.8.6", 17 | "react-native": "https://github.com/expo/react-native/archive/sdk-33.0.0.tar.gz", 18 | "react-native-training-chat-server": "^2.0.0", 19 | "react-native-web": "^0.11.4" 20 | }, 21 | "devDependencies": { 22 | "babel-preset-expo": "^5.1.1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {View, Text, StyleSheet, StatusBar} from 'react-native'; 3 | 4 | export default class Header extends React.Component { 5 | render() { 6 | return ( 7 | 8 | 9 | 10 | #{this.props.title} 11 | 12 | 13 | ); 14 | } 15 | } 16 | 17 | const styles = StyleSheet.create({ 18 | header: { 19 | height: 80, 20 | backgroundColor: 'lightseagreen', 21 | alignItems: 'center', 22 | justifyContent: 'flex-end', 23 | padding: 10, 24 | }, 25 | title: { 26 | color: 'white', 27 | fontWeight: 'bold', 28 | fontSize: 24, 29 | }, 30 | }); 31 | -------------------------------------------------------------------------------- /App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | StyleSheet, 4 | Text, 5 | View, 6 | TextInput, 7 | TouchableOpacity, 8 | KeyboardAvoidingView, 9 | StatusBar, 10 | FlatList, 11 | Image 12 | } from 'react-native'; 13 | 14 | import { send, subscribe } from 'react-native-training-chat-server'; 15 | import Header from './Header'; 16 | 17 | const NAME = '@realDonaldTrump'; 18 | const CHANNEL = 'Random'; 19 | const AVATAR = 20 | 'https://pbs.twimg.com/profile_images/874276197357596672/kUuht00m_400x400.jpg'; 21 | 22 | export default class App extends React.Component { 23 | state = { 24 | typing: '', 25 | messages: [] 26 | }; 27 | 28 | componentWillMount() { 29 | subscribe(CHANNEL, messages => { 30 | this.setState({ messages }); 31 | }); 32 | } 33 | 34 | sendMessage = async () => { 35 | // read message from component state 36 | const message = this.state.typing; 37 | 38 | // send message to our channel, with sender name 39 | await send({ 40 | channel: CHANNEL, 41 | sender: NAME, 42 | avatar: AVATAR, 43 | message 44 | }); 45 | 46 | // set the component state (clears text input) 47 | this.setState({ 48 | typing: '' 49 | }); 50 | }; 51 | 52 | renderItem({ item }) { 53 | return ( 54 | 55 | 56 | 57 | {item.sender} 58 | {item.message} 59 | 60 | 61 | ); 62 | } 63 | 64 | render() { 65 | return ( 66 | 67 |
68 | 73 | 74 | 75 | this.setState({ typing: text })} 81 | /> 82 | 83 | Send 84 | 85 | 86 | 87 | 88 | ); 89 | } 90 | } 91 | 92 | const styles = StyleSheet.create({ 93 | container: { 94 | flex: 1, 95 | backgroundColor: '#fff' 96 | }, 97 | row: { 98 | flexDirection: 'row', 99 | padding: 20, 100 | borderBottomWidth: 1, 101 | borderBottomColor: '#eee' 102 | }, 103 | avatar: { 104 | borderRadius: 20, 105 | width: 40, 106 | height: 40, 107 | marginRight: 10 108 | }, 109 | rowText: { 110 | flex: 1 111 | }, 112 | message: { 113 | fontSize: 18 114 | }, 115 | sender: { 116 | fontWeight: 'bold', 117 | paddingRight: 10 118 | }, 119 | footer: { 120 | flexDirection: 'row', 121 | backgroundColor: '#eee' 122 | }, 123 | input: { 124 | paddingHorizontal: 20, 125 | fontSize: 18, 126 | flex: 1 127 | }, 128 | send: { 129 | alignSelf: 'center', 130 | color: 'lightseagreen', 131 | fontSize: 16, 132 | fontWeight: 'bold', 133 | padding: 20 134 | } 135 | }); 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | React Native Chat App 2 | === 3 | _A step-by-step tutorial to create your own Chat app with React Native_ 4 | 5 | - **At the Reactivate training? 👋 Jump to [Let's code!](#lets-code)** 6 | - See [App.js](App.js) for the ready app source code 7 | - Try the app on [Expo](https://exp.host/@jevakallio/reactnativechatdemo) 8 | - Table of Contents 9 | - [Introduction](#introduction) - Start here to learn how to use Create React Native App 10 | - [Code walkthrough](#code-walkthrough) - Start here if you have created a new Create React Native app but aren't yet familiar with React, ES6 and JSX 11 | - [Let's code!](#lets-code) - Start here if you understand the basics of React and want to get hacking. 12 | - [Publish your app!](#publish-your-app) - When you're happy with your app, publish it to Expo. 13 | 14 | 15 | 16 | # Introduction 17 | 18 | This tutorial will walk through building a simple real-time chat app with React Native for Android and iOS. Along the way, you'll get to practice React Native basics and learn about tools you can use to build apps. 19 | 20 | Work the tutorial at your own pace. The instructions below assume that you are comfortable with writing JavaScript and using npm. The tutorial assumes the use of `npm`, but the equivalent `yarn` commands will work as well. 21 | 22 | ## Set up development environment 23 | 24 | We'll build the app from scratch. That means you don't need to clone this repository. Instead install the [Expo CLI](https://docs.expo.io/versions/latest/workflow/expo-cli/) from NPM and generate a new project: 25 | ```sh 26 | npm install -g expo-cli 27 | expo init MyChatApp 28 | ``` 29 | The cli will ask you to choose between a blank template, and one with react-navigation already set up. For this tutorial, choose a blank template. 30 | 31 | This will create a new React Native project in a directory `./MyChatApp`. 32 | 33 | In order to run the Expo app on your phone, you'll need to create an Expo account and log in. For this, simply go to https://expo.io/signup and sign up for an account. 34 | 35 | Then, log in on the cli: 36 | ```sh 37 | expo login 38 | ``` 39 | 40 | Now let's go back to the project we generated, and start it with `npm start`: 41 | ```sh 42 | cd MyChatApp 43 | npm start 44 | ``` 45 | 46 | This will open Expo Developer Tools in your browser. You are now all set to run the app on your phone. Grab your iOS or Android phone and install **Expo** 47 | ([iOS App Store](https://itunes.apple.com/app/apple-store/id982107779?mt=8) | [Android Play Store](https://play.google.com/store/apps/details?id=host.exp.exponent&referrer=www)), and log in with your newly created Expo account. It will appear in the Projects tab, under "Recently in development". 48 | 49 | **Note**: For your phone to find the local server, **both devices need to be on the same local network.** This means either connected to the same WiFi, or the laptop's internet tethered via the phone's internet sharing functionality. 50 | 51 | After dismissing the first use greeting from Expo, you should now see your app on your phone. 52 | 53 | (If you have iOS or Android simulators installed, you can run `npm run ios` or `npm run android` to start the app on the simulator instead of using a real device.) 54 | 55 | ### What's Expo? 56 | 57 | [Expo](https://expo.io/) lets web developers build truly native apps that work across both iOS and Android by writing them once in just JavaScript. It's open source, free and uses React Native. 58 | 59 | It's possible to create React Native apps without Expo (in fact, you'll need to do that if you want to write any custom Java or Swift code to enhance your app - Expo only supports JavaScript), but for learning the basics Expo is the best choice. For the more manual alternative, see [Getting Started](https://facebook.github.io/react-native/docs/getting-started.html) in React Native docs. 60 | 61 | # Code walkthrough 62 | 63 | Okay, let's get started by familiarising ourselves with the anatomy of a React module. (If you are already familiar with React, feel free to move on to [next section](#lets-code). 64 | 65 | Open the directory in your favorite IDE or text editor: [Atom](https://atom.io/), [VS Code](https://code.visualstudio.com/), [Sublime Text](https://www.sublimetext.com/), Vim, Emacs, WebStorm... anything will do. 66 | 67 | Open the **App.js** file. This is where we will do most of our coding today. It should look something like this. 68 | 69 |
70 | App.js (Click to expand) 71 | 72 | ```js 73 | import React from 'react'; 74 | import { StyleSheet, Text, View } from 'react-native'; 75 | 76 | export default class App extends React.Component { 77 | render() { 78 | return ( 79 | 80 | Open up App.js to start working on your app! 81 | Changes you make will automatically reload. 82 | Shake your phone to open the developer menu. 83 | 84 | ); 85 | } 86 | } 87 | 88 | const styles = StyleSheet.create({ 89 | container: { 90 | flex: 1, 91 | backgroundColor: '#fff', 92 | alignItems: 'center', 93 | justifyContent: 'center', 94 | }, 95 | }); 96 | ``` 97 | 98 |
99 | 100 | 101 | Looking at the file, you can see three main sections. 102 | 103 | #### Imports 104 | 105 | First we import `React` so we can create our own React components, and three named components from `react-native`, which we can use to compose our UI: 106 | 107 | ```js 108 | import React from 'react'; 109 | import { StyleSheet, Text, View } from 'react-native'; 110 | ``` 111 | 112 | The `import` keyword (and its mirror image `export` as seen on the next line) are part of the [ES6 Modules](http://www.reactnativeexpress.com/imports_and_exports) feature that allow us to split our application across multiple files and modules. React Native uses all the latest and greatest JavaScript features, even some that are not yet generally available in web browsers. 113 | 114 | #### Component 115 | 116 | Next we declare our `App` component and export it, so it can be accessed by React Native. React components are [ES6 classes](http://www.reactnativeexpress.com/classes) that extend from React.Component - but for now that is not important. This component has a single method, `render` that returns some [JSX](http://www.reactnativeexpress.com/jsx). Every React component needs a render method, and the output of that method is what you'll see on the screen. 117 | 118 | ```js 119 | export default class App extends React.Component { 120 | render() { 121 | return ( 122 | 123 | Open up App.js to start working on your app! 124 | Changes you make will automatically reload. 125 | Shake your phone to open the developer menu. 126 | 127 | ); 128 | } 129 | } 130 | ``` 131 | 132 | JSX is an extension to JavaScript that adds an ability to render React elements in a HTML-like syntax. In this case we use two types of components, `` and ``. If you were to replace `View` with `div` and `Text` with `span`, this would look almost regular HTML (and exactly the same as React on the web): 133 | ```html 134 |
135 | Open up App.js to start working on your app! 136 | Changes you make will automatically reload. 137 | Shake your phone to open the developer menu. 138 |
139 | ``` 140 | 141 | Because React Native exists for creating native apps, web primitives like div and span aren't available to us. Instead, on line 2 we imported some of the React Native primitives: View, Text, etc. There are counterparts for most important web primitives, as well as hundreds of others, either included in React Native, included in Expo, or installable via NPM. We will look at these later. 142 | 143 | #### Styles 144 | 145 | The last section in the file are the styles. If JSX reminded you of HTML, the React Native style system should remind you of CSS. 146 | ```js 147 | const styles = StyleSheet.create({ 148 | container: { 149 | flex: 1, 150 | backgroundColor: '#fff', 151 | alignItems: 'center', 152 | justifyContent: 'center', 153 | }, 154 | }); 155 | ``` 156 | 157 | The same in CSS would look something like this: 158 | ```CSS 159 | .container { 160 | display: flex; 161 | flex: 1; 162 | background-color: #fff; 163 | align-items: center; 164 | justify-content: center; 165 | } 166 | ``` 167 | 168 | In fact, React Native implements a subset of CSS in JavaScript, including the [Flexbox](http://www.reactnativeexpress.com/flexbox) layout system we'll use to arrange our app components on the screen. The `display: flex` line from the CSS translation is not necessary in React Native, because all components "flex" by default. 169 | 170 | Now, finally, let's code! 171 | 172 | # Let's code! 173 | 174 | ## Step 1. Connect to chat server 175 | In order to be able to chat with other users, we'll need a server. Happily, we have one available, and we can install a client library from NPM! Run the following in the project directory: 176 | 177 | ```sh 178 | npm install --save react-native-training-chat-server 179 | ``` 180 | 181 | Then, in **App.js** add the following lines after the react-native imports: 182 | 183 | ```js 184 | import {send, subscribe} from 'react-native-training-chat-server'; 185 | 186 | const NAME = 'Your Name'; 187 | const CHANNEL = 'Reactivate'; 188 | ``` 189 | 190 | Replace "Your name" with ... well, your name 😂! The channel can be any alphanumeric string - feel free to set up your own secret channel or use the default "Reactivate" to participate with everyone else. You'll now have access to two functions, `send` and `subscribe` that we'll use to send and receive messages. 191 | 192 | ## Step 2. Get messages 193 | 194 | Next, we'll need to subscribe to our channel when our app starts. Add the following lines immediately below the component declaration: 195 | ```js 196 | export default class App extends React.Component { 197 | //new lines below... 198 | state = { 199 | messages: [], 200 | }; 201 | 202 | componentDidMount() { 203 | subscribe(CHANNEL, messages => { 204 | this.setState({messages}); 205 | }); 206 | } 207 | //...end new lines 208 | } 209 | ``` 210 | It's just a few of lines of code, but there is a lot to unpack here. 211 | 212 | 1. First, we declare our [state](https://facebook.github.io/react-native/docs/state.html) - this is where React components can store their data and other dynamically changing state. In this case we initialize an empty state with an empty `messages` array. 213 | 2. Then, we declare a method named (exactly) `componentDidMount`, one of the special [Lifecycle methods](https://facebook.github.io/react/docs/react-component.html#the-component-lifecycle) on every React component that will be called at different phases of the component's lifetime. This one will be called just after the component has "mounted", or in our case, when the App has first started. 214 | 3. Inside the lifecycle method, we `subscribe` to our chat server with a channel name and a callback function. The callback will be called every time new messages arrive. 215 | 4. In the callback, we call another special React method, `this.setState`. This method is responsible for updating the initial state we declared above. After you call `setState`, the component will automatically re-render and update the UI of our application! 216 | 217 | You won't yet see any changes on the screen, because we haven't rendered our messages. We'll get to that next, but if you want to verify the subscription works, you can add a `console.log(messages)` into the callback. To access the debugger menu, shake your device and choose "Debug JS Remotely" option to view the log in Chrome Dev Tools. If running on simulator, see the [Debugging](https://facebook.github.io/react-native/docs/debugging.html) guide in React Native docs. 218 | 219 | ## Step 3. Render messages 220 | 221 | Next, let's put some messages on the screen! We'll want to render the messages as a list, so we'll need to start by importing the [FlatList](https://facebook.github.io/react-native/docs/flatlist.html) component from the react-native package on the top of the file: 222 | ```diff 223 | import React from 'react'; 224 | - import { StyleSheet, Text, View } from 'react-native'; 225 | + import { StyleSheet, Text, View, FlatList } from 'react-native'; 226 | ``` 227 | 228 | Then, we can replace the placeholder text in our `render` method with a FlatList: 229 | ```diff 230 | render() { 231 | return ( 232 | 233 | - Open up App.js to start working on your app! 234 | - Changes you make will automatically reload. 235 | - Shake your phone to open the developer menu. 236 | + 241 | 242 | ); 243 | } 244 | ``` 245 | 246 | We pass FlatList attributes, or [Props](https://facebook.github.io/react-native/docs/props.html). Props is how React components can pass data to each other. In this case, the props are: 247 | 248 | 1. `data={this.state.messages}` - FlatList expects an array of "data" to render, so we give it a list of messages we fetched earlier. 249 | 2. `renderItem={this.renderItem}` - FlatList also needs a callback it can call for each item in the `data` array to render the corresponding row. Here we pass it a method `this.renderItem`. 250 | 3. `inverted` - This prop will render the list in reverse order, so that latest messages are always anchored to the bottom of the list. 251 | 252 | The renderItem method isn't one of the special lifecycle methods - it's just a plain old method on the class. In fact, it doesn't even exist yet, so let's create it now. Place the renderItem method immediately *above* your component's `render` method: 253 | 254 | ```js 255 | renderItem({item}) { 256 | return ( 257 | 258 | {item.sender} 259 | {item.message} 260 | 261 | ); 262 | } 263 | ``` 264 | 265 | If you are using a Channel that someone has posted any messages in it, you should now see them on the screen! They'll look a bit ugly, and a bit squashed, though. That's because we haven't yet added any styles. Let's do that next. 266 | 267 | ## Step 4. Styling the list 268 | 269 | In the `renderItem` method, you see references to styles like `styles.row`, `styles.sender`, `styles.message`. These styles can be defined in the bottom of the file, replacing the existing StyleSheet: 270 | 271 | ```js 272 | const styles = StyleSheet.create({ 273 | container: { 274 | flex: 1, 275 | backgroundColor: '#fff', 276 | }, 277 | row: { 278 | padding: 20, 279 | borderBottomWidth: 1, 280 | borderBottomColor: '#eee', 281 | }, 282 | message: { 283 | fontSize: 18, 284 | }, 285 | sender: { 286 | fontWeight: 'bold', 287 | paddingRight: 10, 288 | }, 289 | }); 290 | ``` 291 | Feel free to play around with the styles and make it look different. The style names and values usually match how CSS works on the web, except names are written using camel casing, e.g `backgroundColor` rather than `background-color`. 292 | 293 | ## Checkpoint 294 | 295 | At this point, you should see a styled list of messages. There is no one right way to achieve this, but this is how my App.js looks like: 296 | 297 |
298 | App.js (Click to expand) 299 | 300 | ```js 301 | import React from 'react'; 302 | import {StyleSheet, Text, View, FlatList} from 'react-native'; 303 | import {send, subscribe} from 'react-native-training-chat-server'; 304 | 305 | const NAME = 'Your name'; 306 | const CHANNEL = 'Reactivate'; 307 | 308 | export default class App extends React.Component { 309 | state = { 310 | messages: [], 311 | }; 312 | 313 | componentWillMount() { 314 | subscribe(CHANNEL, messages => { 315 | this.setState({messages}); 316 | }); 317 | } 318 | 319 | renderItem({item}) { 320 | return ( 321 | 322 | {item.sender} 323 | {item.message} 324 | 325 | ); 326 | } 327 | 328 | render() { 329 | return ( 330 | 331 | 332 | 333 | ); 334 | } 335 | } 336 | 337 | const styles = StyleSheet.create({ 338 | container: { 339 | flex: 1, 340 | backgroundColor: '#fff', 341 | }, 342 | row: { 343 | padding: 20, 344 | borderBottomWidth: 1, 345 | borderBottomColor: '#eee', 346 | }, 347 | message: { 348 | fontSize: 18, 349 | }, 350 | sender: { 351 | fontWeight: 'bold', 352 | paddingRight: 10, 353 | }, 354 | }); 355 | ``` 356 | 357 |
358 | 359 | ## Step 5. Typing up messages 360 | 361 | Next, we'll allow the user to send messages. We already have access to the `send` method of the chat server, we'll just need a text input where the user can type, and a "Send" button the user can press to send the typed message. 362 | 363 | Start by importing the [TextInput](https://facebook.github.io/react-native/docs/textinput.html) primitive from `react-native`: 364 | ```diff 365 | - import {StyleSheet, Text, View, FlatList} from 'react-native'; 366 | + import {StyleSheet, Text, View, FlatList, TextInput} from 'react-native'; 367 | ``` 368 | 369 | Before we render the TextInput, we'll need a place to keep track of the text the user has typed. As you might remember from Step 2, we can use the component `state` for this. Let's amend the initial state on the top of the component declaration by adding a "typing" state variable that we'll update as the user types: 370 | ```diff 371 | state = { 372 | + typing: "", 373 | messages: [], 374 | }; 375 | ``` 376 | 377 | Now we can add our TextInput to our UI. Add the following lines into your `render` method, immediately after the `` component, but before the closing container `
` 378 | ```jsx 379 | 380 | this.setState({typing: text})} 383 | style={styles.input} 384 | underlineColorAndroid="transparent" 385 | placeholder="Type something nice" 386 | /> 387 | 388 | ``` 389 | 390 | Again, there's a lot to unpack here. One by one: 391 | 1. First we declare a footer view. We'll need this so we can later position the send button next to the input, within the footer. 392 | 2. Then we declare the TextInput. The first prop, `value`, sets the current value of the input. This is "bound" to the `typing` state variable: when `typing` is updated, the TextInput value updates. 393 | 3. This update is done in the TextInput's `onChangeText` callback: When the input text is changed by the user, we replace the `typing` state variable with the new text in order to update the input value. This pattern of listening of change events and then feeding the same value back to the component is called "Controlled component" - read about [Handling text input](https://facebook.github.io/react-native/docs/handling-text-input.html) in React Native to learn more. 394 | 4. The rest of the TextInput props are presentational. There are [many more props](https://facebook.github.io/react-native/docs/textinput.html) we could give here to control properties like on-screen keyboard type, autocorrect, autofocus etc. 395 | 396 | We don't yet see anything on the screen. That is because the text input needs styling and dimensions. Add the missing `footer` and `input` style declarations into the StyleSheet at the bottom of the file: 397 | ```js 398 | footer: { 399 | flexDirection: 'row', 400 | backgroundColor: '#eee', 401 | }, 402 | input: { 403 | paddingHorizontal: 20, 404 | paddingVertical: 10, 405 | fontSize: 18, 406 | flex: 1, 407 | }, 408 | ``` 409 | 410 | And now we should have a visible input field at the bottom of the screen! Before we go ahead and add the Send button, there is one more thing we need to do. Notice how the on-screen keyboard hides the text input, so you don't know what you are typing? Annoying, but don't worry, it's easy to fix with the help of [KeyboardAvoidingView](https://facebook.github.io/react-native/docs/keyboardavoidingview.html) 411 | 412 | Import it from react-native: 413 | ```diff 414 | - import {StyleSheet, Text, View, FlatList, TextInput} from 'react-native'; 415 | + import {StyleSheet, Text, View, FlatList, TextInput, KeyboardAvoidingView} from 'react-native'; 416 | ``` 417 | 418 | And wrap it around our footer View, giving it a `behavior` prop value of "padding": 419 | ```diff 420 | + 421 | 422 | this.setState({typing: text})} 428 | /> 429 | 430 | + 431 | ``` 432 | 433 | And that should do it! 434 | 435 | ## Step 6: Send messages 436 | 437 | Now that we can collect user input, we'll want to send it to our server. Let's declare a `sendMessage` method on our component, for example below the `componentDidMount` function we used when subscribing to messages: 438 | 439 | ```js 440 | async sendMessage() { 441 | // send message to our channel, with sender name. 442 | // the `await` keyword means this function execution 443 | // waits until the message is sent 444 | await send({ 445 | channel: CHANNEL, 446 | sender: NAME, 447 | message: this.state.typing 448 | }); 449 | 450 | // set the component state (clears text input) 451 | this.setState({ 452 | typing: '', 453 | }); 454 | } 455 | ``` 456 | 457 | This function looks slightly different that our other methods because of the `async` keyword that precedes the method name. In the middle of the function, you see another keyword `await`. These are part of the ES7 `async/await` feature, which makes it easier to deal with asynchronous code where you would normally have used Promises. For the purposes of this tutorial, going deeper into async/await is not important, but they are very useful and worth [learning more about](https://ponyfoo.com/articles/understanding-javascript-async-await). 458 | 459 | We then need a Send button to call our `sendMessage` method. Let's start (you know the drill by now) by importing one more primitive from react-native, this time `TouchableOpacity`: 460 | ```diff 461 | - import {StyleSheet, Text, View, FlatList, TextInput, KeyboardAvoidingView} from 'react-native'; 462 | + import {StyleSheet, Text, View, FlatList, TextInput, KeyboardAvoidingView, TouchableOpacity} from 'react-native'; 463 | ``` 464 | 465 | [TouchableOpacity](https://facebook.github.io/react-native/docs/touchableopacity.html), and its cousins TouchableHighlight, TouchableWithoutFeedback and TouchableNativeFeedback are the primitive components we can use to compose buttons and other elements with simple press interactions. 466 | 467 | Let's put that inside our footer `View`, on the next line immediately after the `` element: 468 | ```jsx 469 | 470 | Send 471 | 472 | ``` 473 | 474 | And of course, we'll style the button by adding a "send" style key to the StyleSheet: 475 | ```js 476 | send: { 477 | alignSelf: 'center', 478 | color: 'lightseagreen', 479 | fontSize: 16, 480 | fontWeight: 'bold', 481 | padding: 20, 482 | }, 483 | ``` 484 | 485 | ## Step 7: Add a header 486 | 487 | We now have a fully functioning chat app! In fact, you could go ahead and [publish it to the Expo store](#publish-your-app) right now. 488 | 489 | But it doesn't look very nice yet. Let's add a header component and a bit of color. 490 | 491 | We could just keep editing `App.js`, but the file is already getting quite big, and a header feels like a good, isolated component to split out to it's own file. 492 | 493 | Let's start by creating a new file, `Header.js` in our app's root directory. Copy the following component into that file. 494 | ```js 495 | import React from 'react'; 496 | import {View, Text, StyleSheet} from 'react-native'; 497 | 498 | export default class Header extends React.Component { 499 | render() { 500 | return ( 501 | 502 | 503 | #{this.props.title} 504 | 505 | 506 | ) 507 | } 508 | } 509 | 510 | const styles = StyleSheet.create({ 511 | header: { 512 | height: 80, 513 | backgroundColor: 'lightseagreen', 514 | alignItems: 'center', 515 | justifyContent: 'flex-end', 516 | padding: 10, 517 | }, 518 | title: { 519 | color: 'white', 520 | fontWeight: 'bold', 521 | fontSize: 24, 522 | }, 523 | }); 524 | ``` 525 | 526 | Those are the styles I used, but feel free to play around it with it and make it look like you! 527 | 528 | Because we `export` the Header component, it means we can `import` it in our main file. On top of the `App.js`, after the other import statements, add a relative import like so: 529 | 530 | ```js 531 | import Header from './Header'; 532 | ``` 533 | 534 | Then you can just drop in the Header component above the list and pass the channel name as the `title` prop. You should now see a Header on the screen! 535 | ```diff 536 | render() { 537 | return ( 538 | 539 | +
540 | 541 | ``` 542 | 543 | Speaking of [Props](https://facebook.github.io/react-native/docs/props.html), we briefly touched on them earlier, but this is the first time we are using them in our own components. 544 | 545 | In our `App` component we have been using `this.state`. You can think of [State](https://facebook.github.io/react-native/docs/state.html) as the private data that a component itself owns and manages. Contrast this with `this.props`, which are passed as attributes, can be accessed by the component, but a **component can never modify its own props**. Think of them like function arguments. 546 | 547 | ```js 548 | 549 | #{this.props.title} 550 | 551 | ``` 552 | 553 | _(Notice the `#{...}` expression? This is in fact not a React or JSX feature, it's just a hashtag followed by a regular curly brace `{}` expression and the hashtag is displayed on the screen 😁)_ 554 | 555 | #### Aside: Components, Components, Components 556 | 557 | If you squint a little, you'll see that this component's code looks a lot like our App component! If you haven't used React, it may surprise you that an entire app, and a small header component within it are equivalent concepts. 558 | 559 | That's the cool thing about React: It allows you to compose apps from smaller pieces, and even larger apps from smaller mini-apps, if you so wish! In a "real" app, you would probably split the chat UI into smaller, semantically named components. Instead of Texts, Views, TextInput, TouchableOpacities, StyleSheets etc, the main `App` component might looks like something like this! 560 | 561 | ```jsx 562 | 563 |
564 | 565 | 566 | 567 | ``` 568 | 569 | This makes our app code really easy to read and modify! 570 | 571 | 572 | ## Step 8: Customize device status bar (Optional) 573 | 574 | Depending on what colors you chose for you header, and what kind of device you are on, there's a chance the phone's status bar is not clearly visible on top of the header. Whether or not this is the case on your phone, it might be of some of your other users. To account for all possible devices, it's best practice to explicitly declare the status bar color for your app. 575 | 576 | We can do that easily in the `Header.js` file using the [StatusBar](https://facebook.github.io/react-native/docs/statusbar.html) component: 577 | ```diff 578 | -import {View, Text, StyleSheet} from 'react-native'; 579 | +import {View, Text, StyleSheet, StatusBar} from 'react-native'; 580 | ``` 581 | 582 | And dropping that into the our component's `render` method: 583 | ```diff 584 | 585 | + 586 | 587 | #{this.props.title} 588 | 589 | 590 | ``` 591 | 592 | The `backgroundColor` prop only affects Android devices - here we set it to the same color as our header. The `barStyle` works on both platforms, prop has one of three values: 593 | - "default" - the platform default style 594 | - "light-content" - light text, useful for dark backgrounds 595 | - "dark-content" - dark text, useful for light backgrounds 596 | 597 | (Some Android variants don't allow customizing the status bar, therefore this might have no effect.) 598 | 599 | ## Step 9: Implement avatars (Optional) 600 | 601 | It might nice to have avatars! In fact, the backend already support avatars, we just haven't been using them. 602 | 603 | First, find yourself a profile photo that's hosted somewhere online (for example your Twitter or Facebook profile image), copy the image URL and edit that into 604 | the hard-coded configuration section above the App component: 605 | ```diff 606 | const NAME = 'Your name'; 607 | const CHANNEL = 'Reactivate'; 608 | +const AVATAR = 'https://pbs.twimg.com/profile_images/806501058679816192/ZHFWIF-z_400x400.jpg'; 609 | ``` 610 | 611 | Then, in the `sendMessage` function add your avatar to the message payload: 612 | ```diff 613 | await send({ 614 | channel: CHANNEL, 615 | sender: NAME, 616 | + avatar: AVATAR, 617 | message, 618 | }); 619 | ``` 620 | 621 | That takes care of sending your avatar, but we still need to render the avatars next to each message. For that we can use the Image component: 622 | 623 | ```diff 624 | - import {StyleSheet, Text, View, FlatList, TextInput, KeyboardAvoidingView} from 'react-native'; 625 | + import {StyleSheet, Text, View, FlatList, TextInput, KeyboardAvoidingView, Image} from 'react-native'; 626 | ``` 627 | 628 | Remember that the `renderItem` method is responsible for rendering each message. We'll need to add the `Image` element, and add a `View` wrapper around the two Text elements so that we can lay them out nicely: 629 | ```diff 630 | renderItem({item}) { 631 | return ( 632 | 633 | + 634 | + 635 | {item.sender} 636 | {item.message} 637 | + 638 | 639 | ); 640 | } 641 | ``` 642 | 643 | We used the `styles.avatar` and `styles.rowText` styles, but we haven't declared them yet. Here's how I did it, but feel free to play with the styles yourself: 644 | ```js 645 | avatar: { 646 | borderRadius: 20, 647 | width: 40, 648 | height: 40, 649 | marginRight: 10, 650 | }, 651 | rowText: { 652 | flex: 1, 653 | }, 654 | ``` 655 | 656 | Unlike on the web, images loaded from the internet do not get automatically sized. They 657 | need to be either absolutely sized with width and height, as above, or rendered to fill a container with a flex style. This is because we won't know what the size of the image is before it is downloaded, and we don't want the layout to "jank" when the image arrives and changes the layout around it. 658 | 659 | Note that we are using the `borderRadius` prop to create rounded corners for the image. For fully round image, as above, use a `borderRadius` that is half the width and height or the image. For more gently rounded "Twitter-style" corners, try a lower radius. 660 | 661 | 662 | ## Step 10: Change channels (Optional) 663 | 664 | In a real app you'll probably want to change the channel. How to do this is left as an exercise to the reader, but here are some ideas how you might do it: 665 | 666 | - You can call the chat server `subscribe` function with a new channel name, and it will replace the existing subscription and start listening to messages from the new channel. 667 | - In the `send` function, detect if message is of format "/channel NewChannel", and instead of sending the message to the current chat, extract the channel name from the message. 668 | - To change the channel title in the header, move the channel name to App component state instead of using the hardcoded `CHANNEL`. 669 | 670 | ## Steps 11-99 671 | 672 | Now you have a basis for a simple app, but of course it has some limitations, such as a hard-coded username. Experiment with different React Native components (see: [Resources](#resources)) to add more functionality. Sky's the limit! 673 | 674 | # Publish your app! 675 | 676 | Because we've built the app on Expo, you can distribute the app via Expo's `exp` CLI. Let's install that globally on your machine and sign up: 677 | ```sh 678 | npm install -g exp 679 | exp register 680 | ``` 681 | 682 | After filling in your name and email address, you should now have an Expo account. Before we go ahead and publish the app, open your `package.json` and make sure the "name" field is set to something sensible - this will be the display name of your app in the Expo catalogue. Note that it needs to be alphanumeric, and should not contain any spaces. 683 | 684 | If you don't want your app to be publically visible to other Expo users, you can also set a `"privacy": "unlisted",` field in `package.json`. 685 | 686 | Now, all is left to publish the app: 687 | ```sh 688 | exp publish 689 | ``` 690 | 691 | If all went well, you should now have a link you can open on your phone and share with anyone (although given that we haven't implemented username selection, all users will appear as you! 😜) 692 | 693 | 694 | # Summary 695 | Building this small app, we've covered a lot of ground. We... 696 | - Learned how to create a new app with Create React Native App (CRNA) 697 | - Learned how to set up a live-reloading development environment with Expo 698 | - Learned about the anatomy of a React Native module 699 | - Learned about native primitives like Views, Texts, Images and more 700 | - Learned how to style and layout our components with the CSS-like Flexbox implementation 701 | - Learned how to gather user input with TextInput 702 | - Learned how to work with the device keyboard with KeyboardAvoidingView 703 | - Learned how to use async/await to perform asynchronous API calls 704 | - Learned about the power of third-party Components and how to use them in your app 705 | - Learned how to split your app into multiple components 706 | - Learned how to use component State and Props 707 | - Learned how to publish an app to the Expo store 708 | 709 | Of course, we didn't learn them very deeply. You now have an idea on how to build a simple app in React Native, but the learning only starts here! 710 | 711 | # Resources 712 | Useful resources: 713 | - [React Native docs](https://facebook.github.io/react-native/) 714 | - [Built-in components](https://facebook.github.io/react-native/docs/components-and-apis.html) 715 | - [React Native Express](http://www.reactnativeexpress.com/) - A great guide for experienced JavaScripp developers 716 | - [React (Native) Parts](https://react.parts/native) - React Native components from NPM 717 | - [React docs](https://facebook.github.io/react/docs/hello-world.html) 718 | - [Expo docs](https://docs.expo.io/versions/v17.0.0/index.html) 719 | - [Awesome React Native](https://github.com/jondot/awesome-react-native) - More resources than you will ever have time to read! 720 | --------------------------------------------------------------------------------