Crossing platforms with JavaScript & React

Preview:

Citation preview

Crossing platforms with JavaScript & React

@robdel12

JavaScript is EVERYWHERE

Web

Server

iOS

Android

Desktop

What is cross platform JS?

JS that can run on more than one platform

“Why did the iOS team implement it like this?”

“The Android app currently doesn’t support that”

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

Easier to share code across many teams

More team collaboration since there’s more overlap

It allows teams to own products & not be separated by technology

TL;DR your team now owns the iOS, Android, and (maybe) web apps.

Consistency is 🔑

Cheaper

If you can build iOS & Android apps in the same code base it should be cheaper

Why not bet on the web?

Native will be better than mobile web for a while

Why not take the web tooling & get native results?

You are betting on the web

Can’t beat them, join them

I decided to be ambitious

Build an Instagram clone for Web, iOS, & Android

Why an Instagram clone?

Use Impagination.js to power an Infinite scroll of images

Impagination will work on any JS codebase

Building infinite scroll in React Native with Impagination

http://bit.ly/reactnativeinfinitescroll

We’ve already used Impagination in four different platforms

What else can be shared?

Experiment time

Three phases to the experiment

• Planning

• Implementation

• Postmortem

Planning

🥞The stack🥞

• React Native

• React (DOM)

• Auth0 (authentication)

• Graph.cool (backend)

• Impagination (infinite datasets)

What should the app do?

• Login / Sign up

• See your profile & images you’ve posted

• Edit your profile

• Post a new photo

• Main list feed showing everyones posts

Web demo

Implementation

What’s the approach?

Build the web app

Start to build the native app

Realize I’ve already solved these problems in the web app

Refactor

ListPage.js

ListPage.js handles both UI & data right now

ListPage for native duplicates a lot form web ListPage

class ListPage extends React.Component { static propTypes = { data: React.PropTypes.object, } state = { dataset: null, datasetState: null, } setupImpagination() {}

componentWillMount() {this.setupImpagination();}

setCurrentReadOffset = (event) => {}

render () { return ( <div style={{maxWidth: "600px", margin: "0 auto", padding: "20px 0"}}> <Infinite elementHeight={ITEM_HEIGHT} handleScroll={this.setCurrentReadOffset} useWindowAsScrollContainer> {this.state.datasetState.map(record => { if (record.isPending && !record.isSettled) { return <LoadingPost key={Math.random()} />; }

return <Photo key={record.content.id} photo={record.content} user={record.content.user} />; })} </Infinite> </div> ); } }

const FeedQuery = gql`query($skip: Int!, $first: Int!) { allPhotos(orderBy: createdAt_DESC, first: $first, skip: $skip) { } }`;

export default graphql(FeedQuery, {options: {variables: { skip: 0, first: PAGE_SIZE }}})(ListPage);

Web ListPage.js

