Slide 1

Slide 1 text

Rewriting a large hybrid app with React Native Javier Cuevas @javier_dev

Slide 2

Slide 2 text

Javier Cuevas @javier_dev About me

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

About

Slide 5

Slide 5 text

• P2P marketplace for Dog Owners and Dog Sitters
 (tl;dr “Airbnb for dogs”). • Operating since 2012 in Spain, France, UK and Germany. • Launched our first mobile (hybrid) app in 2015 with Ionic Framework v1.2 (Cordova / Angular.js v1.4.3). • Now in the process of becoming a “mobile first” company. About

Slide 6

Slide 6 text

Why did we abandon
 Hybrid?

Slide 7

Slide 7 text

Why did we abandon
 Hybrid? • WebView Performance • Debugging pretty much imposible for Android < 4.3 • State management • Cordova / Phonegap ecosystem seems to be falling behind (libraries not very well maintained). • Upgrading to Ionic Framework v2 meant learning Angular 2 !

Slide 8

Slide 8 text

Why did we abandon
 Hybrid? Corollary: any argument about frameworks or languages can be unfairly won by using a Google Trends chart.

Slide 9

Slide 9 text

Adopting

Slide 10

Slide 10 text

It all started with a datepicker "

Slide 11

Slide 11 text

Date Range Multiple non consecutive dates github.com/gudog/react-native-modal-datepicker

Slide 12

Slide 12 text

Dog Boarding (Date Range) Doggie Daycare (Multiple non consecutive dates) github.com/gudog/react-native-modal-datepicker

Slide 13

Slide 13 text

Getting started with RN: A tale of decisions #

Slide 14

Slide 14 text

PROJECT STRUCTURE STATE MANAGEMENT NAVIGATION TESTING STYLING

Slide 15

Slide 15 text

Project Structure

Slide 16

Slide 16 text

“Since Redux is just a data store library, it has no direct opinion on how your project should be structured.” http://redux.js.org/docs/faq/CodeStructure.html

Slide 17

Slide 17 text

src !"" actions # !"" dog.js # %"" user.js !"" components # !"" Button.js # %"" Modal.js !"" containers # !"" Dog.js # %"" User.js !"" reducers # !"" dog.js # %"" user.js %"" types !"" dog.js %"" user.js Rails-style $

Slide 18

Slide 18 text

Rails-style $ src !"" actions # !"" DogActions.js # %"" UserActions.js !"" components # !"" Button.js # %"" Modal.js !"" containers # !"" Dog.js # %"" User.js !"" reducers # !"" DogReducer.js # %"" UserReducer.js %"" types !"" DogTypes.js %"" UserTypes.js

Slide 19

Slide 19 text

Rails-style $ src !"" actions # !"" DogActions.js # %"" UserActions.js !"" components # !"" Button.js # %"" Modal.js !"" containers # !"" Dog.js # %"" User.js !"" reducers # !"" DogReducer.js # %"" UserReducer.js !"" sagas # !"" DogSagas.js # %"" UserSagas.js !"" selectors # !"" DogSelectors.js # %"" UserSelectors.js !"" styles # !"" ButtonStyle.js # !"" DogStyle.js # %"" ModalStyle.js # %"" UserStyle.js %"" types !"" DogTypes.js %"" UserTypes.js

Slide 20

Slide 20 text

Domain-style % src !"" components # !"" Button # # !"" Button.js # # !"" index.js # # %"" styles.js # %"" Modal # !"" Modal.js # !"" index.js # %"" styles.js %"" containers !"" Dog # !"" Dog.js # !"" actions.js # !"" index.js # !"" reducer.js # !"" sagas.js # !"" selectors.js # !"" styles.js # %"" types.js %"" User !"" User.js !"" actions.js !"" index.js !"" reducer.js !"" sagas.js !"" selectors.js !"" styles.js %"" types.js

Slide 21

Slide 21 text

“Ducks” & src !"" components # !"" Button # # !"" Button.js # # !"" index.js # # %"" styles.js # %"" Modal # !"" Modal.js # !"" index.js # %"" styles.js %"" containers !"" Dog # !"" Dog.js # !"" index.js # !"" redux.js # !"" sagas.js # !"" selectors.js # %"" styles.js %"" User !"" User.js !"" index.js !"" redux.js !"" sagas.js !"" selectors.js %"" styles.js

