How to write
your React
why that should not be done
the author


My way
idea and not a solution

prologue
frontend fears

Common
web applications

Architecture
solve your problem
web components
twig templates
component tree

Component
-
template.twig
-
index.js
-
style.less
// components/ComponentName/index.js
import './style.less';
import Component from 'component-vdom/Component';
class ComponentName extends Component {}
{% component 'name' with {param: 'value'} %}
{% component 'name' embed %}
{% block name %}
{% endblock %}
{% endcomponent %}
Component
{% set ui = ui|default({}) %}
{% set ui = ui|merge({size: 'normal'}) %}
{% set ui = ui|merge({type: 'text'}) %}
<div class="field-text">
<input class="field-text__input
field-text__input_size_{{ ui.size|default('normal') }}
"
type="{{ ui.type|default('text') }}"
{% if data.id %} id="{{ data.id }}" {% endif %}
name="{{ data.name }}"
{% if ui.placeholder %}
placeholder="{{ ui.placeholder|e }}"
{% endif %}
value="{{ data.value|e }}"
>
</div>
awesome
-
just working
-
Backend can edit
-
SSR
Is growing
dynamics
problems
-
every reload page :(
-
rerender
-
reinit component
Hybrid
web applications

It is necessary
to rewrite all!
Between
SPA and common

Idea
maeta language
Works everywhere
SSR
It is good ?
virtual dom
Document
Object Model

Virtual DOM

virtual dom
-
vnode
-
vtree
-
vdom
-
VNode
-
VText
virtual-hyperscript
var h = require('virtual-dom/h')
var tree = h('div.foo#some-id', [
h('span', 'some text'),
h('input', { type: 'text', value: 'foo' })
])
Widget
var h = require("virtual-dom").h;
var createElement = require("virtual-dom").create;
class Widget {
get type() {
return 'Widget';
}
constructor() {}
init() {
return createElement(h("div", "Count is: " + this.count))
}
update(previous, domNode) {}
destroy(domNode) {}
}
Hook
export default class Hook {
constructor () {}
hook (node, propertyName, previousValue) {}
unhook () {}
}
Update
var diff = require("virtual-dom").diff
var patch = require("virtual-dom").patch
var myCounter = new OddCounterWidget()
var currentNode = myCounter
var rootNode = createElement(currentNode)
// A simple function to diff your widgets, and patch the dom
var update = function(nextNode) {
var patches = diff(currentNode, nextNode)
rootNode = patch(rootNode, patches)
currentNode = nextNode
}
document.body.appendChild(rootNode)
setInterval(function(){
update(new OddCounterWidget())
}, 1000)
Problems
-
every reload page :( -
rerender -
reinit component
proof of concept

Plan
profit
twig -> vdom
Augerening
function (data) {
let html = '<input value="';
html+= twig
.getData('data.value')
.filter(twig.filter.e)
.raw();
html+= '">';
return html;
}
<input value="{{ data.value|e }}">
function (data) {
return h('input',{
value: twig
.getData('data.value')
.filter(twig.filter.e)
.raw()
});
}
Reality
twig({ id: "template.twig",
data: [{
"type": "raw", "value": "<input value=\""
}, {
"type": "output",
"stack": [{
"type": "Twig.expression.type.variable",
"value": "data",
"match": ["data"]
},{
"type": "Twig.expression.type.filter",
"value": "e",
"match": ["|e", "e"]
}]
},
{
"type": "raw", "value": "\">"
}],
});
<input value="{{ data.value|e }}">
Old Plan
twig -> fixed twig -> html with data -> vdom -> ... profit
twig -> vdom -> profit
new Plan
problems
-
save twig component
-
parse html to vdom
-
vdom witch component
Extending Twig
<div id="root">
{% component 'entry/Main' with {data: userData} only %}
</div>
Extending parser
Twig.exports.extendTag({
type: 'component',
regex: /^component\s+(ignore missing\s+)?(.+?)\s*(?:with\s+([\S\s]+?))?\s*(only)?$/,
next: [],
open: true,
parse: function (token, context, chain) {
var data = encodeURI(JSON.stringify(innerContext));
var str = '<component name="' + file + '" data="' + data + '"></component>';
return {
chain: chain,
output: str
};
}
});
Base class
import BaseComponent from 'component-vdom/src/widget/Component';
export default class Component extends BaseComponent {
getComponentCallback (name) {
return require('components/' + name + '/index.js').default;
}
}
import template from './template.twig';
import Component from 'components/Component';
export default class FieldText extends Component {
get template () {
return template;
}
}
VNode component
function (getComponentCallback) {
return (tagName, properties, children, key, namespace) => {
if (tagName === 'component') {
let data = JSON.parse(decodeURI(properties.data));
let Component = getComponentCallback(properties.name);
return new Component(data, children);
}
return new VNode(tagName, properties, children, key, namespace);
}
}
problems
-
save twig component -
parse html to vdom -
vdom witch component
Features

// TODO:
-
ref node
-
onClick="function"
-
bind event selector
-
onMount, onUnMount
-
children
-
redux
ref node
export const isRef = (key) => 'ref' === key;
export default (vnode, component, first = false) => {
if (hashAttributes(vnode)) {
const attributes = Object.keys(vnode.properties.attributes);
attributes
.filter(isRef)
.forEach((attr) => {
const refName = vnode.properties.attributes[attr];
vnode.properties.ref = new HookRef(component, refName);
});
}
}
class HookRef extends Hook {
constructor (component, refName) {
super();
this.component = component;
this.refName = refName;
}
hook (node) {
this.component.refs[this.refName] = node;
}
}
onClick="function"
export const isEventNamedAttr = (key) => key.startsWith('on');
export default (vnode, component, first = false) => {
if (hashAttributes(vnode)) {
const attributes = Object.keys(vnode.properties.attributes);
attributes
.filter(isEventNamedAttr)
.forEach((attr) => {
const functionName = vnode.properties.attributes[attr];
if (component[functionName]) {
vnode.properties[attr] = component[functionName]
.bind(component, data);
}
});
}
}
onMount, onUnMount
import HookMount from '../hook/Mount';
export default (vnode, component, first = false) => {
if (first) {
vnode.properties.hookMount = new HookMount(component);
}
}
import nextTick from 'next-tick';
export default class HookMount extends Hook {
hook (node) {
nextTick(this.component.onMount.bind(this.component, node));
}
unhook () {
this.component.onUnMount.bind(this.component);
}
}
children
Redux
import App from 'component-vdom/src/App';
import EntryMain from 'components/entry/Main';
import {createStore} from 'redux';
const initialState = {value: 0};
const reducers = (state = initialState, action) => {
switch (action.type) {
case 'INCREMENT':
return {value: value+1}
case 'DECREMENT':
return {value: value-1}
default:
return state
}
};
let store = createStore(reducers);
let app = new App(initialState, EntryMain);
app.appendTo(document.querySelector('#root'));
store.subscribe(() => {
app.update(store.getState());
});
Redux
{# Main/template.twig #}
<div class="entry-main">
Clicked: {{value}} times
<button onClick="increment">+</button>
<button onClick="decrement">-</button>
</div>
export default class EntryMain extends Component {
get template () {
return template;
}
increment() {
store.dispatch({ type: 'INCREMENT' })
}
decrement() {
store.dispatch({ type: 'DECREMENT' })
}
}
prologue
A few words about all

-
~ 30 lib
-
rewrite twig & twig-loader
-
~50 issues & pull request
-
html-to-vdom
-
main-loop
-
next-tick
-
virtual-dom
-
virtual-html
-
vtree-select
-
twig
-
twig-loader
frontend fears
open source
Thank you!
waiting for your questions
How to write your React - why that should not be done
By Sergey Andreev
How to write your React - why that should not be done
- 1,710