diff --git a/React.podspec b/React.podspec index 3b5d2118167..7d8c3405b63 100644 --- a/React.podspec +++ b/React.podspec @@ -4,7 +4,7 @@ package = JSON.parse(File.read(File.join(__dir__, "package.json"))) Pod::Spec.new do |s| s.name = "React" - s.version = "0.42.0-rc.1" + s.version = "0.42.0-rc.2" s.summary = package["description"] s.description = <<-DESC React Native apps are built using the React JS diff --git a/ReactAndroid/gradle.properties b/ReactAndroid/gradle.properties index 95e5d197337..d52cd524795 100644 --- a/ReactAndroid/gradle.properties +++ b/ReactAndroid/gradle.properties @@ -1,4 +1,4 @@ -VERSION_NAME=0.42.0-rc.1 +VERSION_NAME=0.42.0-rc.2 GROUP=com.facebook.react POM_NAME=ReactNative diff --git a/local-cli/generator/templates.js b/local-cli/generator/templates.js new file mode 100644 index 00000000000..dfbf598e255 --- /dev/null +++ b/local-cli/generator/templates.js @@ -0,0 +1,112 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +const copyProjectTemplateAndReplace = require('./copyProjectTemplateAndReplace'); +const execSync = require('child_process').execSync; +const fs = require('fs'); +const path = require('path'); + +const availableTemplates = { + navigation: 'HelloNavigation', +}; + +function listTemplatesAndExit(newProjectName, options) { + if (options.template === true) { + // Just listing templates using 'react-native init --template'. + // Not creating a new app. + // Print available templates and exit. + const templateKeys = Object.keys(availableTemplates); + if (templateKeys.length === 0) { + // Just a guard, should never happen as long availableTemplates + // above is defined correctly :) + console.log( + 'There are no templates available besides ' + + 'the default "Hello World" one.' + ); + } else { + console.log( + 'The available templates are:\n' + + templateKeys.join('\n') + + '\nYou can use these to create an app based on a template, for example: ' + + 'you could run: ' + + 'react-native init ' + newProjectName + ' --template ' + templateKeys[0] + ); + } + // Exit 'react-native init' + return true; + } + // Continue 'react-native init' + return false; +} + +/** + * @param newProjectName For example 'AwesomeApp'. + * @param templateKey Template to use, for example 'navigation'. + * @param yarnVersion Version of yarn available on the system, or null if + * yarn is not available. For example '0.18.1'. + */ +function createProjectFromTemplate(destPath, newProjectName, templateKey, yarnVersion) { + // Expand the basic 'HelloWorld' template + copyProjectTemplateAndReplace( + path.resolve('node_modules', 'react-native', 'local-cli', 'templates', 'HelloWorld'), + destPath, + newProjectName + ); + + if (templateKey !== undefined) { + // Keep the files from the 'HelloWorld' template, and overwrite some of them + // with the specified project template. + // The 'HelloWorld' template contains the native files (these are used by + // all templates) and every other template only contains additional JS code. + // Reason: + // This way we don't have to duplicate the native files in every template. + // If we duplicated them we'd make RN larger and risk that people would + // forget to maintain all the copies so they would go out of sync. + const templateName = availableTemplates[templateKey]; + if (templateName) { + copyProjectTemplateAndReplace( + path.resolve( + 'node_modules', 'react-native', 'local-cli', 'templates', templateName + ), + destPath, + newProjectName + ); + } else { + throw new Error('Uknown template: ' + templateKey); + } + + // Add dependencies: + + // dependencies.json is a special file that lists additional dependencies + // that are required by this template + const dependenciesJsonPath = path.resolve( + 'node_modules', 'react-native', 'local-cli', 'templates', templateName, 'dependencies.json' + ); + if (fs.existsSync(dependenciesJsonPath)) { + console.log('Adding dependencies for the project...'); + const dependencies = JSON.parse(fs.readFileSync(dependenciesJsonPath)); + for (let depName in dependencies) { + const depVersion = dependencies[depName]; + const depToInstall = depName + '@' + depVersion; + console.log('Adding ' + depToInstall + '...'); + if (yarnVersion) { + execSync(`yarn add ${depToInstall}`, {stdio: 'inherit'}); + } else { + execSync(`npm install ${depToInstall} --save --save-exact`, {stdio: 'inherit'}); + } + } + } + } +} + +module.exports = { + listTemplatesAndExit, + createProjectFromTemplate, +}; diff --git a/local-cli/init/init.js b/local-cli/init/init.js index 15f9837dfe8..3537a97ba68 100644 --- a/local-cli/init/init.js +++ b/local-cli/init/init.js @@ -8,7 +8,10 @@ */ 'use strict'; -const copyProjectTemplateAndReplace = require('../generator/copyProjectTemplateAndReplace'); +const { + listTemplatesAndExit, + createProjectFromTemplate, +} = require('../generator/templates'); const execSync = require('child_process').execSync; const fs = require('fs'); const minimist = require('minimist'); @@ -23,15 +26,15 @@ const yarn = require('../util/yarn'); * @param projectDir Templates will be copied here. * @param argsOrName Project name or full list of custom arguments * for the generator. + * @param options Command line options passed from the react-native-cli directly. + * E.g. `{ version: '0.43.0', template: 'navigation' }` */ function init(projectDir, argsOrName) { - console.log('Setting up new React Native app in ' + projectDir); - const args = Array.isArray(argsOrName) ? argsOrName // argsOrName was e.g. ['AwesomeApp', '--verbose'] : [argsOrName].concat(process.argv.slice(4)); // argsOrName was e.g. 'AwesomeApp' - // args array is e.g. ['AwesomeApp', '--verbose'] + // args array is e.g. ['AwesomeApp', '--verbose', '--template', 'navigation'] if (!args || args.length === 0) { console.error('react-native init requires a project name.'); return; @@ -40,7 +43,14 @@ function init(projectDir, argsOrName) { const newProjectName = args[0]; const options = minimist(args); - generateProject(projectDir, newProjectName, options); + if (listTemplatesAndExit(newProjectName, options)) { + // Just listing templates using 'react-native init --template' + // Not creating a new app. + return; + } else { + console.log('Setting up new React Native app in ' + projectDir); + generateProject(projectDir, newProjectName, options); + } } /** @@ -67,11 +77,7 @@ function generateProject(destinationRoot, newProjectName, options) { yarn.getYarnVersionIfAvailable() && yarn.isGlobalCliUsingYarn(destinationRoot); - copyProjectTemplateAndReplace( - path.resolve('node_modules', 'react-native', 'local-cli', 'templates', 'HelloWorld'), - destinationRoot, - newProjectName - ); + createProjectFromTemplate(destinationRoot, newProjectName, options.template, yarnVersion); if (yarnVersion) { console.log('Adding React...'); diff --git a/local-cli/runAndroid/runAndroid.js b/local-cli/runAndroid/runAndroid.js index 2adf2386e0b..815c8ebaa74 100644 --- a/local-cli/runAndroid/runAndroid.js +++ b/local-cli/runAndroid/runAndroid.js @@ -193,7 +193,7 @@ function runOnAllDevices(args, cmd, packageName, adbPath){ } console.log(chalk.bold( - `Building and installing the app on the device (cd android && ${cmd} ${gradleArgs.join(' ')}...` + `Building and installing the app on the device (cd android && ${cmd} ${gradleArgs.join(' ')})...` )); child_process.execFileSync(cmd, gradleArgs, { diff --git a/local-cli/templates/HelloNavigation/README.md b/local-cli/templates/HelloNavigation/README.md new file mode 100644 index 00000000000..110a6050469 --- /dev/null +++ b/local-cli/templates/HelloNavigation/README.md @@ -0,0 +1,41 @@ +# App template for new React Native apps + +This is a simple React Native app template which demonstrates a few basics concepts such as navigation between a few screens, ListViews, and handling text input. + +Android Example + +iOS Example + +## Purpose + +The idea is to make it easier for people to get started with React Native. Currently `react-native init` creates a very simple app that contains one screen with static text. Everyone new to React Native then needs to figure out how to do very basic things such as: +- Rendering a list of items fetched from a server +- Navigating between screens +- Handling text input and the software keyboard + +This app serves as a template used by `react-native init` so it is easier for anyone to get up and running quickly by having an app with a few screens and a ListView ready to go. + +### Best practices + +Another purpose of this app is to define best practices such as the folder structure of a standalone React Native app and naming conventions. + +## Not using Redux + +This template intentionally doesn't use Redux. After discussing with a few people who have experience using Redux we concluded that adding Redux to this app targeted at beginners would make the code more confusing, and wouldn't clearly show the benefits of Redux (because the app is too small). There are already a few concepts to grasp - the React component lifecycle, rendeing lists, using async / await, handling the software keyboard. We thought that's the maximum amount of things to learn at once. It's better for everyone to see patterns in their codebase as the app grows and decide for themselves whether and when they need Redux. See also the post [You Might Not Need Redux](https://medium.com/@dan_abramov/you-might-not-need-redux-be46360cf367#.f3q7kq4b3) by [Dan Abramov](https://twitter.com/dan_abramov). + +## Not using Flow (for now) + +Many people are new to React Native, some are new to ES6 and most people will be new to Flow. Therefore we didn't want to introduce all these concepts all at once in a single codebase. However, it might make sense to later introduce a separate version of this template that uses Flow annotations. + +## Provide feedback + +We need your feedback. Do you have a lot of experience building React Native apps? If so, please carefully read the code of the template and if you think something should be done differently, use issues in the repo [mkonicek/AppTemplateFeedback](https://github.com/mkonicek/AppTemplateFeedback) to discuss what should be done differently. + +## How to use the template + +``` +$ react-native init MyApp --version 0.42.0-rc.2 --template navigation +$ cd MyApp +$ react-native run-android +$ react-native run-ios +``` diff --git a/local-cli/templates/HelloNavigation/components/KeyboardSpacer.js b/local-cli/templates/HelloNavigation/components/KeyboardSpacer.js index e728cad33be..f720d65c15e 100644 --- a/local-cli/templates/HelloNavigation/components/KeyboardSpacer.js +++ b/local-cli/templates/HelloNavigation/components/KeyboardSpacer.js @@ -1,3 +1,5 @@ +'use strict'; + /* @flow */ import React, { PropTypes, Component } from 'react'; @@ -10,7 +12,7 @@ import { } from 'react-native'; type Props = { - offset?: number; + offset?: number, } type State = { @@ -41,7 +43,7 @@ type State = { */ const KeyboardSpacer = () => ( Platform.OS === 'ios' ? : null -) +); class KeyboardSpacerIOS extends Component { static propTypes = { diff --git a/local-cli/templates/HelloNavigation/components/ListItem.js b/local-cli/templates/HelloNavigation/components/ListItem.js index a51db840f7e..a3a99b0d2c1 100644 --- a/local-cli/templates/HelloNavigation/components/ListItem.js +++ b/local-cli/templates/HelloNavigation/components/ListItem.js @@ -1,3 +1,5 @@ +'use strict'; + import React, { Component } from 'react'; import { Platform, @@ -21,7 +23,7 @@ const Touchable = ({onPress, children}) => { ); } else { return ( - + {child} ); diff --git a/local-cli/templates/HelloNavigation/dependencies.json b/local-cli/templates/HelloNavigation/dependencies.json new file mode 100644 index 00000000000..130c08ed81f --- /dev/null +++ b/local-cli/templates/HelloNavigation/dependencies.json @@ -0,0 +1,3 @@ +{ + "react-navigation": "1.0.0-beta.1" +} diff --git a/local-cli/templates/HelloNavigation/index.android.js b/local-cli/templates/HelloNavigation/index.android.js index b122bdc5dcf..e9ea66bf64b 100644 --- a/local-cli/templates/HelloNavigation/index.android.js +++ b/local-cli/templates/HelloNavigation/index.android.js @@ -2,4 +2,4 @@ import { AppRegistry } from 'react-native'; import MainNavigator from './views/MainNavigator'; -AppRegistry.registerComponent('ChatExample', () => MainNavigator); +AppRegistry.registerComponent('HelloWorld', () => MainNavigator); diff --git a/local-cli/templates/HelloNavigation/index.ios.js b/local-cli/templates/HelloNavigation/index.ios.js index b122bdc5dcf..e9ea66bf64b 100644 --- a/local-cli/templates/HelloNavigation/index.ios.js +++ b/local-cli/templates/HelloNavigation/index.ios.js @@ -2,4 +2,4 @@ import { AppRegistry } from 'react-native'; import MainNavigator from './views/MainNavigator'; -AppRegistry.registerComponent('ChatExample', () => MainNavigator); +AppRegistry.registerComponent('HelloWorld', () => MainNavigator); diff --git a/local-cli/templates/HelloNavigation/lib/Backend.js b/local-cli/templates/HelloNavigation/lib/Backend.js new file mode 100644 index 00000000000..22cde143b63 --- /dev/null +++ b/local-cli/templates/HelloNavigation/lib/Backend.js @@ -0,0 +1,117 @@ +'use strict'; + +// This file just a dummy example of a HTTP API to talk to the backend. +// The state of the "database" that would normally live on the server +// is simply held here in memory. + +const backendStateForLoggedInPerson = { + chats: [ + { + name: 'Claire', + messages: [ + { + name: 'Claire', + text: 'I ❤️ React Native!', + }, + ], + }, + { + name: 'John', + messages: [ + { + name: 'John', + text: 'I ❤️ React Native!', + }, + ], + } + ], +}; + +/** + * Randomly simulate network failures. + * It is useful to enable this during development to make sure our app works + * in real-world conditions. + */ +function isNetworkFailure() { + const chanceOfFailure = 0; // 0..1 + return Math.random() < chanceOfFailure; +} + +/** + * Helper for the other functions in this file. + * Simulates a short delay and then returns a provided value or failure. + * This is just a dummy example. Normally we'd make a HTTP request, + * see http://facebook.github.io/react-native/docs/network.html + */ +function _makeSimulatedNetworkRequest(getValue) { + const durationMs = 400; + return new Promise(function (resolve, reject) { + setTimeout(function () { + if (isNetworkFailure()) { + reject(new Error('Network failure')); + } else { + getValue(resolve, reject); + } + }, durationMs); + }); +} + +/** + * Fetch a list of all chats for the logged in person. + */ +async function fetchChatList() { + return _makeSimulatedNetworkRequest((resolve, reject) => { + resolve(backendStateForLoggedInPerson.chats.map(chat => chat.name)); + }); +} + +/** + * Fetch a single chat. + */ +async function fetchChat(name) { + return _makeSimulatedNetworkRequest((resolve, reject) => { + resolve( + backendStateForLoggedInPerson.chats.find( + chat => chat.name === name + ) + ); + }); +} + +/** + * Send given message to given person. + */ +async function sendMessage({name, message}) { + return _makeSimulatedNetworkRequest((resolve, reject) => { + const chatForName = backendStateForLoggedInPerson.chats.find( + chat => chat.name === name + ); + if (chatForName) { + chatForName.messages.push({ + name: 'Me', + text: message, + }); + resolve(); + } else { + reject(new Error('Uknown person: ' + name)); + } + }); +} + +const Backend = { + fetchChatList, + fetchChat, + sendMessage, +}; + +export default Backend; + +// In case you are looking into using Redux for state management, +// this is how network requests are done in the f8 app which uses Redux: +// - To load some data, a Component fires a Redux action, such as loadSession() +// - That action makes the HTTP requests and then dispatches a redux action +// {type: 'LOADED_SESSIONS', results} +// - Then all reducers get called and one of them updates a part of the application +// state by storing the results +// - Redux re-renders the connected Components +// See https://github.com/fbsamples/f8app/search?utf8=%E2%9C%93&q=loaded_sessions diff --git a/local-cli/templates/HelloNavigation/views/HomeScreenTabNavigator.js b/local-cli/templates/HelloNavigation/views/HomeScreenTabNavigator.js index 123ee3a78a1..7ccfba46556 100644 --- a/local-cli/templates/HelloNavigation/views/HomeScreenTabNavigator.js +++ b/local-cli/templates/HelloNavigation/views/HomeScreenTabNavigator.js @@ -1,24 +1,20 @@ -import React, { Component } from 'react'; -import { - ListView, - Platform, - Text, -} from 'react-native'; +'use strict'; + import { TabNavigator } from 'react-navigation'; import ChatListScreen from './chat/ChatListScreen'; -import FriendListScreen from './friends/FriendListScreen'; +import WelcomeScreen from './welcome/WelcomeScreen'; /** * Screen with tabs shown on app startup. */ const HomeScreenTabNavigator = TabNavigator({ + Welcome: { + screen: WelcomeScreen, + }, Chats: { screen: ChatListScreen, }, - Friends: { - screen: FriendListScreen, - }, }); export default HomeScreenTabNavigator; diff --git a/local-cli/templates/HelloNavigation/views/MainNavigator.js b/local-cli/templates/HelloNavigation/views/MainNavigator.js index 20a5fb11b9b..949fb08a972 100644 --- a/local-cli/templates/HelloNavigation/views/MainNavigator.js +++ b/local-cli/templates/HelloNavigation/views/MainNavigator.js @@ -1,3 +1,5 @@ +'use strict'; + /** * This is an example React Native app demonstrates ListViews, text input and * navigation between a few screens. diff --git a/local-cli/templates/HelloNavigation/views/chat/ChatListScreen.js b/local-cli/templates/HelloNavigation/views/chat/ChatListScreen.js index a29fa1443af..c899f5dec74 100644 --- a/local-cli/templates/HelloNavigation/views/chat/ChatListScreen.js +++ b/local-cli/templates/HelloNavigation/views/chat/ChatListScreen.js @@ -1,11 +1,16 @@ +'use strict'; + import React, { Component } from 'react'; import { + ActivityIndicator, Image, ListView, Platform, StyleSheet, + View, } from 'react-native'; import ListItem from '../../components/ListItem'; +import Backend from '../../lib/Backend'; export default class ChatListScreen extends Component { @@ -29,28 +34,47 @@ export default class ChatListScreen extends Component { super(props); const ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2}); this.state = { - dataSource: ds.cloneWithRows([ - 'Claire', 'John' - ]) + isLoading: true, + dataSource: ds, }; } + async componentDidMount() { + const chatList = await Backend.fetchChatList(); + this.setState((prevState) => ({ + dataSource: prevState.dataSource.cloneWithRows(chatList), + isLoading: false, + })); + } + // Binding the function so it can be passed to ListView below - // and 'this' works properly inside _renderRow - _renderRow = (name) => { + // and 'this' works properly inside renderRow + renderRow = (name) => { return ( this.props.navigation.navigate('Chat', {name: name})} + onPress={() => { + // Start fetching in parallel with animating + this.props.navigation.navigate('Chat', { + name: name, + }); + }} /> - ) + ); } render() { + if (this.state.isLoading) { + return ( + + + + ); + } return ( ); @@ -58,6 +82,11 @@ export default class ChatListScreen extends Component { } const styles = StyleSheet.create({ + loadingScreen: { + backgroundColor: 'white', + paddingTop: 8, + flex: 1, + }, listView: { backgroundColor: 'white', }, diff --git a/local-cli/templates/HelloNavigation/views/chat/ChatScreen.js b/local-cli/templates/HelloNavigation/views/chat/ChatScreen.js index 8b732f9a0ac..04a9c62efd6 100644 --- a/local-cli/templates/HelloNavigation/views/chat/ChatScreen.js +++ b/local-cli/templates/HelloNavigation/views/chat/ChatScreen.js @@ -1,14 +1,17 @@ +'use strict'; + import React, { Component } from 'react'; import { + ActivityIndicator, Button, ListView, - Platform, StyleSheet, Text, TextInput, View, } from 'react-native'; import KeyboardSpacer from '../../components/KeyboardSpacer'; +import Backend from '../../lib/Backend'; export default class ChatScreen extends Component { @@ -19,23 +22,58 @@ export default class ChatScreen extends Component { constructor(props) { super(props); const ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2}); - const messages = [ - { - name: props.navigation.state.params.name, - name: 'Claire', - text: 'I ❤️ React Native!', - }, - ]; this.state = { - messages: messages, - dataSource: ds.cloneWithRows(messages), + messages: [], + dataSource: ds, myMessage: '', + isLoading: true, + }; + } + + async componentDidMount() { + let chat; + try { + chat = await Backend.fetchChat(this.props.navigation.state.params.name); + } catch (err) { + // Here we would handle the fact the request failed, e.g. + // set state to display "Messages could not be loaded". + // We should also check network connection first before making any + // network requests - maybe we're offline? See React Native's NetInfo + // module. + this.setState({ + isLoading: false, + }); + return; + } + this.setState((prevState) => ({ + messages: chat.messages, + dataSource: prevState.dataSource.cloneWithRows(chat.messages), + isLoading: false, + })); + } + + onAddMessage = async () => { + // Optimistically update the UI + this.addMessageLocal(); + // Send the request + try { + await Backend.sendMessage({ + name: this.props.navigation.state.params.name, + // TODO Is reading state like this outside of setState OK? + // Can it contain a stale value? + message: this.state.myMessage, + }); + } catch (err) { + // Here we would handle the request failure, e.g. call setState + // to display a visual hint showing the message could not be sent. } } - addMessage = () => { + addMessageLocal = () => { this.setState((prevState) => { - if (!prevState.myMessage) return prevState; + if (!prevState.myMessage) { + return prevState; + } const messages = [ ...prevState.messages, { name: 'Me', @@ -48,10 +86,10 @@ export default class ChatScreen extends Component { myMessage: '', } }); - this.refs.textInput.clear(); + this.textInput.clear(); } - myMessageChange = (event) => { + onMyMessageChange = (event) => { this.setState({myMessage: event.nativeEvent.text}); } @@ -63,10 +101,16 @@ export default class ChatScreen extends Component { ) render() { + if (this.state.isLoading) { + return ( + + + + ); + } return ( { this.textInput = textInput; }} style={styles.textInput} - placeholder='Type a message...' + placeholder="Type a message..." text={this.state.myMessage} - onSubmitEditing={this.addMessage} - onChange={this.myMessageChange} + onSubmitEditing={this.onAddMessage} + onChange={this.onMyMessageChange} /> {this.state.myMessage !== '' && (