Pure Magic: Functional Reactive Web Apps

JS

Gleb Bahmutov

C / C++ / C# / Java / CoffeeScript / JavaScript / Node / Angular / Vue / Cycle.js / FP FunP

JavaScript ninja, image processing expert, software quality fanatic

Purdue University

EveryScape

virtual tours

MathWorks

MatLab on the web

Kensho

finance dashboards

Around 50 conference decks at https://slides.com/bahmutov

with videos at https://glebbahmutov.com/videos

OSCON, ng-conf, NodeConf.eu ...

link to more information

VP of Eng at

Testing, the way it should be

Usual E2E runners need delays or run slow in this case ... (Karma, Protractor, Selenium)

  1. Click on a button
  2. Action is delayed by 2 seconds
beforeEach(() => {
  cy.visit('index.html')
})
it('starts with 0', () => {
  cy.contains('Counter: 0')
})
it('increments counter 3 times', () => {
  cy.contains('Increment').click()
  cy.contains('Counter: 1')
  cy.contains('Increment').click()
  cy.contains('Counter: 2')
  cy.contains('Increment').click()
  cy.contains('Counter: 3')
})

Not  single "delay" yet it runs in 6 seconds!

Talk

  1. OO in JavaScript

  2. FP in JavaScript

    1. partial application

    2. pure functions

    3. composition

  3. hyper-app.js

  4. reactive streams

  5. cycle.js

⏰ 40 min

⏰ 40 min

break

Code always grows

more complex

Company lifetime

Number of engineers

How do you write a complex application?

A: Write complex code

B: Assemble simple pieces

Complex code

  • Giant singletons

  • Code is hard to test

  • Code is hard to reuse

Example problem

var numbers = [3, 1, 7]
var constant = 2
// 6 2 14

given an array of numbers, multiply each number by a constant and print the result.

Highly recommended

var numbers = [3, 1, 7]
var constant = 2
// expected output [6, 2, 14]

Also: comment-value, Quakka

procedural / imperative JS

const numbers = [3, 1, 7]
const constant = 2
let k = 0
for(k = 0; k < numbers.length; k += 1) {
  console.log(numbers[k] * constant)
}
// 6 2 14
  • "for" loop is a pain to debug and use

  • mixing data manipulation with printing

  • code is hard to reuse

Refactoring helps

const numbers = [3, 1, 7]
const constant = 2

reusable simple functions

complex logic

iteration

function mul(a, b) {
  return a * b
}
function print(x) {
  console.log(x)
}
function processNumber(n) {
  print(mul(n, constant))
}
for(let k = 0; k < numbers.length; k += 1) {
  processNumber(numbers[k])
}
// 6 2 14

A function should do a single thing

 

No AND / OR / INCLUDING

OO to the rescue

function NumberMultiplier() {}

Object-Oriented JavaScript

NumberMultiplier.prototype.setNumbers = function (numbers) {
    this.numbers = numbers;
    return this;
};
NumberMultiplier.prototype.multiply = function (constant) {
    for (var k = 0; k < this.numbers.length; k += 1) {
      this.numbers[k] = constant * this.numbers[k];
    }
    return this;
};
NumberMultiplier.prototype.print = function () {
    console.log(this.numbers);
};

Object-Oriented JavaScript

// using NEW keyword
new NumberMultiplier()
  .setNumbers(numbers)
  .multiply(constant)
  .print();
// [ 6, 2, 14 ]

Object-Oriented JavaScript: 3 parts

function NumberMultiplier() {}

NumberMultiplier.prototype.setNumbers = function() {}
NumberMultiplier.prototype.multiply = function() {}
NumberMultiplier.prototype.print = function() {}

new NumberMultiplier();

constructor function

prototype

"new" keyword

// NumberMultiplier.prototype object
{
  setNumbers: function () { ... },
  multiply: function () { ... },
  print: function () { ... }
}

OO JavaScript is prototypical

// instance object
{
  numbers: [3, 1, 7]
}

prototype

var NumberMultiplier = {
  setNumbers: function (numbers) {
    this.numbers = numbers; return this;
  },
  multiply: function (constant) {
    for (var k = 0; k < this.numbers.length; k += 1) {
      this.numbers[k] = constant * this.numbers[k];
    } return this;
  },
  print: function () { console.log(this.numbers); }
};

