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

Realtime Mobile Apps with React Native and Elix...

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.
Avatar for Osa Gaius Osa Gaius
September 09, 2017

Realtime Mobile Apps with React Native and Elixir - ElixirConf 2017

Building realtime mobile apps presents several challenges. Most approaches default to complex polling and caching techniques, and/or use subpar toolchains. This talk argues that functional programming is the best way to build scalable mobile apps. The talk demonstrates how to quickly build a realtime cross-platform mobile app with React Native. In addition, it describes how to use Elixir and the Phoenix framework to drastically reduce the effort and complexity necessary to build a realtime web server. Lastly, the talk argues that this approach is the most maintainable for small to medium-sized development teams.

Avatar for Osa Gaius

Osa Gaius

September 09, 2017
Tweet

More Decks by Osa Gaius

Other Decks in Technology

Transcript

  1. Outline 1. Intro + Demo 2. Functional Programming 3. Reactive

    Programming 4. React Native 5. Redux 6. Elixir + Phoenix 7. Conclusion
  2. Choosing an Approach • “Do you even need real-time?” •

    Team • Complexity over time ◦ External dependencies ◦ Testing • Functional + Reactive
  3. Choosing an Approach • “Do you even need real-time?” •

    Team • Complexity over time ◦ External dependencies ◦ Testing • Functional + Reactive
  4. Pure Functions Impure function square(x) { updateXInDatabase(x); return x *

    x; } Pure function square(x) { // return the output return x * x; }
  5. Pure Functions contd. Impure var count = 0; function increaseCount(val)

    { count += val; } Pure var count = 0; function increaseCount(curentVal, val) { curentVal + val; } var count = increaseCount(count, 1);
  6. Javascript • Developer friendly • Tooling ◦ Write ◦ Build

    ◦ Test npm install -g react-native-cli react-native-cli init AwesomeProject react-native-cli run-ios
  7. Components contd. const Button = (props) => { return (

    <TouchableOpacity onPress={props.onPress}> <Text> I am Button </Text> </TouchableOpacity> ); }; export { Button }; Button
  8. Components contd. ... import { Button, Card, CardSection, Input, Spinner

    } from './common'; class LoginForm extends React.Component { onUserNameChange() { //handle username change } render() { return ( <Card> <CardSection> <Input onChangeText={this.onUserNameChange.bind(this)} /> </CardSection> {this.renderError()} <Button title=”Log In”> </Card> ); } } LoginForm
  9. Components contd. ... import { Button, Card, CardSection, Input, Spinner

    } from './common'; class LoginForm extends React.Component { onUserNameChange() { //handle username change } render() { return ( <Card> <CardSection> <Input onChangeText={this.onUserNameChange.bind(this)} /> </CardSection> {this.renderError()} {this.renderButton()} </Card> ); } } LoginForm
  10. Components contd. ... import { Button, Card, CardSection, Input, Spinner

    } from './common'; class LoginForm extends React.Component { onUserNameChange() { //handle username change } render() { return ( <Card> <CardSection> <Input onChangeText={this.onUserNameChange.bind(this)} /> </CardSection> {this.renderError()} {this.renderButton()} </Card> ); } } LoginForm
  11. Store import { Provider } from 'react-redux'; import ReduxThunk from

    'redux-thunk'; import { createStore, applyMiddleware } from 'redux'; import reducers from './reducers'; export default class App extends React.Component { render() { const store = createStore(reducers, {}, applyMiddleware(ReduxThunk)); return ( <Provider store={store}> ... </Provider> ); } }
  12. Store import { Provider } from 'react-redux'; import ReduxThunk from

    'redux-thunk'; import { createStore, applyMiddleware } from 'redux'; import reducers from './reducers'; export default class App extends React.Component { render() { const store = createStore(reducers, {}, applyMiddleware(ReduxThunk)); return ( <Provider store={store}> ... </Provider> ); } }
  13. Login Form revisited ... import { Button, Card, CardSection, Input,

    Spinner } from './common'; class LoginForm extends React.Component { onUserNameChange(text) { //handle username change } render() { return ( <Card> <CardSection> <Input value=? onChangeText={this.onUserNameChange.bind(this)} /> </CardSection> ... </Card> ); } }
  14. Login Form revisited ... import { Button, Card, CardSection, Input,

    Spinner } from './common'; class LoginForm extends React.Component { onUserNameChange(text) { //handle username change } render() { return ( <Card> <CardSection> <Input value=? onChangeText={this.onUserNameChange.bind(this)} /> </CardSection> ... </Card> ); } }
  15. Login Form revisited ... import { Button, Card, CardSection, Input,

    Spinner } from './common'; class LoginForm extends React.Component { onUserNameChange(text) { //handle username change } render() { return ( <Card> <CardSection> <Input value=? onChangeText={this.onUserNameChange.bind(this)} /> </CardSection> ... </Card> ); } }
  16. Login Form revisited ... import { Button, Card, CardSection, Input,

    Spinner } from './common'; class LoginForm extends React.Component { onUserNameChange(text) { … } render() { return ( <Card> <CardSection> <Input value=? onChangeText={this.onUserNameChange.bind(this)} /> </CardSection> ... </Card> ); } }
  17. Login Form revisited ... import { Button, Card, CardSection, Input,

    Spinner } from './common'; class LoginForm extends React.Component { onUserNameChange(text) { this.props.userNameChanged(text); } render() { return ( <Card> <CardSection> <Input value=? onChangeText={this.onUserNameChange.bind(this)} /> </CardSection> ... </Card> ); } }
  18. Login Form revisited import { userNameChanged } from '../actions'; class

    LoginForm extends React.Component { ... } const mapStateToProps = (state) => { const { userName, error, loading } = state.auth; return { userName, error, loading }; }; export default connect(mapStateToProps, { userNameChanged })(LoginForm);
  19. Login Form revisited import { userNameChanged } from '../actions'; class

    LoginForm extends React.Component { ... } const mapStateToProps = (state) => { const { userName, error, loading } = state.auth; return { userName, error, loading }; }; export default connect(mapStateToProps, { userNameChanged })(LoginForm);
  20. Login Form revisited import { userNameChanged } from '../actions'; class

    LoginForm extends React.Component { ... } const mapStateToProps = (state) => { const { userName, error, loading } = state.auth; return { userName, error, loading }; }; export default connect(mapStateToProps, { userNameChanged })(LoginForm);
  21. Login Form revisited import { userNameChanged } from '../actions'; class

    LoginForm extends React.Component { ... } const mapStateToProps = (state) => { const { userName, error, loading } = state.auth; return { userName, error, loading }; }; export default connect(mapStateToProps, { userNameChanged })(LoginForm);
  22. Login Form revisited import { userNameChanged } from '../actions'; class

    LoginForm extends React.Component { ... } const mapStateToProps = (state) => { const { userName, error, loading } = state.auth; return { userName, error, loading }; }; export default connect(mapStateToProps, { userNameChanged })(LoginForm);
  23. Actions export const userNameChanged = (text) => { return {

    type: “username_changed”, payload: text }; };
  24. Actions export const userNameChanged = (text) => { return {

    type: “username_changed”, payload: text }; };
  25. Reducers const INITIAL_STATE = { userName: '', error: '' };

    export default (state = INITIAL_STATE, action) => { switch (action.type) { case “username_changed”: return { ...state, userName: action.payload, error: '' }; default: return state; } };
  26. Login Form revisited ... import { Button, Card, CardSection, Input,

    Spinner } from './common'; class LoginForm extends React.Component { onUserNameChange(text) { //handle username change } render() { return ( <Card> <CardSection> <Input value=? onChangeText=... /> </CardSection> {this.renderError()} {this.renderButton()} </Card> ); } }
  27. Login Form revisited ... import { Button, Card, CardSection, Input,

    Spinner } from './common'; class LoginForm extends React.Component { onUserNameChange(text) { //handle username change } render() { return ( <Card> <CardSection> <Input value={this.props.userName} onChangeText=... /> </CardSection> {this.renderError()} {this.renderButton()} </Card> ); } }
  28. Login Form revisited import { userNameChanged } from '../actions'; class

    LoginForm extends React.Component { ... } const mapStateToProps = (state) => { const { userName } = state.auth; return { userName }; }; export default connect(mapStateToProps, { userNameChanged })(LoginForm);
  29. Login Form revisited import { userNameChanged } from '../actions'; class

    LoginForm extends React.Component { ... } const mapStateToProps = (state) => { const { userName } = state.auth; return { userName }; }; export default connect(mapStateToProps, { userNameChanged })(LoginForm);
  30. Login Form revisited import { userNameChanged } from '../actions'; class

    LoginForm extends React.Component { ... } const mapStateToProps = (state) => { const { userName } = state.auth; return { userName }; }; export default connect(mapStateToProps, { userNameChanged })(LoginForm);
  31. Channels contd. defmodule ChatServer.UserSocket do use Phoenix.Socket channel "rooms:*", PhoenixChat.RoomChannel

    transport :websocket, Phoenix.Transports.WebSocket def connect(%{"user" => user}, socket) do {:ok, assign(socket, :user, user)} end end
  32. Channels contd. defmodule ChatServer.UserSocket do use Phoenix.Socket channel "rooms:*", PhoenixChat.RoomChannel

    transport :websocket, Phoenix.Transports.WebSocket def connect(%{"user" => user}, socket) do {:ok, assign(socket, :user, user)} end end
  33. Channels contd. defmodule ChatServer.UserSocket do use Phoenix.Socket channel "rooms:*", PhoenixChat.RoomChannel

    transport :websocket, Phoenix.Transports.WebSocket def connect(%{"user" => user}, socket) do {:ok, assign(socket, :user, user)} end end
  34. Channels contd. defmodule ChatServer.UserSocket do use Phoenix.Socket channel "rooms:*", ChatServer.RoomChannel

    transport :websocket, Phoenix.Transports.WebSocket def connect(%{"user" => user}, socket) do {:ok, assign(socket, :user, user)} end end
  35. Login Form revisited import { joinSocket } from '../actions'; class

    LoginForm extends React.Component { onButtonPress() { this.props.joinSocket(); } render() { return ( <Card> <Button onPress={this.onButtonPress.bind(this)}> Log In </Button> </Card> ); } } export default connect(mapStateToProps, { userNameChanged, joinSocket })(LoginForm);
  36. Login Form revisited import { joinSocket } from '../actions'; class

    LoginForm extends React.Component { onButtonPress() { this.props.joinSocket(); } render() { return ( <Card> <Button onPress={this.onButtonPress.bind(this)}> Log In </Button> </Card> ); } } export default connect(mapStateToProps, { userNameChanged, joinSocket })(LoginForm);
  37. Login Form revisited import { joinSocket } from '../actions'; class

    LoginForm extends React.Component { onButtonPress() { this.props.joinSocket(); } render() { return ( <Card> <Button onPress={this.onButtonPress.bind(this)}> Log In </Button> </Card> ); } } export default connect(mapStateToProps, { userNameChanged, joinSocket })(LoginForm);
  38. Login Form revisited import { joinSocket } from '../actions'; class

    LoginForm extends React.Component { onButtonPress() { this.props.joinSocket(); } render() { return ( <Card> <Button onPress={this.onButtonPress.bind(this)}> Log In </Button> </Card> ); } } export default connect(mapStateToProps, { joinSocket })(LoginForm);
  39. Actions export const joinSocket = () => { return (dispatch)

    => { const socket = new Socket('http://localhost:4000/socket';); socket.connect(); socket.onOpen(() => { dispatch({ type: CONNECT_SUCCESS, payload: { socket } }); Actions.main(); }); }; };
  40. Actions export const joinSocket = () => { return (dispatch)

    => { const socket = new Socket('http://localhost:4000/socket';); socket.connect(); socket.onOpen(() => { dispatch({ type: CONNECT_SUCCESS, payload: { socket } }); Actions.main(); }); }; };
  41. Actions export const joinSocket = () => { return (dispatch)

    => { const socket = new Socket('http://localhost:4000/socket';); socket.connect(); socket.onOpen(() => { dispatch({ type: CONNECT_SUCCESS, payload: { socket } }); Actions.main(); }); }; };
  42. Actions export const joinSocket = () => { return (dispatch)

    => { const socket = new Socket('http://localhost:4000/socket';); socket.connect(); socket.onOpen(() => { dispatch({ type: CONNECT_SUCCESS, payload: { socket } }); Actions.main(); }); }; };
  43. Actions export const joinSocket = () => { return (dispatch)

    => { const socket = new Socket('http://localhost:4000/socket';); socket.connect(); socket.onOpen(() => { dispatch({ type: CONNECT_SUCCESS, payload: { socket } }); Actions.main(); }); }; };
  44. Reducers export default (state = INITIAL_STATE, action) => { switch

    (action.type) { case CONNECT_SUCCESS: return { ...state, socket: action.payload.socket }; default: return state; } };
  45. Actions export const joinSocket = () => { return (dispatch)

    => { const socket = new Socket('http://localhost:4000/socket';); socket.connect(); socket.onOpen(() => { dispatch({ type: CONNECT_SUCCESS, payload: { socket } }); Actions.main(); }); }; };
  46. The Lobby import { joinRoom } from '../actions'; class RoomList

    extends React.Component { componentWillMount() { const redirectToRoom = false; this.props.joinRoom( 'lobby', this.props.socket, this.props.userName, redirectToRoom ); this.createDataSource(this.props.rooms); } }
  47. The Lobby import { joinRoom } from '../actions'; class RoomList

    extends React.Component { componentWillMount() { const redirectToRoom = false; this.props.joinRoom( 'lobby', this.props.socket, this.props.userName, redirectToRoom ); this.createDataSource(this.props.rooms); } }
  48. The Lobby import { joinRoom } from '../actions'; class RoomList

    extends React.Component { componentWillMount() { const redirectToRoom = false; this.props.joinRoom( 'lobby', this.props.socket, this.props.userName, redirectToRoom ); this.createDataSource(this.props.rooms); } }
  49. The Lobby const mapStateToProps = (state) => { const {

    socket, userName } = state.auth; return { socket, userName }; }; export default connect(mapStateToProps, { joinRoom })(RoomList);
  50. The Lobby const mapStateToProps = (state) => { const {

    socket, userName } = state.auth; return { socket, userName }; }; export default connect(mapStateToProps, { joinRoom })(RoomList);
  51. The Lobby export const joinRoom = (roomName, socket, userName, redirect)

    => { return (dispatch) => { const channel = socket.channel(`room:${roomName}`, { userName }); channel.join() .receive('ok', () => { dispatch({ type: JOIN_ROOM_SUCCESS, payload: { channel, name } }); }) }; };
  52. The Lobby export const joinRoom = (roomName, socket, userName, redirect)

    => { return (dispatch) => { const channel = socket.channel(`room:${roomName}`, { userName }); channel.join() .receive('ok', () => { dispatch({ type: JOIN_ROOM_SUCCESS, payload: { channel, name } }); }) }; };
  53. The Lobby export const joinRoom = (roomName, socket, userName, redirect)

    => { return (dispatch) => { const channel = socket.channel(`room:${roomName}`, { userName }); channel.join() .receive('ok', () => { dispatch({ type: JOIN_ROOM_SUCCESS, payload: { channel, name } }); }) }; };
  54. The Lobby export const joinRoom = (roomName, socket, userName, redirect)

    => { return (dispatch) => { const channel = socket.channel(`room:${roomName}`, { userName }); channel.join() .receive('ok', () => { dispatch({ type: JOIN_ROOM_SUCCESS, payload: { channel, name } }); }) }; };
  55. The Lobby export const joinRoom = (roomName, socket, userName, redirect)

    => { return (dispatch) => { const channel = socket.channel(`room:${roomName}`, { userName }); channel.join() .receive('ok', () => { dispatch({ type: JOIN_ROOM_SUCCESS, payload: { channel, name } }); }) }; };
  56. The Lobby defmodule ChatServer.RoomChannel do use Phoenix.Channel def join("room:lobby", _params,

    socket) do send self(), :after_join {:ok, socket} end def handle_info(:after_join, socket) do broadcast socket, "rooms_list", %{value: get_rooms()} {:noreply, socket} end end
  57. The Lobby defmodule ChatServer.RoomChannel do use Phoenix.Channel def join("room:lobby", _params,

    socket) do send self(), :after_join {:ok, socket} end def handle_info(:after_join, socket) do broadcast socket, "rooms_list", %{value: get_rooms()} {:noreply, socket} end end
  58. The Lobby defmodule ChatServer.RoomChannel do use Phoenix.Channel def join("room:lobby", _params,

    socket) do send self(), :after_join {:ok, socket} end def handle_info(:after_join, socket) do broadcast socket, "rooms_list", %{value: get_rooms()} {:noreply, socket} end end
  59. The Lobby defmodule ChatServer.RoomChannel do use Phoenix.Channel def join("room:lobby", _params,

    socket) do send self(), :after_join {:ok, socket} end def handle_info(:after_join, socket) do broadcast socket, "rooms_list", %{value: get_rooms()} {:noreply, socket} end end
  60. The Lobby const setUpRoomsListHandler = (dispatch, channel) => { channel.on('rooms_list',

    payload => { const rooms = payload.value; dispatch({ type: NEW_ROOMS_LIST_RECEIVED, payload: rooms }); }); };
  61. The Lobby const setUpRoomsListHandler = (dispatch, channel) => { channel.on('rooms_list',

    payload => { const rooms = payload.value; dispatch({ type: NEW_ROOMS_LIST_RECEIVED, payload: rooms }); }); };
  62. The Lobby const setUpRoomsListHandler = (dispatch, channel) => { channel.on('rooms_list',

    payload => { const rooms = payload.value; dispatch({ type: NEW_ROOMS_LIST_RECEIVED, payload: rooms }); }); };
  63. The Lobby const setUpRoomsListHandler = (dispatch, channel) => { channel.on('rooms_list',

    payload => { const rooms = payload.value; dispatch({ type: NEW_ROOMS_LIST_RECEIVED, payload: rooms }); }); };
  64. The Lobby export default (state = INITIAL_STATE, action) => {

    switch (action.type) { case NEW_ROOMS_LIST_RECEIVED: return { ...state, rooms: action.payload, }; default: return state; } };
  65. The Lobby export default (state = INITIAL_STATE, action) => {

    switch (action.type) { case NEW_ROOMS_LIST_RECEIVED: return { ...state, rooms: action.payload, }; default: return state; } };
  66. The Lobby export default (state = INITIAL_STATE, action) => {

    switch (action.type) { case NEW_ROOMS_LIST_RECEIVED: return { ...state, rooms: action.payload, }; default: return state; } };