1 of 37

Building Expo Snack

2 of 37

Hello!

I’m Satyajit Sahoo

callstack.io

@satya164

3 of 37

4 of 37

What is Snack

A playground for React Native

5 of 37

6 of 37

Expectations

  • Start without effort
  • Preview results instantly
  • Preview as it’s meant to be
  • Debuggable
  • Helpful
  • Shareable

7 of 37

RNPlay

  • Used React Native Packager to bundle the projects
  • Packager instance was slow to start and rebuild
  • Online simulator wasn’t fast

8 of 37

9 of 37

A simple concept

What if we could just patch-in code that’s changing instead of rebundling every time?

10 of 37

11 of 37

Code from Editor

Patched with Eval

Snack runtime

12 of 37

Challenges

  • Allow to write ESNext code
  • Build a module system so you can import stuff
  • Prevent the app go to bad state because of rendering errors

13 of 37

Babel Standalone

  • Standalone version of Babel
  • Can be used in Browser and Node (and in React Native)

14 of 37

Transpile Code on the Fly

let transformedCode = null, error = null;

try {

transformedCode = babelStandalone.transform(

codeFromEditor, {

presets: [

'es2015',

'React',

'Expo'

],

}).code;

} catch (e) {

error = e;

}

15 of 37

Wrap to provide modules support

(function(require, module, exports) {

exports = {};

module = {exports: exports};

// Transformed code from previous slide goes here

return module;

})(function(name) {

var modules = global['$$$___Snack_Modules___$$$'];

if (name in modules) return modules[name];

throw new Error("Trying to import unknown module '" + name + "'");

});

16 of 37

Eval to get exported module

let m;

try {

if (wrappedCode)

m = eval(wrappedCode);

} catch (e) {

error = e;

}

let MyComponent = m;

if (m && m.exports && m.exports.__esModule) {

MyComponent = m.exports.default;

} else if (m && m.exports) {

MyComponent = m.exports;

} else {

MyComponent = undefined;

}

17 of 37

Eval to get exported module

let m;

try {

if (wrappedCode)

m = eval(wrappedCode);

} catch (e) {

error = e;

}

let MyComponent = m;

if (m && m.exports && m.exports.__esModule) {

MyComponent = m.exports.default;

} else if (m && m.exports) {

MyComponent = m.exports;

} else {

MyComponent = undefined;

}

18 of 37

Patch to catch errors

Object.getOwnPropertyNames(MyComponent).forEach(prop => {

const method = MyComponent[prop];

if (method && method.constructor && method.call && method.apply) {

const old = MyComponent[prop], _this = this;

MyComponent[prop] = function(...args) {

try {

const result = old.apply(this, args);

if (prop === 'render') _this.props.onError(null);

return result;

} catch (error) {

return _this._renderError(error);

}

};

}

});

19 of 37

Patch to catch errors

Object.getOwnPropertyNames(MyComponent).forEach(prop => {

const method = MyComponent[prop];

if (method && method.constructor && method.call && method.apply) {

const old = MyComponent[prop], _this = this;

MyComponent[prop] = function(...args) {

try {

const result = old.apply(this, args);

if (prop === 'render') _this.props.onError(null);

return result;

} catch (error) {

return _this._renderError(error);

}

};

}

});

20 of 37

Shortcomings

  • Native errors crash the app
  • RedBox and YellowBox are not available

21 of 37

Import from NPM

  • Should be easy to use
  • Should be deterministic
  • Should support asset files like images and fonts

22 of 37

Bundling

  • Two types of files: code and asset
  • Require for assets are transpiled to code with metadata on the asset
  • The modules are transpiled and then stored in an object in the bundle
  • When any file changes, the bundle is rebuilt

23 of 37

Asset management

var AssetRegistry = require('AssetRegistry');

module.exports = AssetRegistry.registerAsset({

httpServerLocation: 'https://s3.../',

name: 'doge.gif,

width: 200,

height: 200,

type: 'gif',

hash: [...],

fileHashes: [...],

scales: [1, 1.5, 2],

});

24 of 37

Metro Bundler

  • Creates a bundle with your code and all its dependencies
  • Serves assets via the dev server, or copy to app’s assets folder when packaging the app
  • Includes polyfills and environment setup code so the app runs

25 of 37

Metro Bundler

  • Doesn’t package the code to be used as a module
  • Cannot be configured to exclude extra polyfills or setup code
  • Cannot be configured to exclude dependencies

26 of 37

Snackager

  • Uses Webpack behind the scenes
  • Uses loaders and plugins from Haul
  • Creates a module bundle with with a single dependency and it’s dependencies
  • Uploads assets to a S3 bucket
  • Doesn’t include any polyfills
  • Bundling triggers with a GET request

27 of 37

bundle/

[name]@[version]

?platforms=

ios,android

Fetch metadata from npm

Build a fresh bundle and upload to S3, return it

Check S3 for existing bundle, if exists return it

28 of 37

29 of 37

The Editor

  • Drag n drop components to code faster
  • ESLint integration for writing better code
  • Prettier integration for auto-formatting

30 of 37

31 of 37

Usecases

  • Teaching and learning React Native
  • Prototyping
  • Documentation
  • Sharing code snippets
  • Bug reports

32 of 37

33 of 37

native.directory

  • Curated list of React Native libraries
  • Many libraries have examples on Snack to try quickly
  • Can filter libraries based on their platform and compatibility with Expo

34 of 37

35 of 37

Future

  • Multi-file support
  • User accounts

36 of 37

37 of 37

Thank you