Skip to content

Commit 2250724

Browse files
author
kamesh sethupathi
committed
first commit
0 parents  commit 2250724

File tree

8 files changed

+358
-0
lines changed

8 files changed

+358
-0
lines changed

.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/.vscode/
2+
/node_modules/
3+
/dist/
4+
/package-lock.json

README.md

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# kReact
2+
> Let's learn React by building react within 100 lines of code
3+
4+
### Introduction
5+
- kReact is built similar to react and mimics its internal working.
6+
- React is a great library that automates most of the DOM handling for us.
7+
- But most of us don't know how this magic automation happens in the backend 😰.
8+
- Let's us also know and learn the magic and build one for ourselves 🐱‍🏍.
9+
- The main objective of this project is to make you understand how React works internally.
10+
11+
### How to Use?
12+
- Clone this repository
13+
- Run `npm install` to install JS dependencies
14+
- Run `npm start`
15+
16+
## License
17+
This repository is available MIT license.

package.json

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "kReact",
3+
"version": "1.0.0",
4+
"description": "A mimic of react concepts",
5+
"main": "app.js",
6+
"scripts": {
7+
"start": "rollup -c --watch"
8+
},
9+
"keywords": [],
10+
"author": "",
11+
"license": "ISC",
12+
"dependencies": {
13+
"buble": "^0.20.0",
14+
"rollup": "^2.58.0",
15+
"rollup-plugin-buble": "^0.19.8",
16+
"rollup-plugin-livereload": "^2.0.5",
17+
"rollup-plugin-serve": "^1.1.0",
18+
"rollup-plugin-static-files": "^0.2.0"
19+
}
20+
}

public/index.html

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>kReact - a lightweight library which mimics React</title>
7+
</head>
8+
<body>
9+
<div id="app"></div>
10+
<script async src="/app.[hash].js"></script>
11+
</body>
12+
</html>

rollup.config.js

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import buble from 'rollup-plugin-buble';
2+
import static_files from 'rollup-plugin-static-files';
3+
import serve from 'rollup-plugin-serve';
4+
import livereload from 'rollup-plugin-livereload';
5+
6+
export default [{
7+
input: 'src/app.js',
8+
output: {
9+
dir: 'dist',
10+
format: 'esm',
11+
entryFileNames: '[name].[hash].js',
12+
assetFileNames: '[name].[hash][extname]',
13+
sourcemap: true
14+
},
15+
plugins: [
16+
buble({
17+
jsx: 'createElement',objectAssign: 'Object.assign'
18+
}),
19+
static_files({
20+
include: ['./public']
21+
}),
22+
serve({
23+
open: true,
24+
contentBase: 'dist'
25+
}),
26+
livereload()
27+
]
28+
}];

src/app.js

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { Component, render, createElement } from './kReact.js';
2+
3+
class ListItem extends Component {
4+
constructor(props) {
5+
super(props);
6+
}
7+
8+
componentWillUnmount() {
9+
console.log(this.props);
10+
}
11+
12+
render() {
13+
let { value, idx, handleRemove } = this.props;
14+
return (
15+
<li>
16+
{ value } - <span style="color: red;" onClick={()=> handleRemove(idx)}>REMOVE</span>
17+
</li>
18+
);
19+
}
20+
}
21+
22+
function TodoEditor(props) {
23+
return (
24+
<div class="todo-editor">
25+
<input ref={ props.refHandler }/>
26+
<button onClick={ props.addHandler }>Add Todo</button>
27+
</div>
28+
);
29+
}
30+
31+
class TodoList extends Component {
32+
constructor(props) {
33+
super(props);
34+
35+
this.state = {
36+
todos: ["Hello", "world"]
37+
};
38+
}
39+
40+
addTodo() {
41+
let todo = this.ipt.value;
42+
this.setState({ todos: [...this.state.todos, todo]});
43+
this.ipt.value = '';
44+
}
45+
46+
assignInputRef(ref) {
47+
this.ipt = ref;
48+
}
49+
50+
handleRemove(idx) {
51+
this.setState({ todos: this.state.todos.filter((v, i)=> i != idx) });
52+
}
53+
54+
render() {
55+
return (
56+
<div>
57+
<h1>My Todo List</h1>
58+
<TodoEditor addHandler={this.addTodo.bind(this)} refHandler={this.assignInputRef.bind(this)} />
59+
<ul>
60+
{ this.state.todos.map((item, idx)=> <ListItem value={item} idx={idx} handleRemove={ this.handleRemove.bind(this) } />)}
61+
</ul>
62+
</div>
63+
);
64+
}
65+
}
66+
67+
render(<TodoList />, document.querySelector('#app') );