Slide 22

Slide 22 text

“Ducks” & src !"" components # !"" Button # # !"" Button.js # # !"" index.js # # %"" styles.js # %"" Modal # !"" Modal.js # !"" index.js # %"" styles.js %"" containers !"" Dog # !"" Dog.js # !"" index.js # !"" redux.js # !"" sagas.js # !"" selectors.js # %"" styles.js %"" User !"" User.js !"" index.js !"" redux.js !"" sagas.js !"" selectors.js %"" styles.js src !"" components # !"" Button.js # %"" Modal.js !"" containers # !"" Dog.js # %"" User.js !"" redux # !"" DogRedux.js # %"" UserRedux.js !"" sagas # !"" DogSaga.js # %"" UserSaga.js !"" selectors # !"" DogSelectors.js # %"" UserSelectors.js %"" styles !"" DogStyle.js %"" UserStyle.js Domain-style + ducks Rails-style + ducks

Slide 23

Slide 23 text

https://twitter.com/dan_abramov/status/773912160896421889

Slide 24

Slide 24 text

'

Slide 25

Slide 25 text

twitter.com/javier_dev

Slide 26

Slide 26 text

Fetching Data /
 Side Effects

Slide 27

Slide 27 text

// actions/dogs.js import { DOGS_REQUEST, DOGS_SUCCESS, DOGS_FAILURE } from "./constants"; export function dogsRequest() { return { type: DOGS_REQUEST }; } export function dogsSuccess(data) { return { type: DOGS_SUCCESS, data }; } export function dogsFailure() { return { type: DOGS_FAILURE }; } Action Creators

Slide 28

Slide 28 text

Dogs Reducer

Slide 29

Slide 29 text

// reducers/dogs.js export default function dogsReducer( state = {}, action ) { switch (action.type) { case DOGS_REQUEST: return { ...state, fetching: true }; case DOGS_SUCCESS: return { ...state, fetching: false, dogs: action.dogs }; case DOGS_FAILURE: return { ...state, fetching: false, error: true }; } } Dogs Reducer

Slide 30

Slide 30 text

// reducers/index.js import { combineReducers } from "redux"; import dogsReducer from "./dogs"; const rootReducer = combineReducers({ dogs: dogsReducer }); export default rootReducer; Root Reducer // configureStore.js import { createStore } from "redux"; import rootReducer from "./reducers"; export default function configureStore() { const store = createStore(rootReducer); return store; } Configure Store

Slide 31

Slide 31 text

// services/api.js import apisauce from "apisauce"; const api = apisauce.create({ baseURL: "https://gudog.com/not-our-actual-API-(" }); const getDogs = () => api.get("/dogs"); export default { getDogs }; API Service

Slide 32

Slide 32 text

// services/api.js import apisauce from "apisauce"; import { camelizeKeys, decamelizeKeys } from "humps"; const api = apisauce.create({ baseURL: "https://gudog.com/not-our-actual-API-(" }); api.addResponseTransform(response => { response.data = camelizeKeys(response.data, { split: /(?=[A-Z0-9])/ }); }); api.addRequestTransform(request => { request.params = decamelizeKeys(request.params); request.data = decamelizeKeys(request.data, { split: /(?=[A-Z0-9])/ }); }); const getDogs = () => api.get("/dogs"); export default { getDogs }; API Service

Slide 33

Slide 33 text

// services/api.js import apisauce from "apisauce"; import { camelizeKeys, decamelizeKeys } from "humps"; const api = apisauce.create({ baseURL: "https://gudog.com/not-our-actual-API-(" }); api.addResponseTransform(response => { response.data = camelizeKeys(response.data, { split: /(?=[A-Z0-9])/ }); }); api.addRequestTransform(request => { request.params = decamelizeKeys(request.params); request.data = decamelizeKeys(request.data, { split: /(?=[A-Z0-9])/ }); }); const getDogs = () => api.get("/dogs"); const setToken = token => { api.setHeader("Authorization", `Bearer ${token}`); }; export default { getDogs, setToken }; API Service

Slide 34

Slide 34 text

redux-thunk

Slide 35

Slide 35 text

