Building With Components

A presentation vaguely about Preact.

What is a

Component?

What is a Component?

  • A building block
  • Lego for your DOM
  • Unit of Composition
  • Instantiated with data
  • Renders something

Why build like this...

...when we think like this?

<div class="toc">
  <ul>
    <li>
      <a href="#1">One</a>
    </li>
    <li>
      <a href="#2">Two</a>
    </li>
    <li>
      <a href="#3">Three</a>
    </li>
  </ul>
</div>
<toc>


  <link to="1">One</link>


  <link to="2">Two</link>


  <link to="3">Three</link>


</toc>

Building blocks

that produce markup

assembled as markup

"Virtual DOM"

Actual DOM

First

Things
First

JSX

Markup in Code

You can't hate it yet

From the Authors

  • XML-like syntax extension to ECMAScript without any defined semantics.
  • Defines a concise and familiar syntax for defining tree structures with attributes
  • facebook.github.io/jsx

JSX Explained

  • A new type of JS Expression
  • Transpilers convert to a function call
    • Any name, but use h()
  • It's optional: can write this by hand
<div id="one">
  Hello
</div>


<Foo hello />
h('div', { id:'one' },
  'Hello'
);


h(Foo, { hello: true })

JSX Explained

  • Inside braces: any JavaScript expression
  • Capitalized node name = reference
let world = 'World!';

<div one={ 1 }>
  Hello, { world }
</div>

let Link = 'a';
<Link>Test</Link>
var world = 'World!';

h('div', { one: 1 },
  'Hello, ', world
);

var Link = 'a';
h(Link, 'Test');

Proof

Lets Build a JSX Renderer

Hint: it's easy

The Reviver Function

  • Gets inserted in place of every <tag>
  • Called at runtime by transpiled JSX

  h(
​    nodeName,    // String or Function
    attributes,  // Object or null
    ...children  // any remaining args
  )

Rough Implementation


 function h(nodeName, attributes, ...children) {

   // flatten any nested Arrays in children:
   children = [].concat(...children);

   // a simple VDOM node is just an object:
   return { nodeName, attributes, children };

 }

... that's all there is

We write this:

<div id="foo">
  Hello!
  <br />
</div>

... which transpiles to this:

h('div', { id:'foo' },
  'Hello!',
  h('br')
)

... which returns this:

{
  nodeName:'div',
  attributes: { id:'foo' },
  children: [
    'Hello!',
    { nodeName:'br' }
  ]
}

Rendering JSX to DOM

  • The output of h() describes a DOM node
  • .children[] is an Array of child nodes
  • We can use this to build a DOM tree
function render(vnode) {  
  if (typeof vnode==='string') {
    return document.createTextNode(vnode);
  }

  let n = document.createElement(vnode.nodeName);

  let a = vnode.attributes || {};
  Object.keys(a).forEach( k => n.setAttribute(k, a[k]) );

  (vnode.children || []).forEach( c => n.appendChild(render(c)) );

  return n;
}

Does it work?

// build our virtual DOM:
let vdom = (
  <div id="foo">
    <h1>Hello</h1>
    <ol>
      <li>One</li>
      <li>Two</li>
    </ol>
  </div>
);

// use it to build real DOM nodes:
let dom = render(vdom);

// insert it into the page:
document.body.appendChild(dom);

YUP

Deploy It

Back

To

Components

Components are:

  • Encapsulated
  • Composable
  • Reusable
  • Reusable
  • Consistent
  • Testable
  • Minimal

Encapsulation

  • One API
    • props => vdom
  • Internals can vary
    • Pure function
    • ES Class
    • Object
    • Module
    • State
  • Props Down, Events Up

The golden rule:

Props Down, Events Up

To give a component data:

pass it props

 

To get data back out of a component:

pass a function as a prop,

then call it from the child component.

 

Composition

High-Order Components:   "I am a ____"

 const Parent = props => (
   <Child />
 )

Nested Components:      "I contain a ____"

 const Ancestor = props => (
   <div>
     <Descendant />
     <Descendant />
   </div>
 )

A components can render another component as its root

A components can render other components as nested children

Re-use

Generic "Dumb" Components

  • Encapsulate rendering of common UI
  • Pure: props in, vdom out
  • Highly re-usable, libraries

 

Specific "Smart" Components

  • Connect to data sources, persistence
  • Maintain (or delegate) state
  • Less re-usable, business logic

Re: use

  • Isomorphic / universal rendering:
    • client: renders to DOM
    • server: renders static HTML
    • no parser: just plain objects

 

  • Only requirement: JavaScript

Remember: Server rendering is an optimization. Web crawlers run JS.

Write-once, Run Anywhere

Consistency

  • Every component works the same way
  • Rendering always works the same way
  • Lifecycle always invoked the same way

Why does this matter?

No guessing. Remember jQuery?

instantiate: $('guess').??(????)
     render: side effect, varies
     update: automatic? varies

This is the value of a component API.

Testability

  • Mocking the DOM is hard / horrible
  • Abstracting the DOM is hard
  • VDOM (JSX) is a DOM abstraction
  • Mocking VDOM is free

Testing VDOM is... beautiful:

expect(
  <List items={['one','two']} />
).to.deep.equal(
  <ul>
    <li>one</li>
    <li>two</li>
  </ul>
)
const List = ({ items }) => (
  <ul>
    { items.map( item => (
      <li>{ item }</li>
    )) }
  </ul>
)