src/backup.js

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
export class Component {
2+
constructor( props = {}) {
3+
this.props = props;
4+
this.state = null;
5+
}
6+
7+
setState(nextState) {
8+
const isCompat = isObject(this.state) && isObject(nextState);
9+
const commitState = ()=> isCompat? Object.assign({}, this.state, nextState) : nextState;
10+
const prevState = isObject(this.state)? Object.assign({}, this.state) : this.state;
11+
12+
if( runHook(this, 'shouldComponentUpdate') && this.base ) {
13+
runHook(this, 'componentWillUpdate', this.props, nextState);
14+
commitState();
15+
patch(this.base, this.render());
16+
runHook(this, 'componentDidUpdate', this.props, prevState);
17+
} else {
18+
commitState();
19+
}
20+
}
21+
22+
static render(vnode, parent) {
23+
if( isClassComponent(vnode) ) {
24+
let instance = new vnode.type( vnode.props );
25+
runHook(instance, 'componentWillMount');
26+
instance.base = render( instance.render(), parent);
27+
instance.base.instance = instance;
28+
runHook(instance, 'componentDidMount');
29+
return instance.base;
30+
} else {
31+
return render( vnode.type( vnode.props, parent) );
32+
}
33+
}
34+
35+
static patch(dom, vdom, parent=dom.parentNode) {
36+
const props = Object.assign({}, vdom.props, {children: vdom.children});
37+
if (dom.instance && dom.instance.constructor == vdom.type) {
38+
runHook(dom.instance, 'componentWillReceiveProps', props);
39+
dom.instance.props = props;
40+
return patch(dom, dom.instance.render(), parent);
41+
} else if ( isClassComponent(vnode.type) ) {
42+
const newdom = Component.render(vdom, parent);
43+
return parent ? (replace(newdom, dom, parent) && newdom) : (newdom);
44+
} else if ( !isClassComponent(vnode.type) ) {
45+
return patch(dom, vdom.type(props), parent);
46+
}
47+
}
48+
}
49+
50+
export const createElement = (type, props, ...children ) => ({ type, props: props || {}, children });
51+
52+
// render( vnode, parent ):
53+
// 1: Vode --> DOMNode
54+
// 2: DOMNode --> Mount to parent
55+
export function render(vnode, parent) {
56+
let dom, { type, props, children } = vnode;
57+
58+
if( isObject(vnode) ) {
59+
dom = isFunction(type) ? Component.render(vnode, parent) : document.createElement( type );
60+
children.flat(1).map((child)=> render(child, dom));
61+
Object.keys(props).map((key)=> setAttribute(dom, key, props[key]));
62+
} else {
63+
dom = document.createTextNode(vnode || '');
64+
}
65+
66+
return mount( dom, parent );
67+
}
68+
69+
function patch(dom, vnode, parent=dom.parentNode) {
70+
if( (!isObject(vnode) || isTextNode(dom)) ) {
71+
return dom.textContent != vnode ? replace( render(vnode), dom, parent ) : dom;
72+
} else if( isObject(vnode) ) {
73+
if( isFunction( vnode.type ) ) return Component.patch( dom, vnode, parent );
74+
else {
75+
let dom_map = Array.from(dom.childNodes).reduce((prev, cur, idx)=> ({...prev, [cur._idx || `__${idx}`]: cur}), {});
76+
vnode.children.flat(1).map((child, idx)=> {
77+
let key = (child.props && child.props.key) || `__${idx}`;
78+
mount( dom_map[key]? patch( dom_map[key], child ) : render(child), dom);
79+
delete dom_map[key];
80+
});
81+
Object.keys(dom_map).map((key)=> {
82+
runHook( dom_map[key].instance, 'componentWillUnmount');
83+
dom_map[key].remove();
84+
});
85+
return dom;
86+
}
87+
}
88+
}
89+
90+
function setAttribute(dom, key, value) {
91+
if( key.startsWith('on') && isFunction(value) ) delegateEvent(dom, key, value);
92+
else if( key == 'ref' && isFunction( value ) ) value( dom );
93+
else if( ['checked', 'value', 'className', 'key'].includes(key) ) dom[key=='key'? '_idx' :key] = value;
94+
else dom.setAttribute(key, value);
95+
}
96+
97+
// Utils
98+
const isFunction = ( node ) => typeof node == 'function';
99+
const isObject = ( node ) => typeof node == 'object';
100+
const isTextNode = ( node ) => node.nodeType == 3;
101+
const replace = (el, dom, parent)=> (parent && parent.replaceChild(el, dom) && el);
102+
const mount = (el, parent)=> parent? parent.appendChild( el ) : el;
103+
const isClassComponent = ( node ) => Component.isPrototypeOf( node.type );
104+
const runHook = (instance, hook, ...args) => isFunction(instance && instance[hook]) ? instance[hook]( ...args) : true;
105+
const delegateEvent = (dom, event, handler)=> {
106+
event = event.slice(2).toLowerCase();
107+
dom._evnt = dom._evnt || {};
108+
dom.removeEventListener(event, dom._evnt[ event ]);
109+
dom.addEventListener(event, dom._evnt[ event ] = handler);
110+
}

src/kReact.js

