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)
- Get passed a VNode
- If nodeName is function
- Call the function, returns VNode
- Pass VNode to #1
- Create a DOM node for the type
- Copy VNode attributes to node
- 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
- Preact's is called preact-cycle
- Basically what we just wrote
- git.io/preact-cycle
That's it!
...for now.
Go build stuff and focus on what, not how.
Resources
- Preact
- 3.5kb implementation of this stuff
- Docs & Demos: git.io/preact
- Get started with a Boilerplate:
- Clone: git.io/preact-boilerplate
- Demo: preact-boilerplate.surge.sh
Component Rendering Intro
By developit
Component Rendering Intro
- 3,367