Minimalism

  • Tiny API, no magic
  • Just functions calling other functions
  • Mutations automatically re-render
  • Nearly 100% opt-in (JSX, classes, ...)

Classes

When functions simply won't Do

Components can also be classes

  • Slightly larger API
  • Lifecycle methods
  • Stateful

Ways to Be Classy

class Foo extends Component {
  state = {
    text: 'initial text'
  };

  componentDidMount() {
    // Now attached to the DOM
  }

  componentWillUnmount() {
    // About to be removed
  }

  render(props, state) {
    // analogous to a functional component
    return <button class="foo">Foo!</button>
  }
}
function Foo() {
  Component.apply(this, arguments);
  this.state = {
    text: 'initial text'
  };
}

Foo.prototype = new Component();  // * the hacks

Foo.prototype.componentDidMount = function() {
  // Now attached to the DOM
};

Foo.prototype.componentWillUnmount = function() {
  // About to be removed
};

Foo.prototype.render = function(props, state) {
  // analogous to a functional component
  return h('button', {'class':'foo'}, 'Foo!');
};
function Foo() {
  Component.apply(this, arguments);
  this.state = {
    text: 'initial text'
  };
}

Foo.prototype = Object.create(Component);

Object.assign(Foo.prototype, {
  componentDidMount: function() {
    // Now attached to the DOM
  },

  componentWillUnmount: function() {
    // About to be removed
  },

  render: function(props, state) {
    // analogous to a functional component
    return h('button', {'class':'foo'}, 'Foo!');
  }
});

ES6

...ES5

...ES3

Component State

Only one way:

Pass new/updated properties to setState()

Properties are copied onto current state.

 // initial state:
 state = { text:'foo' };

 // any updates:
 this.setState({ text:'bar' });

 state.text==='bar';  // true

Why setState?

  • setState() re-renders the component
  • Avoids observers, microtasks, etc
  • Promotes immutability
  • Allows batching

State Example

class Foo extends Component {
  @bind
  updateText(e) {
    // update component state from input value:
    this.setState({ text: e.target.value });
  }

  render(props, state) {
    return (
      <input value={state.text}
        onInput={this.updateText} />
    );
  }
}

State management

can be annoying

So preact includes another trick.

(Similar add-on available for React)

Linked State Example

class Foo extends Component {
  render(props, state) {
    return (
      <input
        value={state.text}
        onInput={this.linkState('text')}
      />
    );
  }
}
linkState(key)
  • ​Creates an updater for key in state
  • Cached: essentially no performance penalty
  • Supports dot-notated deep key paths
  • Optional source path for custom linkage

Performance

What's all this noise
about Virtual DOM?

Re-rendering everything

  • How can this be fast?
  • Makes no sense

Devs in your area speed up their
DOMs with this one simple trick!!

Teaser:

WHEN ANYTHING CHANGES

WHAT IF...

INSTEAD OF RENDERING

WE DIFFED the VDOM objects

AGAINST THE CURRENT DOM

...and applied just the difference?

Virtual DOM

Real DOM

{
  nodeName: 'div',
  attributes: {
    key: 'value'
  },
  children: [
    'text node'
  ]
}
Element(
  nodeName: 'div',
  attributes: {
    getItem('key'),
    setItem('key', 'value')
  },
  childNodes: [
    TextNode('text node')
  ]
)

Parallels

Virtual DOM DOM Declarative DOM Imperative
nodeName nodeName createElement(nodeName)
attributes[] attributes{} setAttribute(key, value)
getAttribute(key): value
children[] childNodes[] appendChild(child)
removeChild(child)
insertBefore(child, next)
replaceChild(old, child)

we diff these

Now we're talkin'

  • As few DOM operations as possible?
  • That sounds fast.
  • Turns out it is fast.

 

 

  • Create 100 + sin(i)*100 nodes
  • Style & position them all
  • Make them follow the mouse
  • Re-render every single node
  • Do it at 60fps (16.6ms/render)

 

 

Why?

  • Predictable
    • Return desired DOM from render()
    • Differences handled automatically
  • Repeatable
    • render() for initial DOM creation
    • render() for later DOM updates
  • Composable
    • Natural component nesting
    • Full control over child updates

(rendering)

  1. Get passed a VNode
  2. If nodeName is function
    • Call the function, returns VNode
    • Pass VNode to #1
  3. Create a DOM node for the type
  4. Copy VNode attributes to node
  5. Loop over VNode children
    • For each, pass child to #1

Process​

Any time anything changes...

Just render it all again.

Things that didn't change are an empty diff.

(Updating)

Process​

Prior Art

  • Game loops
  • Processing
  • Video

Great artists steal, right?

Functional

Programming

Detour

Functional Programming

import { render, h } from 'preact-cycle';

const ADD = v => v + 1;

const App = ({ value, mutation }) => (
  <div>
    <p>Value: { value }</p>
    <button onClick={ mutation('value', ADD) }>
      Increment
    </button>
  </div>
);

render(App, { value: 0 });

Not Complicated

function cycle(fn, data, into) {
  let root;

  // apply mutate() to a key and re-render
  data.mutation = (key, mutate) => () => {
    data[key] = mutate(data[key]);
    update();
  };

  // render to DOM
  let update = () => {
    root = render(h(fn, data), into, root);
  };

  // initial render
  update();
}

There are 1000 libraries for this

That's it!

...for now.

Go build stuff and focus on what, not how.

Resources

Component Rendering Intro

By developit

Component Rendering Intro

  • 1,149
Loading comments...

More from developit