`

class ListPage extends React.Component { static propTypes = { data: React.PropTypes.object, } state = { dataset: null, datasetState: null, } setupImpagination() {}

componentWillMount() {this.setupImpagination();}

setCurrentReadOffset = (event) => {}

render () { return ( <ScrollView style={{flex: 1}} scrollEventThrottle={300} onScroll={this.setCurrentReadOffset} removeClippedSubviews={true}> {this.state.datasetState.map(record => { if(record.isPending && !record.isSettled) { return <LoadingPost key={Math.random()}/>; }

return <Photo key={record.content.id} photo={record.content} user={record.content.user} />; })} </ScrollView> ); } }

const FeedQuery = gql`query($skip: Int!, $first: Int!) { allPhotos(orderBy: createdAt_DESC, first: $first, skip: $skip) { } }`;

export default graphql(FeedQuery, {options: {variables: { skip: 0, first: PAGE_SIZE }}})(ListPage);

Native ListPage.js

Everything but the UI is the same

New structure

Presentation & container components

<IndexRoute component={ListPageContainer} /> <Route path='feed' component={ListPageContainer} />

import ListPageView from ‘../components/presentational/ListPageView’;

class ListPageContainer extends React.Component { state = { dataset: null, datasetState: null, } setupImpagination() {}

componentWillMount() {this.setupImpagination();}

setCurrentReadOffset = (event) => {}

render () { return ( <ListPageView setCurrentReadOffset={this.setCurrentReadOffset} datasetState={this.state.datasetState} />; ); } }

const FeedQuery = gql`query($skip: Int!, $first: Int!) { allPhotos(orderBy: createdAt_DESC, first: $first, skip: $skip) { } }`;

export default graphql(FeedQuery, {options: {variables: { skip: 0, first: PAGE_SIZE }}})(ListPage);

Make the container component render a separate presentation component

Leave setting the readOffset to the presentation components

setCurrentReadOffset function is passed as a prop from the container component

t

import React, { Component } from 'react'; import Infinite from 'react-infinite'; import Photo from '../presentational/Photo'; import LoadingPost from '../presentational/LoadingPost';

const ITEM_HEIGHT = 600; const HEADER_HEIGHT = 80;

class ListPageView extends Component { setCurrentReadOffset = (event) => { let currentItemIndex = Math.ceil((window.scrollY - HEADER_HEIGHT) / ITEM_HEIGHT);

this.props.setCurrentReadOffset(currentItemIndex); }

render() { return ( <div style={{maxWidth: "600px", margin: "0 auto", padding: "20px 0"}}> <Infinite elementHeight={ITEM_HEIGHT} handleScroll={this.setCurrentReadOffset} useWindowAsScrollContainer> {this.props.datasetState.map(record => { if (record.isPending && !record.isSettled) { return <LoadingPost key={Math.random()} />; }

return <Photo key={record.content.id} photo={record.content} user={record.content.user} />; })} </Infinite> </div> ); } }

export default ListPageView;

Web presentation component

Native presentation component

import React, { Component } from 'react'; import Photo from '../presentational/Photo'; import LoadingPost from '../presentational/LoadingPost'; import { ScrollView } from 'react-native';

const ITEM_HEIGHT = 485;

class ListPageView extends Component { setCurrentReadOffset = (event) => { let currentOffset = Math.floor(event.nativeEvent.contentOffset.y); let currentItemIndex = Math.ceil(currentOffset / ITEM_HEIGHT);

this.props.setCurrentReadOffset(currentItemIndex); }

render() { return ( <ScrollView style={{flex: 1}} scrollEventThrottle={300} onScroll={this.setCurrentReadOffset} removeClippedSubviews={true}> {this.props.datasetState.map(record => { if(record.isPending && !record.isSettled) { return <LoadingPost key={Math.random()}/>; }

return <Photo key={record.content.id} photo={record.content} user={record.content.user} />; })} </ScrollView> ); } }

export default ListPageView;

This theme continues throughout the entire app

<IndexRoute component={ListPageContainer} /> <Route path='feed' component={ListPageContainer} /> <Route path='new' component={CreatePostContainer} onEnter={this.requireAuth.bind(this)} /> <Route path='signup' component={CreateUserContainer} /> <Route path='profile' component={UserProfileContainer} > <IndexRoute component={UserProfileContainer} /> <Route path='edit' component={EditProfileContainer} /> </Route> <Route path='logout' component={() => <Logout logout={this.handleToken.bind(this)} /> } />

Native app demo

Postmortem

Building the apps in time was hard…

Figuring out what code is shareable

Figuring out how to make that code shareable

React Router is neat & works cross platform

There are different imports for React Native & React

Auth0 was very easy to implement on both platforms.

There are different APIs for React Native & React

AsyncStorage vs localStorage

What all ended up being shared?

✅ List feed✅ User profile✅ Edit user profile✅ Sign up✅ New post

Beyond login mostly everything else is the same

The UI changed but not the business logic

Key takeaways

We’re in a post DOM world

Write JavaScript interaction models

The UI framework will change but the underlying model driving it won’t

“It’s just JavaScript”

We get stronger libraries by increasing the number of users & contributors.

React makes this very easy thanks to React & React Native

I was able to share the same five container components across three different platforms

Write one container component and many UI components

The core of this app is shared

That’s a cost savings

I own this entire product & its 3 platforms

In 2 weeks I was able do all of this

Cross platform JS FTW

Instagram also agrees with mehttps://engineering.instagram.com/react-native-at-instagram-

dd828a9a90c7#.i364vchox

If this kind of stuff interests you

We’re hiring!

Thanks!@robdel12

Recommended