Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Rewriting a large hybrid app with React Native ...

Rewriting a large hybrid app with React Native (Chain React Conf 2017)

Rewriting a large hybrid app with React Native (Chain React Conf 2017)

Javier Cuevas
(https://twitter.com/javier_dev

Keynote version with nice animations and cool gifs: https://www.icloud.com/keynote/0SB6eU9zN3_-_ZG1zLQ4g400Q#Chain_React_Conf

Javier Cuevas

July 10, 2017
Tweet

More Decks by Javier Cuevas

Other Decks in Programming

Transcript

  1. • 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
  2. 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 !
  3. Why did we abandon
 Hybrid? Corollary: any argument about frameworks

    or languages can be unfairly won by using a Google Trends chart.
  4. Dog Boarding (Date Range) Doggie Daycare (Multiple non consecutive dates)

    github.com/gudog/react-native-modal-datepicker
  5. “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
  6. 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 $
  7. 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
  8. 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
  9. 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
  10. “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
  11. “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
  12. '

  13. // 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
  14. // 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
  15. // 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
  16. // 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
  17. // 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
  18. // 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
  19. redux-thunk // configureStore.js import { createStore } from "redux"; import

    rootReducer from "./reducers"; export default function configureStore() { const store = createStore(rootReducer); return store; }
  20. 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; }
  21. // 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
  22. // 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
  23. "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...
  24. redux-saga // configureStore.js import { createStore } from "redux"; import

    rootReducer from "./reducers"; export default function configureStore() { const store = createStore(rootReducer); return store; }
  25. 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; }
  26. 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) ]; }
  27. App !"" Components !"" Config !"" Containers !"" Fixtures !""

    I18n !"" Images !"" Lib !"" Navigation !"" Redux !"" Sagas !"" Services !"" Themes %"" Transforms
  28. 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
  29. [ { "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 // { // ... // } ]
  30. 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 });
  31. [ { "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 } } }
  32. 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) ]; }
  33. 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) ]; }
  34. entities reducer // reducers/index.js import { combineReducers } from "redux";

    import dogsReducer from "./dogs"; const rootReducer = combineReducers({ dogs: dogsReducer }); export default rootReducer;
  35. 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;
  36. 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" } );
  37. // Navigation/OwnerNavigator.js import { TabNavigator } from "react-navigation"; import TabIcon

    from "./TabIcon"; // ... import screens const OwnerNavigator = TabNavigator({ Search: { screen: Search // navigationOptions { .. } }, Bookings: { screen: props => <Bookings {...props} role="user" />, navigationOptions: { tabBarLabel:"Bookings", tabBarIcon: ({ tintColor }) => <TabIcon iconName={"ios-notifications-outline"} color={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 => <Bookings {...props} role="sitter" />, navigationOptions: { tabBarLabel: "Bookings", tabBarIcon: ({ tintColor }) => <TabIcon iconName={"ios-notifications-outline"} color={tintColor} /> } }, Messages: { screen: Messages // navigationOptions { ... } }, AvailabilityCalendar: { screen: AvailabilityCalendar // navigationOptions { ... } }, Dogs: { screen: Dogs // navigationOptions { ... } }, Account: { screen: Account // navigationOptions { ... } } }); export default SitterNavigator;
  38. 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 ...
  39. 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 ...