redux-thunk // configureStore.js import { createStore } from "redux"; import rootReducer from "./reducers"; export default function configureStore() { const store = createStore(rootReducer); return store; }

Slide 36

Slide 36 text

redux-thunk // configureStore.js import { createStore, applyMiddleware } from "redux"; import thunk from "redux-thunk"; import rootReducer from "./reducers"; export default function configureStore() { const store = createStore(rootReducer, applyMiddleware(thunk)); return store; }

Slide 37

Slide 37 text

// actions/dogs.js import { DOGS_REQUEST, DOGS_SUCCESS, DOGS_FAILURE } from "./constants"; export function dogsRequest() { return { type: DOGS_REQUEST }; } export function dogsSuccess(data) { return { type: DOGS_SUCCESS, data }; } export function dogsFailure() { return { type: DOGS_FAILURE }; } redux-thunk

Slide 38

Slide 38 text

// actions/dogs.js import { DOGS_REQUEST, DOGS_SUCCESS, DOGS_FAILURE } from "./constants"; import api from "../services/api"; export function dogsRequest() { return { type: DOGS_REQUEST }; } export function dogsSuccess(data) { return { type: DOGS_SUCCESS, data }; } export function dogsFailure() { return { type: DOGS_FAILURE }; } export function fetchDogs() { return dispatch => { dispatch(dogsRequest()); const response = api.getDogs(); if (response.ok) { dispatch(dogsSuccess(response.data)); } else { dispatch(dogsFailure()); } }; } redux-thunk

Slide 39

Slide 39 text

No content

Slide 40

Slide 40 text

redux-saga

Slide 41

Slide 41 text

"Like a separate thread in your application that's solely responsible for side effects." https://redux-saga.js.org/ redux-saga "A piece of code which runs in the background, watch for dispatched actions, may perform some async calls and can dispatch other actions." https://survivejs.com/blog/redux-saga-interview/ A saga is...

Slide 42

Slide 42 text

redux-saga // configureStore.js import { createStore } from "redux"; import rootReducer from "./reducers"; export default function configureStore() { const store = createStore(rootReducer); return store; }

Slide 43

Slide 43 text

redux-saga // configureStore.js import { createStore, applyMiddleware } from "redux"; import createSagaMiddleware from "redux-saga"; import rootReducer from "./reducers"; import rootSaga from "/sagas"; const sagaMiddleware = createSagaMiddleware(); export default function configureStore() { const store = createStore(rootReducer, applyMiddleware(sagaMiddleware)); sagaMiddleware.run(rootSaga); return store; }

Slide 44

Slide 44 text

redux-saga // sagas/index.js import { call, put, takeEvery } from "redux-saga/effects"; import api from "../services/api"; import { dogsSuccess, dogsFailure } from “../actions/dogs” import { DOGS_REQUEST, DOGS_SUCCESS, DOGS_FAILURE } from “../actions/constants”; export function* getDogs(api, action) { const response = yield call(api.getDogs); if (response.ok) { yield put(dogsSuccess(response.data)); } else { yield put(dogsFailure()); } } export default function* root() { yield [ takeEvery(DOGS_REQUEST, getDogs) // takeEvery(DELETE_DOG_REQUEST, deleteDog) ]; }

Slide 45

Slide 45 text

https://medium.com/react-native-training/redux-4-ways-95a130da0cdc

Slide 46

Slide 46 text

https://twitter.com/dan_abramov/status/831110009970900996

Slide 47

Slide 47 text

No content

Slide 48

Slide 48 text

$ npm install -g ignite-cli $ ignite new AwesomeReactNativeApp

Slide 49

Slide 49 text

App !"" Components !"" Config !"" Containers !"" Fixtures !"" I18n !"" Images !"" Lib !"" Navigation !"" Redux !"" Sagas !"" Services !"" Themes %"" Transforms

Slide 50

Slide 50 text

App !"" Components !"" Config !"" Containers !"" Fixtures !"" I18n !"" Images !"" Lib !"" Navigation !"" Redux !"" Sagas !"" Services !"" Themes %"" Transforms $ ignite generate container Dog ✔ App/Containers/Dog.js ✔ App/Containers/Styles/DogStyle.js $ ignite generate redux Dog ✔ App/Redux/DogRedux.js $ ignite generate saga Dog ✔ App/Sagas/DogSagas.js $ ignite add maps $ ignite generate map DogsMap ✔ App/Components/DogsMap.js ✔ App/Components/Styles/DogsMapStyles.js ✔ App/Components/DogsMapCallout.js ✔ App/Components/Styles/DogsMapCalloutStyles.js

