A presentation vaguely about Preact.
<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
<div id="one">
Hello
</div>
<Foo hello />
h('div', { id:'one' },
'Hello'
);
h(Foo, { hello: true })
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');
h(
nodeName, // String or Function
attributes, // Object or null
...children // any remaining args
)
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' }
]
}
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;
}
// 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);
props => vdom
The golden rule:
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.
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
Remember: Server rendering is an optimization. Web crawlers run JS.
Write-once, Run Anywhere
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.
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>
)
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
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
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} />
);
}
}
So preact includes another trick.
(Similar add-on available for React)
class Foo extends Component {
render(props, state) {
return (
<input
value={state.text}
onInput={this.linkState('text')}
/>
);
}
}
linkState(key)
What's all this noise
about Virtual DOM?
Devs in your area speed up their
DOMs with this one simple trick!!
Teaser:
{
nodeName: 'div',
attributes: {
key: 'value'
},
children: [
'text node'
]
}
Element(
nodeName: 'div',
attributes: {
getItem('key'),
setItem('key', 'value')
},
childNodes: [
TextNode('text node')
]
)
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
Any time anything changes...
Just render it all again.
Things that didn't change are an empty diff.
Great artists steal, right?
Detour
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 });
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();
}
...for now.
Go build stuff and focus on what, not how.