Prototype is a plain object!

var numberMultiplier = Object.create(NumberMultiplier);
numberMultiplier
  .setNumbers(numbers)
  .multiply(constant)
  .print();
// [ 6, 2, 14 ]

plain object

class NumberMultiplier {
  setNumbers(numbers) {
    this.numbers = numbers;
    return this;
  }
  multiply(constant) {
    for (var k = 0; k < this.numbers.length; k += 1) {
      this.numbers[k] = constant * this.numbers[k];
    }
    return this;
  }
  print() {
    console.log(this.numbers);
    return this;
  }
}
new NumberMultiplier()

ES6 classes: just don't

class NumberMultiplier {
  setNumbers(numbers) {
    this.numbers = numbers;
    return this;
  }
  multiply(constant) {
    for (var k = 0; k < this.numbers.length; k += 1) {
      this.numbers[k] = constant * this.numbers[k];
    }
    return this;
  }
  print() {
    console.log(this.numbers);
    return this;
  }
}
const n = new NumberMultiplier()
n.setNumbers([1, 2, 3])
setTimeout(n.print, 1000) // undefined

this will betray you

🔥🔥🔥

I ❤️❤️❤️ Vue.js

<div id="app-5">
  <p>{{ message }}</p>
  <button v-on:click="reverseMessage">Reverse</button>
</div>
new Vue({
  el: '#app-5',
  data: {
    message: 'Hello Vue.js!'
  },
  methods: {
    reverseMessage: function () {
      this.message = this.message
        .split('').reverse().join('')
    }
  }
})

this

