You Don't Know React JS: ydk-reactjs
Unlearn and relearn the fundamentals - Preparing the mental model
We have been working and exploring React for quite a lot of time now and I thought of documenting React's JSX, the renderer and state here, so here we go!
Pre-requisite:
- Add typescript compiler and parcel in your project directory.
- enable
"jsx": "react",
intsconfig.json
. - Create a html and a JS file, add that JS file via
<script>
tags. - Please follow below steps sequentially with me to get a better understanding!
Okay, so we have a project directory with following structure, please follow this github repo URL: ydk-reactjs
Now lets try to implement our own version of React.createElement
, ReactDOM.renderer()
and some useState
!
A. Create your own version of React.createElement
and prepare the VDOM
:
In the index.tsx file, lets just add some JSX like syntax:
const a = <div>hello</div>
If you write this and due to the hot reloading of parcel
this change will be parcelified
and presented to you and notice in the browser console you will get some error:
Uncaught ReferenceError: React is not defined
Let's give it what it wants right? So lets just add an object with the name React
with a createElement
key inside it:
const React = {
createElement: () => {},
}
Okay, fine! now the errors are gone...but what does the createElement function has as arguments
, lets find out:
const React = {
createElement: (...args) => {
console.log(args); // (3)> ["div", null, "hello"] -->Note: this null is the props you pass
},
}
Now modify and lets add some stuff this:
const a =(
<div className="ydk-react">
<h1>Hello!</h1>
<p>How are you?</p>
</div>
);
Once this is done you should be getting a bunch of "undefined" from the console.log(args)
, so we actually need to return something from the createElement
function as we can see that it is getting called multiple times for each element, so obviously we need to construct and return an element similar to JSX:
const React = {
createElement: (tag, props, ...children) => { // children is an array
const element = { tag, props: {...props, children} }
console.log(element) // here we will get a tree, and what else is a tree -> a DOM is a tree -> so this our virtual DOM -> it is just an object, an object with children
return element
},
}
Congratulations! You have constructed the Virtual DOM!
So its all good but what we have is just in console logs, we need to put it on the screen and for that we need to implement the render method to render JSX!
B. Create the renderer:
So if we want to render it in the DOM, we need something which takes the virtual DOM tree and puts it in the DOM. For React apps, ReactDOM.render()
does the job and here we will be trying to create that.
To make const a = (....)
-> a react component, right now which is just a React element, we need to modify the code so that it becomes something like this which is much more
familiar to us:
const App = () => (
<div className="ydk-react">
<h1>Hello!</h1>
<p>How are you?</p>
</div>
);
<App />
And the createElement should now accept functions as well in tag
, so:
const React = {
createElement: (tag, props, ...children) => { // children is an array
if (typeof tag === "function") {
return tag(props);
}
const element = { tag, props: {...props, children} }
console.log(element)
return element
},
}
So by our visual knowledge, probably we are familiar with something like this: ReactDOM.render(<App />, doucment.getElementById('root'));
Lets create our own render method now!
render(<App />, doucment.getElementById('root'));
For obvious reasons, When we do this, in the browser console, we will get an error like this: ReferenceError: render is not defined
In the index.html, till now we only had: <script src="index.tsx"></script>
; lets add a div with id root alongside that: <div id="root"></div>
So here we need to map the virtualDOM element with the actual DOM right? This is what the render does ona high level, so:
const render = (reactElement, container) => {
const actualDomEle = document.createElement(reactElement.tag)
// Now take the props, apply them on this actual DOM element, and nest rest of the children
if (reactElement.props) {
Object.keys(reactElement.props)
// filter out children
.filter(p => p !=== "children")
// for each of them, apply them on the actual DOM element
.forEach(p => actualDomEle[p] = reactElement.props[p])
}
// for children, render recursively and continue same above process
if (reactElement.props.children) {
// Here render wach child inside the actualDomElement -> so now the container is actualDomEle
reactElement.props.children.forEach(child => render(child, actualDomEle));
}
// At the last step, append the root to the container
container.appendChild(actualDomEle);
}
Pro Note: Now the above might fail because there is one case which we didn't account for: It fails with
TypeError: cannot read property children of undefined
; because those items inside HTML tags like for eg: "Hello!", "How are you?" - they are not React elements, they are String primitives, so they obviously won't be having any children property in them!
So the reactElement
above can be a reactElement, a number or a string or anything!!
So we will be needing a check at the very first line of render that if it has a string or number, so need to add this at the very first line:
if (['string', 'number'].includes(typeof reactElement)) {
container.appendChild(doucument.createTextNode(String(reactElement)));
return;
}
Alright! We have our render!!!!
Now that is ok, but our App is stateless, lets add some state in it!
C. Create a State for your App using the approach of useState()
:
Lets just modify our App to a proper function component:
const App = () => {
const [name, setName] = useState("person")
return (
<div className="ydk-react">
<h1>Hello, {name}!</h1>
<input value={name} onChange={e => setName(e.target.value)} placeholder="name" /> // make onChange -> onchange
<p>How are you?</p>
</div>
)
};
This is not going to work because the actual W3C standards for event handlers is this: onchange
, but we are writing "onChange" -> which is React! For this session, we are going to use the W3C standards!
Now browser throws error: useState() is not defined
, lets write useState:
const useState = (initialState) => {
const state = initialState;
const setState = newState => state = newState;
return [state, setState];
}
Ok but does it work? does it update the person's name??? No!
Because for starters, the component must re-render when state is updated!
So let's just take our renderer, put it inside a function witrh a name rerender() and call it!
const rerender = () => {
document.querySelector.firstChild.remove("#root")
render(<App />, doucment.getElementById('root'));
};
And our useState becomes:
const useState = (initialState) => {
const state = initialState;
const setState = newState => {
state = newState;
rerender();
}
return [state, setState];
}
But it still does not work! Because if we console.log() the initialState and newState - we will see that the value is changing and immediately rebouncing to its old value.
To solve this problem we will be using the concept of closure. We need to put our main state in the global context of our particular file - something which is higher up that the invidual function scopes! Everytime we call useState, it is going to set a state in this array at index. We will take another global variable to track these array indexes.
let stateCursor = 0;
const states = [];
const useState = (initialState) => {
// freeze the index value with currentr scope
const cursorIdx = stateCursor;
states[cursorIdx] = states[cursorIdx] || initialState; // its wither the value stored in our gobal state or the initial state
const setState = newState => {
states[cursorIdx] = newState;
rerender();
}
//Move the stateCursor to next level
stateCursor++;
return [states[cursorIdx], setState];
}
But it still doesn't work, because on re-render, we need to reset the global stateCursor
. So the rerender becomes:
const rerender = () => {
stateCursor = 0;
document.querySelector.firstChild.remove("#root"); // to remove duplicate React elements
render(<App />, doucment.getElementById('root'));
};
Conclusion:
So that's it! The input update might be a bit behind because of the way inputs work and onchange reflects the change depending on our code. Special thanks to: Tejas Kumar, he is awesome!