Slide 51

Slide 51 text

Handling relational & nested data

Slide 52

Slide 52 text

[ { "id": 392, "name": "Jasper", "observations": "Jasper is a real character and not your normal Jack Russell, preferring a laid back approach to life!", "age": 6, "gender": "male", "size": "small", "breed": { "id": 206, "name": "Jack Russell Terrier" }, "user": { "id": 107471, "type": "Caregiver", "name": "Anthony", "email": "[email protected]" }, } // more awesome dogs // { // ... // } ]

Slide 53

Slide 53 text

normalizr // schemas/index.js import { schema } from "normalizr"; export const UserSchema = new schema.Entity("users"); export const DogSchema = new schema.Entity("dogs"); export const BreedSchema = new schema.Entity("breeds"); UserSchema.define({ dogs: [DogSchema] }); DogSchema.define({ user: UserSchema, breed: BreedSchema });

Slide 54

Slide 54 text

[ { "id": 392, "name": "Jasper", "observations": "Jasper is a real character and not your normal Jack Russell, preferring a laid back approach to life!", "age": 6, "gender": "male", "size": "small", "breed": { "id": 206, "name": "Jack Russell Terrier" }, "user": { "id": 107471, "type": "Caregiver", "name": "Anthony", "email": "[email protected]" }, } // more awesome dogs // { // ... // } ] { "result": [ "392", ... ], "entities": { "dogs": { "392": { "id": 392, "name": "Jasper", "observations": "Jasper is a real character and not your normal Jack Russell, preferring a laid back approach to life!", "age": 6, "gender": "male", "size": "small", "breed": 206, "user": 107471 }, // more awesome dogs }, "users": { "107471": { "id": "107471", "type": "Caregiver", "name": "Anthony", "email": "[email protected]" }, // more users }, "breeds": { "206": { "id": "324", "name": "Jack Russell Terrier" }, // more breeds } } }

Slide 55

Slide 55 text

redux-saga + normalizr // sagas/index.js import { call, put, takeLatest } from "redux-saga/effects"; import api from "../services/api"; import { dogsSuccess, dogsFailure } from “../actions/dogs” import { DOGS_REQUEST, DOGS_SUCCESS, DOGS_FAILURE } from “../actions/constants”; export function* getDogs(api, action) { const response = yield call(api.getDogs); if (response.ok) { yield put(dogsSuccess(response.data)); } else { yield put(dogsFailure()); } } export default function* root() { yield [ takeLatest(DOGS_REQUEST, getDogs) ]; }

Slide 56

Slide 56 text

redux-saga + normalizr // sagas/index.js import { normalize } from 'normalizr' import { call, put, takeLatest } from "redux-saga/effects"; import api from "../services/api"; import { dogsSuccess, dogsFailure } from “../actions/dogs” import { DOGS_REQUEST, DOGS_SUCCESS, DOGS_FAILURE } from “../actions/constants”; import { DogSchema } from '../schemas' export function* getDogs(api, action) { const response = yield call(api.getDogs); if (response.ok) { const normalizedData = normalize(data, [DogSchema]) yield put(dogsSuccess(normalizedData)); } else { yield put(dogsFailure()); } } export default function* root() { yield [ takeLatest(DOGS_REQUEST, getDogs) ]; }

Slide 57

Slide 57 text

entities reducer // reducers/index.js import { combineReducers } from "redux"; import dogsReducer from "./dogs"; const rootReducer = combineReducers({ dogs: dogsReducer }); export default rootReducer;

Slide 58

Slide 58 text

entities reducer // reducers/index.js import { combineReducers } from "redux"; import merge from "lodash/merge"; import dogsReducer from "./dogs"; const entities = (state = {}, action) => { const { payload } = action; if (payload && payload.entities) { return merge({}, state, payload.entities); } return state; }; const rootReducer = combineReducers({ entities, dogs: dogsReducer }); export default rootReducer;

Slide 59

Slide 59 text

navigation⛵

Slide 60

Slide 60 text

navigation⛵ • aksonov/react-native-router-flux • react-community/react-navigation • ReactTraining/react-router • leolebras/react-router-navigation • wix/react-native-navigation • airbnb/native-navigation • .... *

Slide 61

Slide 61 text

react-navigation // Navigation/AppNavigator.js import { StackNavigator } from "react-navigation"; import Splash from "../Components/Splash"; import Onboarding from "../Containers/Onboarding"; import Login from "../Containers/Login"; import SignUp from "../Containers/SignUp"; import OwnerNavigator from "./OwnerNavigator"; import SitterNavigator from "./SitterNavigator"; import DogProfile from "../Containers/DogProfile"; // ... More imports of screens export default StackNavigator( { Splash: { screen: Splash }, Onboarding: { screen: Onboarding }, Login: { screen: Login }, SignUp: { screen: SignUp }, OwnerHome: { screen: OwnerNavigator }, SitterHome: { screen: SitterNavigator }, DogProfile: { screen: DogProfile } // ... More screens }, { initialRouteName: "Splash" } );

Slide 62

Slide 62 text

// Navigation/OwnerNavigator.js import { TabNavigator } from "react-navigation"; import TabIcon from "./TabIcon"; // ... import screens const OwnerNavigator = TabNavigator({ Search: { screen: Search // navigationOptions { .. } }, Bookings: { screen: props => , navigationOptions: { tabBarLabel:"Bookings", tabBarIcon: ({ tintColor }) => } }, Messages: { screen: Messages // navigationOptions { .. } }, Dogs: { screen: Dogs // navigationOptions { .. } }, Account: { screen: Account // navigationOptions { .. } } }); export default OwnerNavigator; // Navigation/SitterNavigator.js import { TabNavigator } from "react-navigation"; import TabIcon from "./TabIcon"; // ... import screens const SitterNavigator = TabNavigator({ Bookings: { screen: props => , navigationOptions: { tabBarLabel: "Bookings", tabBarIcon: ({ tintColor }) => } }, Messages: { screen: Messages // navigationOptions { ... } }, AvailabilityCalendar: { screen: AvailabilityCalendar // navigationOptions { ... } }, Dogs: { screen: Dogs // navigationOptions { ... } }, Account: { screen: Account // navigationOptions { ... } } }); export default SitterNavigator;

Slide 63

Slide 63 text

Project Structure Rails-style Domain-style Rails-style + Ducks Domain-style + Ducks Domain-style + Ducks * + Router-based ... Side Effects
 (Redux Middlewares) redux-thunk redux-saga + redux-observable redux-promise- middleware redux-loop redux-persist + redux-offline Managing Relational & Nested Data normalizr redux-orm Navigation react- navigation + react-router (v4) aksonov/react- native-router-flux wix/react-native- navigation airbnb/native- navigation react-native-ya- navigator ...

Slide 64

Slide 64 text

Project Structure Rails-style Domain-style Rails-style + Ducks Domain-style + Ducks Domain-style + Ducks * + Router-based ... Side Effects
 (Redux Middlewares) redux-thunk redux-saga + redux-observable redux-promise- middleware redux-loop redux-persist + redux-offline Managing Relational & Nested Data normalizr redux-orm Navigation react- navigation + react-router (v4) aksonov/react- native-router-flux wix/react-native- navigation airbnb/native- navigation react-native-ya- navigator ... Linting 
 & Code Style prettier prettier-eslint eslint-config-airbnb standard JS + Typechecking prop-types + Flow TypeScript Styling StyleSheet + styled-components glamorous fela react-native-css Testing jest + ava enzyme + Immutability & Functional seamless- immutable + ImmutableJS ramda + Boilerplates / CLI / Generators ignite + create-react-native- app expo snowflake pepperoni-app- kit kittenTricks UI Kits shoutem native-base react-native- elements react-native- material-design nachos-ui react-native-ui- kitten Miscellaneous react-native- maps + lottie socket.io react-native-gifted- chat react-native- i18n + react-native- vector-icons + react-native-code- push apisauce + reduxsauce + reactotron + reselect + tcomb-form- native ...

Slide 65

Slide 65 text

Is React Native productive?

Slide 66

Slide 66 text

No content

Slide 67

Slide 67 text

THANK YOU. Javier Cuevas @javier_dev