├── .gitignore ├── .babelrc ├── .editorconfig ├── index.html ├── src ├── counter.js ├── index.js ├── qnd-react-dom.js └── qnd-react.js ├── webpack.dev.js ├── README.md ├── webpack.prod.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | /node_modules 3 | dist 4 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | ["@babel/plugin-transform-react-jsx", { 4 | "pragma": "QndReact.createElement", // default pragma is React.createElement 5 | "throwIfNamespace": false // defaults to true 6 | }] 7 | ] 8 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | QndReact 7 | 8 | 9 |
10 |

Hello World from webpack starter pack

11 |

12 | This is a very basic webpack setup with just ES6 support and everything else is left upto your imagination 13 |

14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /src/counter.js: -------------------------------------------------------------------------------- 1 | import QndReact from './qnd-react'; 2 | 3 | export default class Counter extends QndReact.Component { 4 | constructor(props) { 5 | super(props); 6 | 7 | this.state = { 8 | count: 0 9 | } 10 | } 11 | 12 | componentDidMount() { 13 | console.log('Component mounted'); 14 | } 15 | 16 | render() { 17 | return ( 18 |
19 |

Count: {this.state.count}

20 | 23 |
24 | ) 25 | } 26 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // file: src/index.js 2 | // QndReact needs to be in scope for JSX to work 3 | import QndReact from "./qnd-react"; 4 | import QndReactDom from "./qnd-react-dom"; 5 | import Counter from "./counter"; 6 | 7 | // functional component to welcome someone 8 | const Greeting = ({ name }) =>

Welcome {name}!

; 9 | const foods = [ 10 | 'idly', 11 | 'dosa', 12 | 'vada' 13 | ] 14 | 15 | const App = ( 16 |
17 |

18 | QndReact is Quick and dirty react 19 |

20 |

It is about building your own React in 90 lines of JavsScript

21 | 22 | 23 |

The following renders a list of food

24 |
    25 | {foods.map(food =>
  • {food}
  • )} 26 |
27 |
28 | ); 29 | 30 | QndReactDom.render(App, document.getElementById("root")); -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | devtool: 'eval-cheap-module-source-map', 6 | entry: './src/index.js', 7 | devServer: { 8 | port: 3000, 9 | contentBase: path.join(__dirname, "dist") 10 | }, 11 | node: { 12 | fs: 'empty' 13 | }, 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.js$/, 18 | exclude: /node_modules/, 19 | loader: 'babel-loader', 20 | options: { 21 | presets: ['@babel/preset-env'] 22 | } 23 | } 24 | ] 25 | }, 26 | plugins: [ 27 | new HtmlWebpackPlugin({ 28 | template: './index.html', 29 | inject: true 30 | }) 31 | ] 32 | }; 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quick and Dirty React :hammer: 2 | 3 | This is my quick and dirty implementation of react to help myself and others to understand what react does under the hood 4 | 5 | This goes along well with the article [Build your own React in 90 lines of JavaScript 6 | ](https://dev.to/ameerthehacker/build-your-own-react-in-90-lines-of-javascript-1je2) 7 | 8 | * The replication of ReactDom is available in __src/qnd-react-dom.js__ 9 | * The replication of React is available in __src/qnd-react.js__ 10 | 11 | ## How to run it? 12 | 13 | 1. Clone the repo 14 | 2. Install the dependencies 15 | 16 | ```sh 17 | npm install 18 | ``` 19 | 20 | 3. Run the sample project using QndReact.js :heart: 21 | 22 | ```sh 23 | npm start 24 | ``` 25 | 26 | ## Found any issue? 27 | 28 | Please feel free to raise an issue or PR :wink: 29 | 30 | Show your support by :star: the repo 31 | 32 | ## License 33 | 34 | MIT © [Ameer Jhan](mailto:ameerjhanprof@gmail.com) -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | 5 | const buildPath = path.resolve(__dirname, 'dist'); 6 | 7 | module.exports = { 8 | devtool: 'source-map', 9 | entry: './src/index.js', 10 | output: { 11 | filename: '[name].[hash:20].js', 12 | path: buildPath 13 | }, 14 | node: { 15 | fs: 'empty' 16 | }, 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.js$/, 21 | exclude: /node_modules/, 22 | loader: 'babel-loader', 23 | options: { 24 | presets: ['@babel/preset-env'] 25 | } 26 | } 27 | ] 28 | }, 29 | plugins: [ 30 | new HtmlWebpackPlugin({ 31 | template: './index.html', 32 | // Inject the js bundle at the end of the body of the given template 33 | inject: 'body', 34 | }) 35 | ] 36 | }; 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-starter-pack", 3 | "version": "0.0.1", 4 | "description": "This is a very basic webpack setup with just ES6 support and everything else is left upto your creativity", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "webpack-dev-server --config webpack.dev.js --mode development", 9 | "build": "webpack --config webpack.prod.js --mode production", 10 | "preview": "npm run build && http-server dist", 11 | "kickstart": "node kickstarter" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/ameerthehacker/webpack-starter-pack.git" 16 | }, 17 | "keywords": [ 18 | "react" 19 | ], 20 | "author": "ameerthehacker", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/ameerthehacker/webpack-starter-pack/issues" 24 | }, 25 | "homepage": "https://github.com/ameerthehacker/webpack-starter-pack#readme", 26 | "dependencies": { 27 | "normalize.css": "^8.0.0", 28 | "snabbdom": "^0.7.3" 29 | }, 30 | "devDependencies": { 31 | "@babel/core": "^7.6.4", 32 | "@babel/plugin-transform-react-jsx": "^7.3.0", 33 | "@babel/preset-env": "^7.6.3", 34 | "babel-loader": "^8.0.6", 35 | "html-webpack-plugin": "^3.1.0", 36 | "http-server": "^0.11.1", 37 | "rimraf": "^2.6.2", 38 | "source-map-loader": "^0.2.3", 39 | "webpack": "^4.20.2", 40 | "webpack-cli": "^3.1.1", 41 | "webpack-dev-server": "^3.1.11" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/qnd-react-dom.js: -------------------------------------------------------------------------------- 1 | // file: src/qnd-react-dom.js 2 | import * as snabbdom from 'snabbdom'; 3 | import propsModule from 'snabbdom/modules/props'; 4 | import eventlistenersModule from 'snabbdom/modules/eventlisteners'; 5 | import QndReact from './qnd-react'; 6 | 7 | // propsModule -> this helps in patching text attributes 8 | // eventlistenersModule -> this helps in patching event attributes 9 | const reconcile = snabbdom.init([propsModule, eventlistenersModule]); 10 | // we need to maintain the latest rootVNode returned by render 11 | let rootVNode; 12 | 13 | // React.render(, document.getElementById('root')); 14 | // el -> 15 | // rootDomElement -> document.getElementById('root') 16 | const render = (el, rootDomElement) => { 17 | // logic to put el into the rootDomElement 18 | // ie. QndReactDom.render(, document.getElementById('root')); 19 | // happens when we call render for the first time 20 | if (rootVNode == null) { 21 | rootVNode = rootDomElement; 22 | } 23 | 24 | // remember the VNode that reconcile returns 25 | rootVNode = reconcile(rootVNode, el); 26 | } 27 | 28 | // QndReactDom telling React how to update DOM 29 | QndReact.__updater = (componentInstance) => { 30 | // logic on how to update the DOM when you call this.setState 31 | 32 | // get the oldVNode stored in __vNode 33 | const oldVNode = componentInstance.__vNode; 34 | // find the updated DOM node by calling the render method 35 | const newVNode = componentInstance.render(); 36 | 37 | // update the __vNode property with updated __vNode 38 | componentInstance.__vNode = reconcile(oldVNode, newVNode); 39 | } 40 | 41 | // to be exported like ReactDom.render 42 | const QndReactDom = { 43 | render 44 | }; 45 | 46 | export default QndReactDom; -------------------------------------------------------------------------------- /src/qnd-react.js: -------------------------------------------------------------------------------- 1 | // file: src/qnd-react.js 2 | import { h } from 'snabbdom'; 3 | 4 | const createElement = (type, props = {}, ...children) => { 5 | // flatten the children 6 | // this to make todos.map(todo =>

{todo}

) work in jsx 7 | // [['idly'], ['dosa', 'vada']] -> ['idly', 'dosa', 'vada'] 8 | children = children.flat(); 9 | 10 | // if type is a Class then 11 | // 1. create a instance of the Class 12 | // 2. call the render method on the Class instance 13 | if (type.prototype && type.prototype.isQndReactClassComponent) { 14 | const componentInstance = new type(props); 15 | 16 | // remember the current vNode instance 17 | componentInstance.__vNode = componentInstance.render(); 18 | 19 | // add hook to snabbdom virtual node to know whether it was added to the actual DOM 20 | componentInstance.__vNode.data.hook = { 21 | create: () => { 22 | componentInstance.componentDidMount() 23 | } 24 | } 25 | 26 | return componentInstance.__vNode; 27 | } 28 | // if type is a function then call it and return it's value 29 | if (typeof (type) == 'function') { 30 | return type(props); 31 | } 32 | 33 | props = props || {}; 34 | let dataProps = {}; 35 | let eventProps = {}; 36 | 37 | // This is to seperate out the text attributes and event listener attributes 38 | for(let propKey in props) { 39 | // event props always startwith on eg. onClick, onChange etc. 40 | if (propKey.startsWith('on')) { 41 | // onClick -> click 42 | const event = propKey.substring(2).toLowerCase(); 43 | 44 | eventProps[event] = props[propKey]; 45 | } 46 | else { 47 | dataProps[propKey] = props[propKey]; 48 | } 49 | } 50 | 51 | // props -> snabbdom's internal text attributes 52 | // on -> snabbdom's internal event listeners attributes 53 | return h(type, { props: dataProps, on: eventProps }, children); 54 | }; 55 | 56 | // component base class 57 | class Component { 58 | constructor() { } 59 | 60 | componentDidMount() { } 61 | 62 | setState(partialState) { 63 | // update the state by adding the partial state 64 | this.state = { 65 | ...this.state, 66 | ...partialState 67 | } 68 | // call the __updater function that QndReactDom gave 69 | QndReact.__updater(this); 70 | } 71 | 72 | render() { } 73 | } 74 | 75 | // add a static property to differentiate between a class and a function 76 | Component.prototype.isQndReactClassComponent = true; 77 | 78 | // to be exported like React.createElement, React.Component 79 | const QndReact = { 80 | createElement, 81 | Component 82 | }; 83 | 84 | export default QndReact; 85 | --------------------------------------------------------------------------------