People ❤️❤️❤️ React

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  tick() {
    this.setState({
      date: new Date()
    });
  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

this

Functional React Components

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

Fun.

Programming

Complex logic assembled from simple parts

OO

"Smart" objects

Pass data around

Functional

"Dumb" objects

Pass logic around

new NumberMultiplier()
  .setNumbers([3, 1, 7])
  .multiply(2)
  .print()
f(g(numbers))

Object-oriented VS functional

"Dumb" JavaScript Arrays

typeof [1, 2] // "object"
Array.isArray([1, 2]) // true
Array.prototype
/*
  map
  forEach
  filter
  reduce
  ...
*/

Arrays: OO + FP

var numbers = [3, 1, 7];
var constant = 2;
function mul(a, b) {
  return a * b;
}
function print(n) {
  console.log(n);
}

clear "multiply then print" semantics

Object-oriented methods

numbers
    .map(x => mul(constant, x))
    .forEach(print);
// 6 2 14

FP features

var numbers = [3, 1, 7];
var constant = 2;
function mul(a, b) {
  return a * b;
}
function print(n) {
  console.log(n);
}
numbers
    .map(x => mul(constant, x))
    .forEach(print);
// 6 2 14

"dumb" object

pass logic as

arguments

When do we know arguments?

var numbers = [3, 1, 7];
var constant = 2;
function mul(a, b) {
  return a * b;
}
function print(n) {
  console.log(n);
}
numbers
    .map(x => mul(constant, x))
    .forEach(print);
// 6 2 14

known right away

known much later

Partial application

var numbers = [3, 1, 7];
var constant = 2;
function mul(a, b) {
  return a * b;
}
const mulBy = mul.bind(null, constant)
function print(n) {
  console.log(n);
}
numbers
    .map(x => mulBy(x))
    .forEach(print);
// 6 2 14

Make new function

make code simpler

function fn(a, b, c) { ... }
var newFn = fn.bind(null, valueA, valueB)
newFn(valueC)

Super power: partial application

Partial application from the left

function fn(a, b, c) { ... }
var newFn = fn.bind(null, valueA, valueB);
// or
var _ = require('lodash');
var newFn = _.partial(fn, valueA, valueB);
// or
var R = require('ramda');
var newFn = R.partial(fn, valueA, valueB);

Partial application from the right

['1', '2', '3'].map(parseInt); // [1, NaN, NaN]
// function parseInt(x, radix)
// but Array.map sends (value, index, array)
['1', '2', '3'].map(function (x) {
    return parseInt(x, 10);
});
const base10 = _.partialRight(parseInt, 10)
['1', '2', '3'].map(base10); 
// [1, 2, 3]
// radix is bound, 
// index and array arguments are ignored

Function signature design

Place on the left arguments most likely to be known early

function savePassword(userId, newPassword) {...}
// vs
function savePassword(newPassword, userId) {...}

Point-free code

var numbers = [3, 1, 7];
var constant = 2;
function mul(a, b) {
  return a * b;
}
const mulBy = mul.bind(null, constant)
function print(n) {
  console.log(n);
}
numbers
    .map(x => mulBy(x))
    .forEach(print);
// 6 2 14

Point-free code

var numbers = [3, 1, 7];
var constant = 2;
function mul(a, b) {
  return a * b;
}
const mulBy = mul.bind(null, constant)
function print(n) {
  console.log(n);
}
numbers
    .map(mulBy)
    .forEach(print);
// 6 2 14

single argument

Prepare for partial application

var numbers = [3, 1, 7];
var constant = 2;
const mul = a => b => a * b
const mulBy = mul(constant)
function print(n) {
  console.log(n);
}
numbers
    .map(mulBy)
    .forEach(print);
// 6 2 14

These functions are different!

hint: look where the variables are coming from

function mul(a, b) {
  return a * b
}
function print(x) {
  console.log(x)
}
function mul(a, b) {
  return a * b
}
function print(x) {
  console.log(x)
}

These functions are different!

Pure functions

function mul(a, b) {
  return a * b
}

only uses its arguments

same arguments => same result

does not modify the outside environment

Impure functions

function print(x) {
  console.log(x)
}

uses outside variable

modifies the outside environment

function print(console, x) {
  console.log(x)
}

Pure functions are the best

Simple to write

Simple to read

Simple to test

Pure functions are the best

Bonus: make perfect callbacks

Non-pure function red flag: no return value

Why use JS at all?

  • No other language reaches as widely as JS

  • JS does most of the FP things "well enough"

  • JS is quickly evolving

To me functional programming in JS is all about passing and returning functions

When do you know FP in JS?

['1', '2', '3'].map(parseInt) // [1, NaN, NaN]
// function parseInt(x, radix)
// but Array.map sends (value, index, array)
['1', '2', '3'].map(/* ? */)

Write a FUNCTION that makes parseInt a unary function.

function unary() {
  return function (x) {
    return parseInt(x)
  }
}
['1', '2', '3'].map(unary()) 
// [1, 2, 3]

unary strips second argument to parseInt

make unary a PURE function.

function unary(f) {
  return function (x) {
    return f(x)
  }
}
['1', '2', '3'].map(unary(parseInt)) 
// [1, 2, 3]

unary is pure and reusable function

function unary(f) {
  return function (x) {
    return f(x)
  }
}
['1', '2', '3'].map(unary(parseInt)) 
// [1, 2, 3]

functional JavaScript

Takes function as argument

Returns new function

Atomization

initial spaghetti code

mul
print
...

 readable code

"atoms"

...

Atomization

initial spaghetti code

mul
print

short readable code

Clumping

compose

...
...
for (k = 0; k < numbers.length; k += 1) {
  print(byConstant(numbers[k]))
}

fn

fn

data

Composition = shorter code

for (k = 0; k < numbers.length; k += 1) {
  print(byConstant(numbers[k]))
}

fn

fn

data

write function that executes `f(g(x))`

mulAndPrint = compose(print, byConstant)
mulAndPrint(numbers[k])
// print ( byConstant ( numbers[k] ) )

?

"compose" is a function

mulAndPrint = compose(print, byConstant)
mulAndPrint(numbers[k])
// print ( byConstant ( numbers[k] ) )
function compose()

that takes two arguments

mulAndPrint = compose(print, byConstant)
mulAndPrint(numbers[k])
// print ( byConstant ( numbers[k] ) )
function compose(f, g)

and returns another function

mulAndPrint = compose(print, byConstant)
mulAndPrint(numbers[k])
// print ( byConstant ( numbers[k] ) )
function compose(f, g) {
  return function() {
  
  }
}

that expects a single argument

mulAndPrint = compose(print, byConstant)
mulAndPrint(numbers[k])
// print ( byConstant ( numbers[k] ) )
function compose(f, g) {
  return function(x) {
  
  }
}

and applies original functions to it

mulAndPrint = compose(print, byConstant)
mulAndPrint(numbers[k])
// print ( byConstant ( numbers[k] ) )
function compose(f, g) {
  return function(x) {
    return f(g(x))
  }
}
const mulPrint = compose(print, byConstant)
for (k = 0; k < numbers.length; k += 1) {
  mulPrint(numbers[k])
}

fn

data

Composition

function compose(f, g) {
  return function(x) {
    return f(g(x))
  }
}
const mulPrint = compose(print, byConstant)
for (k = 0; k < numbers.length; k += 1) {
  mulPrint(numbers[k])
}

fn

data

How do you write a complex application?

B: Assemble simple pieces

Note: since JavaScript functions return single result, it is easier to compose unary functions

const F = compose(f, g)
F(x)

pure

pure

Note: composition of pure functions is a pure function

mulAndPrint = compose(print, byConstant)

pure

print "pollutes" the composed function

function app(window) {
  return function () {
    window.title = 'Hello'
  }
}
const run = app(window)
run()

Note: a pure function can return impure function

pure

impure

impure

pure

// cannot declare argument const
function foo (a) {
  a = 'whatever'
}
function foo (a) {
  delete a.important
}
const b = {...}
foo(b) // 😎😎😎

ES6 helps a little

For heavy lifting see

map(fn, list) vs map(list, fn)

Order of arguments

function cb(item, index, array) { ... }
// ES5 method
Array.prototype.map(cb)
// Lodash method
_.map(array, cb)

Place on the left arguments that are likely to be known first 

Order of arguments

_.map(array, cb)

Place on the left arguments that are likely to be known first 

Ramda library

var R = require('ramda')
// callback is first argument!
R.map(cb, array)
// all functions are curried
var by5 = R.multiply(5)
by5(10) // 50

Multiply all numbers by 5

var R = require('ramda')
R.map(R.multiply(5), array)
// same as
R.map(R.multiply(5))(array)

Multiply then print

const printAll = R.forEach(console.log)
printAll(multiplyAll(numbers))
const numbers = [3, 1, 7]
const constant = 2
const R = require('ramda')
const multiplyAll = R.map(R.multiply(constant))

Composition

const multiplyAll = R.map(R.multiply(constant))
const printAll = R.forEach(console.log)
printAll(multiplyAll(numbers))

fn

fn

data

Compose

// printAll(multiplyAll(numbers))
const computation = R.compose(printAll, multiplyAll)
computation (numbers)

Static logic

Dynamic data

R.pipe is like R.compose

const {pipe, map, 
  multiply, forEach} = require('ramda')
const mulPrint = pipe(
  map(multiply(constant)),
  forEach(console.log)
)
mulPrint (numbers)

Try to make JavaScript read just like English

Ramda + pointfree code makes my code simpler to read and modify

If you want to know more:

If you want to know more:

Arrays

Pure Funcs

Composition

Immutable

Web Apps?

HyperApp.js

<body>
<script src="https://unpkg.com/hyperapp@0.7.1"></script>
<script src="https://wzrd.in/standalone/hyperx"></script>
<script>
  const {h, app} = hyperapp
  const html = hyperx(h)
  app({
    model: 'World!',
    view: (model) => html`<h2>Hello, ${model}</h2>`
  })
</script>
</body>

pure view function

ES6 templates to VDOM

Actions change model

app({
  model: 0,
  actions: {
    click: model => model + 1
  },
  view: (model, actions) => html`
    <div>
      <h2>${model}</h2>
      <button onClick=${actions.click}>click</button>
    </div>
  `
})

pure view function

 pure action functions

Subscriptions: external events

app({
  model: 0,
  actions: {
    tick: model => model + 1,
    reset: () => 0
  },
  subscriptions: [
    (model, actions) =>
      setInterval(actions.tick, 1000)
  ],
  view: (model, actions) => html`
    <div>
      <h2>${model}</h2>
      <button onClick=${actions.reset}>reset</button>
    </div>
  `
})

pure

pure

pure

not pure

Model

View

User

Actions

Subscriptions

H

Queue of pure actions

view: (model, actions) => {
  const queue = []
  const schedule = queue.push.bind(queue)
  const go = _ => {
    queue.forEach(f => f())
    queue.length = 0
  }
  return html`
  <div>
    <h2>${model}</h2>
    <button onClick=${_ => schedule(actions.click)}>
      schedule</button>
    <button onClick=${go}>go</button>
  </div>
  `
}

Frameworks like HyperApp are nice, but

  1. Side effects!

  2. Linking components is too hard

We need "better" variables for async

Arrays cannot deal with dynamic data

Promises are single-shot

observables

are like infinite arrays

var numbers = [1, 2, 3]
numbers.forEach(print) 
// 1, 2, 3
numbers.push(100)
// nothing happens
numbers = Rx.Observable.create(observer => {







})
numbers.subscribe(print)
  observer.onNext(1)
  observer.onNext(2)
  observer.onNext(3)
  // ... after long delay
  observer.onNext(100)
  observer.onCompleted()
// 1, 2, 3
// ...
// 100

observables are like  functions that can return multiple values

// creation
var fn = function () { ... }
var ob = Rx.Observable.create(...)
// running
fn() // or fn.call()
numbers.subscribe(...)

success, error, end

var ob = Rx.Observable.create(...)
numbers.subscribe(onNext, onError, onEnd)
// stream with error
// ----1---2---3----X

// stream with end event
// --1---2-3----|

diagrams

// ----1---2------3----|
//    .map(double) 
// ----2---4------6----|
//  .filter(x => x > 4)
// ---------------6----|

diagrams: marbles

operators

[1, 2, 3]
    .map(double)        // [2, 4, 6]
    .filter(x => x > 4) // [6]
    .forEach(print)     // 6
Rx.from([1, 2, 3])       // Observable 1
    .map(double)         // Observable 2 
    .filter(x => x > 4)  // Observable 3
    .subscribe(print)    // 6

must subscribe to cold 

stream to start the event flow!

~175 RxJS operators

Rx.from([1, 2, 3])
    .map(double)
    .buffer(3)
    .delay(5)
    .subscribe(print)    
// pause of 5 seconds
// [2, 4, 6]

User events

var numbers = Rx.from([1, 2, 3])
var clicks = Rx.Observable.fromEvent(
    document.querySelector('#btn'), 'click')
// -1-2-3-|             numbers 
// --c-------c-c------  clicks
//  zip((number, c) => number)
// --1-------2-3-|
numbers
    .zip(clicks, (number, t) => number)
    .subscribe(print)
function searchWikipedia (term) {
    return $.ajax({
        url: 'http://en.wikipedia.org/w/api.php',
        dataType: 'jsonp',
        data: {
            action: 'opensearch',
            format: 'json',
            search: term
        }
    }).promise();
}

Write autocomplete

Hard problem: need to throttle, handle out of order returned results, etc.

var keyups = Rx.Observable.fromEvent($('#input'), 'keyup')
    .map(e => e.target.value)
    .filter(text => text.length > 2)

Autocomplete with RxJs

keyups.throttle(500)
    .distinctUntilChanged()
    .flatMapLatest(searchWikipedia)
    .subscribe(function (data) {
        // display results
    });

How?

var keyups = Rx.Observable.fromEvent($('#input'), 'keyup')
    .map(e => e.target.value)
    .filter(text => text.length > 2)

display results

keyups.throttle(500)
    .distinctUntilChanged()
    .flatMapLatest(searchWikipedia)
    .subscribe(function (data) {
        $('#results').innerHTML = '<ul>' +
            data.map(x => `<li>${x}</li>`) +
          '</ul>'
    });

Slow if only part of data changes

var keyups = Rx.Observable.fromEvent($('#input'), 'keyup')
    .map(e => e.target.value)
    .filter(text => text.length > 2)

use virtual dom

keyups.throttle(500)
    .distinctUntilChanged()
    .flatMapLatest(searchWikipedia)
    .subscribe(function (data) {
        var vtree = vdom.ul(data.map(vdom.li))
        vdom.patch($('#results'), vtree)
    });

reactive streams

RxJs

Bacon.js

Kefir

Most.js

xstream

your choice

widely used, fast

light

fastest

tiny

remember pure functions?

cycle.js makes your application pure

factor out side effects

function main() {
  var keyups = Rx.Observable.fromEvent(
        $('#input'), 'keyup')
    .map(e => e.target.value)
    .filter(text => text.length > 2)
  keyups.throttle(500)
    .distinctUntilChanged()
    .flatMapLatest(searchWikipedia)
    .subscribe(function (data) {
        var vtree = vdom.ul(data.map(vdom.li))
        vdom.patch($('#results'), vtree)
    });
}
main()

Input from DOM

Output to DOM

step 1: separate output

function main() {
  var keyups = Rx.Observable.fromEvent(
        $('#input'), 'keyup')
    .map(e => e.target.value)
    .filter(text => text.length > 2)
  return keyups.throttle(500)
    .distinctUntilChanged()
    .flatMapLatest(searchWikipedia)
    .map(data => vdom.ul(data.map(vdom.li))
}
function run(fn) {
  fn().subscribe(function (vtree) {
    vdom.patch($('#results'), vtree)
  });
}
run(main)

Input from DOM

Output to DOM

step 2: separate input

function main(dom$) {
  var keyups = dom$('#input', 'keyup')
    .map(e => e.target.value)
    .filter(text => text.length > 2)
  return keyups.throttle(500)
    .distinctUntilChanged()
    .flatMapLatest(searchWikipedia)
    .map(data => vdom.ul(data.map(vdom.li))
}
function run(fn) {
  var dom$ = (sel, event) 
    => Rx.Observable.fromEvent(...) }
  fn(dom$).subscribe(function (vtree) {
    vdom.patch($('#results'), vtree)
  });
}
run(main)

Input and Output

pure app

step 3: profit! (Cycle.js)

function main(dom$) {
  var keyups = dom$('#input', 'keyup')
    .map(e => e.target.value)
    .filter(text => text.length > 2)
  return keyups.throttle(500)
    .distinctUntilChanged()
    .flatMapLatest(searchWikipedia)
    .map(data => vdom.ul(data.map(vdom.li))
}
function run(fn) {
  var dom$ = (sel, event) 
    => Rx.Observable.fromEvent(...) }
  fn(dom$).subscribe(function (vtree) {
    vdom.patch($('#results'), vtree)
  });
}
run(main)

cycle.js framework

pure app

cycle.js is a tiny layer for controlling side effects ON TOP OF REACTIVE STREAMS 

function main({DOM}) {
    const decrement$ = DOM.select('.decrement')
      .events('click').map(ev => -1);

    const increment$ = DOM.select('.increment')
      .events('click').map(ev => +1);

    const action$ = Observable.merge(decrement$, increment$);
    const count$ = action$.startWith(0).scan((x,y) => x+y);

    const vtree$ = count$.map(count =>
      div([
        button('.decrement', 'Decrement'),
        button('.increment', 'Increment'),
        p('Counter: ' + count)
      ])
    );
    return { DOM: vtree$ };
  }

counter example

ui to intent

intent to model

model to view

function main({DOM}) {
    const decrement$ = DOM.select('.decrement')
      .events('click').map(ev => -1);

    const increment$ = DOM.select('.increment')
      .events('click').map(ev => +1);

    const action$ = Observable.merge(decrement$, increment$);
    const count$ = action$.startWith(0).scan((x,y) => x+y);

    const vtree$ = count$.map(count =>
      div([
        button('.decrement', 'Decrement'),
        button('.increment', 'Increment'),
        p('Counter: ' + count)
      ])
    );
    return { DOM: vtree$ };
  }

Functional Reactive Programming

every function

including 'main'

is pure

learning cycle.js

Cycle docs are fantastic

A couple of presentations and videos at

CycleConf2017 videos https://vimeo.com/album/4578937 

Pure functions are the best

Simple to write

Simple to read

Simple to test

Learn to use Observables

Great for async data flow

Factor out side effects

Some modern frameworks help with that

The end

JS

👏 thank you 👏

Pure Magic: Functional Reactive Web Applications in 2017 and Beyond

By Gleb Bahmutov

Pure Magic: Functional Reactive Web Applications in 2017 and Beyond

There are a few web frameworks that are all the rage right now: React, VueJs, but which frameworks are functional AND reactive? Dr. Gleb Bahmutov will talk about functional reactive web applications you can write today, and why they are going to rock tomorrow. Presented at Maine.JS meetup in May of 2017

  • 2,467
Loading comments...

More from Gleb Bahmutov