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

Realtime Mobile Apps with React Native and Elix...

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; } };