+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
export class Component {
2+
constructor( props = {}) {
3+
this.props = props;
4+
this.state = null;
5+
}
6+
7+
setState(nextState) {
8+
const isCompat = isObject(this.state) && isObject(nextState);
9+
const commitState = ()=> this.state = isCompat? Object.assign({}, this.state, nextState) : nextState;
10+
const prevState = isObject(this.state)? Object.assign({}, this.state) : this.state;
11+
12+
if( runHook(this, 'shouldComponentUpdate') && this.base ) {
13+
runHook(this, 'componentWillUpdate', this.props, nextState);
14+
commitState();
15+
patch(this.base, this.render());
16+
runHook(this, 'componentDidUpdate', this.props, prevState);
17+
} else commitState();
18+
}
19+
20+
static render(vnode, parent) {
21+
if( isClassComponent(vnode) ) {
22+
let instance = new vnode.type( combineChildrenWithProps( vnode ) );
23+
runHook(instance, 'componentWillMount');
24+
instance.base = render( instance.render(), parent);
25+
instance.base.instance = instance;
26+
runHook(instance, 'componentDidMount');
27+
return instance.base;
28+
} else return render( vnode.type(combineChildrenWithProps( vnode )), parent );
29+
}
30+
31+
static patch(dom, vnode, parent=dom.parentNode) {
32+
if (dom.instance && dom.instance.constructor == vnode.type) {
33+
runHook(dom.instance, 'componentWillReceiveProps', combineChildrenWithProps( vnode ) );
34+
dom.instance.props = combineChildrenWithProps( vnode );
35+
return patch(dom, dom.instance.render(), parent);
36+
} else if ( isClassComponent(vnode.type) ) {
37+
const newdom = Component.render(vnode, parent);
38+
return parent ? (replace(newdom, dom, parent) && newdom) : (newdom);
39+
} else if ( !isClassComponent(vnode.type) ) return patch(dom, vnode.type( combineChildrenWithProps( vnode ) ), parent);
40+
}
41+
}
42+
43+
export const createElement = (type, props, ...children ) => ({ type, props: props || {}, children });
44+
45+
export function render(vnode, parent) {
46+
if( isObject(vnode) ) {
47+
let dom = isFunction(vnode.type) ? Component.render(vnode, parent) : document.createElement( vnode.type );
48+
vnode.children.flat(1).map((child)=> render(child, dom));
49+
!isFunction(vnode.type) && Object.keys(vnode.props).map((key)=> setAttribute(dom, key, vnode.props[key]));
50+
return mount( dom, parent );
51+
} else return mount( document.createTextNode(vnode || ''), parent );
52+
}
53+
54+
function patch(dom, vnode, parent=dom.parentNode) {
55+
if( isObject(vnode) ) {
56+
if( isTextNode(dom) ) return replace( render(vnode, parent), dom, parent );
57+
else if( isFunction(vnode.type) ) return Component.patch( dom, vnode, parent);
58+
else {
59+
let dom_map = Array.from(dom.childNodes) // Build a key value map to identify dom-node to its equivalent vnode
60+
.reduce((prev, node, idx)=> ({...prev, [node._idx || `__${idx}`]: node}), {});
61+
62+
vnode.children.flat(1).map((child, idx)=> {
63+
let key = (child.props && child.props.key) || `__${idx}`;
64+
mount( dom_map[key]? patch(dom_map[key], child, dom) : render(child, dom) );
65+
delete dom_map[key]; // marks dom-vnode pair available by removing from map
66+
});
67+
68+
Object.values(dom_map).forEach(element => { // Unmount DOM nodes which are missing in the latest vnodes
69+
runHook( element.instance, 'componentWillUnmount');
70+
element.remove();
71+
});
72+
73+
!isFunction(vnode.type) && Object.keys(vnode.props).map((key)=> setAttribute(dom, key, vnode.props[key]));
74+
}
75+
}
76+
else if( isTextNode(dom) && dom.textContent != vnode ) return replace( render(vnode, parent), dom, parent );
77+
}
78+
79+
function setAttribute(dom, key, value) {
80+
if( key.startsWith('on') && isFunction(value) ) delegateEvent(dom, key, value);
81+
else if( key == 'ref' && isFunction( value ) ) value( dom );
82+
else if( ['checked', 'value', 'className', 'key'].includes(key) ) dom[key=='key'? '_idx' :key] = value;
83+
else dom.setAttribute(key, value);
84+
}
85+
86+
// Utils
87+
const isFunction = ( node ) => typeof node == 'function';
88+
const isObject = ( node ) => typeof node == 'object';
89+
const isTextNode = ( node ) => node.nodeType == 3;
90+
const replace = (el, dom, parent)=> (parent && parent.replaceChild(el, dom) && el);
91+
const mount = (el, parent)=> parent? parent.appendChild( el ) : el;
92+
const isClassComponent = ( node ) => Component.isPrototypeOf( node.type );
93+
const runHook = (instance, hook, ...args) => isFunction(instance && instance[hook]) ? instance[hook]( ...args) : true;
94+
const delegateEvent = (dom, event, handler)=> {
95+
event = event.slice(2).toLowerCase();
96+
dom._evnt = dom._evnt || {};
97+
dom.removeEventListener(event, dom._evnt[ event ]);
98+
dom.addEventListener(event, dom._evnt[ event ] = handler);
99+
}
100+
const combineChildrenWithProps = ({ props, children })=> Object.assign({}, props, { children });

0 commit comments

Comments
 (0)