π Introduction
π·ββοΈ Tasks
Generally, when we speak about React we talk about both React and ReactDOM. Prior to v0.14, all ReactDOM functionality was part of the React package. This may be a source of confusion, since older documentation won't mention the distinction between the React and ReactDOM packages.
ReactDOM is the glue between React and the DOM.
When you want to show your React application you need to use ReactDOM.render()
from the ReactDOM package.
This package include the reconciliation algorithm and platform-specific code β also known as renderers.
React β often referred to as React core and includes the top-level React APIs. It only includes the APIs necessary to define components:Β the component base class, lifecycle functions, state, props and all the concepts we know and love.
React elements are the building blocks of React applications. React elements might be confused with the concept of
React components. To clarify, React elements are generally what gets rendered on the screen, i.e.Β the return value of
the render()
function of a React component or the return of a functional component.
const element = <p>I'm an element</p>;
React was originally created for the DOM, but the concept of renderers was introduced to support native platforms like React Native. A renderer is responsible for turning a tree of React elements into the underlying platform. In other words, if we want to support another platform all we need is a new renderer.
In this workshop we are going to create a renderer that renders React components to the DOM, just like ReactDOM.
Different renderers, such as ReactDOM and React Native, share a lot of logic. Rendering, custom components, state, lifecycle functions and refs should work consistently across platforms.
When you use React you can think of the render()
function as creating a tree of React elements. If props or state is
changed, the render()
function might return a different tree. The reconciler then needs to figure out how to
effectively update the UI to match the most recent tree with the minimum number of operations required.
If you want to learn more about this, the React documentation contains an article that explains the choices made in React's diffing algorithm.
First of all, run npm install
We have provided you with tests for each task. We urge you to use these and run them after each task to verify your implementation or to point you in the right direction.
To run the tests for a specific task, you can simply specify the task (in this case task 1):
npm run test1
To run tests for task 2, just replace test1
with test2
, and so on.
To run all tests:
npm run test
Note that these test scripts will also run the tests for all the previous tasks. This way you can be sure you don't break anything in the process.
In addition to the tests, you can edit src/index.js
to play with your implementation.
To run the code:
npm start
The dev server should now be running on http://localhost:1234
We have provided you with e examples you can use in src/examples
To run an example:
- Change directory to the example
cd src/examples/<the example you want to test>
- Install and run the example with
npm
For instance, if you want to test the todo-example
cd src/examples/todo
npm install
npm start
If you've already looked in the /react-dom
directory or /react
directory, you might have noticed that they
are not empty.
We've taken the liberty of implementing a skeleton of empty functions for you to implement.
To stay true to the virtual-DOM mindset you will find VCompositeNode.js
and VDomNode.js
in the react-dom
directory. VDomNode.js
is a "virtual" DOM-node, while the VCompositeNode
represents a "virtual" react-component node.
Everything that can be represented in the DOM, such as a number
, string
, div
, a
, p
etc. should be a
VDomNode
. Everything else, and by that we mean stateful- or stateless-components should be a VCompositeNode
.
These "virtual" nodes can have children, which again are "virtual" nodes. This means that we get a tree-structure of nodes known as the "virtual DOM". The "virtual DOM" that we are about to implement is pretty naive. Nevertheless, the structure is there to make it easier to extend with a more advanced reconciliation algorithm that can just render portions of a sub-tree instead of rendering the whole tree every time.
Time to get your hands dirty.
To make your life easier, we have used emojis to mark important content:
π - A task.
π‘ - Tips and helpful information to solve a specific task.
π₯ - An extra task if you're up for it.
π - Some extended information you might check out some other time.
π - We'll keep on reminding you to run the tests.
createElement
creates and returns a new React element of a given type. The function signature of createElement
takes three arguments:
type
- the type of the element we are creating. This can be either be an HTML element or a React component. If we are creating an HTML element, the name of the element (div
,p
etc.) is passed as a string. If we are creating a React component, the variable that the component is assigned to is passed as the value.props
- An object containing the properties (props
) that get passed to the component.children
- The children of the component. You can pass as many children as you want.
React.createElement(type, props, ...children);
The function returns an object like the one below.
{
type: 'div',
props: {
className: 'my-class',
randomProp: 'randomValue',
children: [{
type: 'button',
props: { className: 'blue' }
}, {
type: 'button',
props: { className: 'red' }
}]
},
$$typeof: Symbol.for("react.element"),
ref: null,
_owner: null
}
π Implement the createElement
function in the file named react/index.js
π‘ Unfamiliar with React.createElement()
? Code written with JSX will be converted to use React.createElement(). You will not typically invoke React.createElement() directly if you are using JSX.
π‘ We use the rest operator ...children
to handle several children. However, if the app code specifies children as an array, the rest operator will still wrap the argument in an array.
When this is the case you need to flatten the array (requires polyfill for IE/Edge).
π In this workshop, we won't make use of $$typeof
, ref
or _owner
, but do take a look at this blog post for details about what $$typeof
is. Essentially it is to protect
against XSS-attacks.
π It's time to run some tests. If you haven't already, run npm install
first. Then run npm run test1
.
Time to render our newly created React element!
React elements can be of different types (HTML elements, React components or primitive types like number
and string
), specified by the type
value in our newly created object. Let's start with the HTML elements.
The specific HTML element we are going to render is specified by the type
value of the React element with a string
. HTML elements are the only type of React elements that are specified by a string.
The following call to ReactDOM.render()
...
ReactDOM.render(
React.createElement('div', {}),
document.getElementById('root')
);
...should result in a div
element within our root element.
<div id="root">
<div></div>
</div>
π Create a new HTML node and append it to the DOM. Write your code in /react-dom
.
To complete our task, we need to:
-
return a
new VDomNode(reactElement)
from theinstantiateVNode
function inreact-dom/index.js
. -
In
render
, we instantiate our virtual node with our reactElement by callinginstantiateVNode(reactElement)
. Store it in a variable namedvNode
.
Now we need to mount (create a DOM-element) for our virtual node and append it to the DOM.
-
In
render
we need to mount our virtual node by calling the mount method on the virtual node.vNode.mount()
-
Append the result of the mount method to the
domContainerNode
.
π‘ Node.appendChild() function adds a node to the end of the list of children of a specified parent node.
Remember to also implement the constructor
and mount
in VDomNode
:
-
The
constructor
need to set thereactElement
-argument as a class-property. For instance like this -
mount
has to create a DOM-element from thereactElement
class-property and return it.
π‘ document.createElement() can be used to create HTML elements.
π Remember to run the tests npm run test2
π
Great, we are now able to create one HTML element! In order to render more than one element we need to handle children.
To do so we have to extend the mount()
function in VDomNode.js
to iterate over possible children:
The following call to ReactDOM.render()
..
ReactDOM.render(
React.createElement('div', {}, React.createElement('div', {})),
document.getElementById('root')
);
..should result in two nested div
elements within our root element.
<div id="root">
<div>
<div></div>
</div>
</div>
π Extend the mount
function in VDomNode.js
to support children.
- Get
props.children
of thereactElement
and map the children toinstantiateVNode
, which will create virtual DOM-nodes.
π‘ You can use this util method to get the children as an array from the props
function getChildrenAsArray(props) {
const { children = [] } = props || {};
return Array.isArray(children) ? children : [children];
}
- Iterate over the array of virtual child nodes, mount each of the virtual child nodes with the
.mount()
and useappendChild
to append the result ofmount
to the element you created in the previous task.
π Third time's the charm, run those tests! npm run test3
Your next task is to handle primitive types like number
and string
, as well as empty elements.
Unlike HTML elements and React components, primitive types and empty elements are not represented as a standard React element.
Moreover, they are not represented as an object with a type
field. Instead, they are represented as their own value.
Because of this primitive types and empty elements are always leaf nodes (i.e. children of another React element).
The following call to ReactDOM.render()
...
ReactDOM.render(
React.createElement('div', {}, 'Hello world!'),
document.getElementById('root')
);
...should result in a div
element with the text Hello world!
inside it.
<div id="root">
<div>
Hello world!
</div>
</div>
...while...
ReactDOM.render(
React.createElement('div', {}, null),
document.getElementById('root')
);
...should result in just a div
.
<div id="root">
<div></div>
</div>
π Extend the mount
function in VDomNode
to support primitive types and empty elements.
- Check if the
reactElement
is a empty (null
orundefined
)
π‘ Primitive types and empty elements are not represented with an object with a type
field.
- If the element is in fact empty, return an empty DOM-node.
π‘ createTextNode is perfect for
representing primitive types and empty nodes in the DOM. Use createTextNode
with an empty string as an argument. Since
this won't render anything to the DOM.
- Check if the
reactElement
is a primitive type
π‘ You can use the typeof operator to check the type of a variable, like this util-function:
function isPrimitive(reactElement) {
return !reactElement.type &&
(typeof reactElement === 'string' || typeof reactElement === 'number');
}
- If it is a primitive (
number
orstring
), create a new DOM-node and return it.
π‘ Primitives are always leaf-nodes and does not have children.
- If it's not a primitive, then do the logic we implemented in the previous tasks.
π You know what to do: npm run test4
In many ways React components are like JavaScript functions.
Just like functions, they accept arbitrary input. All input values are passed to the component in a single object called props
.
Props are used to customise components, and they enable component re-use.
For example, this code renders "Hello, NDC" on the page.
function Greeting(props) {
return <p>Hello, {props.name}</p>;
}
const element = <Greeting name="NDC" />;
ReactDOM.render(element, document.getElementById('root'));
In the above example, the prop "name" is set as a JSX attribute. React passes all JSX attributes to our user-defined component in a single object.
π Extend react-dom/index.js
and VCompositeNode.js
to handle functional components.
To get functional components working, you should:
- Extend
instantiateVNode
inreact-dom/index.js
to be able to instantiate aVCompositeNode
. To do this, just check if thetype
attribute ofreactElement
is afunction
(usetypeof
).
You also need to implement VCompositeNode.js
:
-
The
constructor
needs to set thereactElement
argument as a class property, just like we did forVDomNode
in task 2. -
The next thing we need to do is to render our component in
mount
. Call the functional component (type
) with itsprops
as the argumenttype(props)
.
π‘ this.reactElement.type
is a functional component (like Greeting
in the snippet above).
- Call
instantiateVNode
with the result of the rendering we did in step 3 to get a virtual node.
π‘ User defined (composite) components always render exactly one React element (which in turn can contain multiple React elements as children), hence we only need to call instantiateVNode
once with the value returned from our component.
- The last thing we need to do is to call
mount
on the virtual node from step 4 and return the value.
π Don't forget the tests! npm run test5
No application is complete without styling. In React, there are two main ways to style your elements βΒ inline styling and CSS. We'll cover CSS in this task and inline styling in task #7.
To specify a CSS class of an element, use the className
attribute. This is one of the JSX attributes (props
) that are reserved by React. It is used to set the class attribute of the specific element.
π Implement support for the className
attribute in VDomNode.js
π‘ You can use the className property of the Element interface to set the value of the class attribute of a specific HTML element.
π Tests FTW! npm run test6
Inline styling is another way to style your application. The style
attribute accepts a JavaScript object with camelCased properties. For instance, background-color
becomes backgroundColor
etc.
This is different from HTML where the style attribute accepts a CSS-string.
π Implement support for the style
attribute in VDomNode.js
π‘ You can use the style property of the HTMLElement to set the style attribute of a specific HTML element.
π You know the drill. npm run test7
If you are familiar with HTML, you know that we need to support more attributes than style
and className
. Luckily for us, most of these attributes are similar for React (we will handle events in the next task).
π Loop through the rest of the attributes (props
) and add them to the DOM node.
π‘ You can use setAttribute() to set attributes.
π‘ You can use Object.entries to loop through the keys and values of an object.
π You know the hammer too? Just kidding. That was a tool joke. What a tool. npm run test8
With plain html and JavaScript we primarily have two ways of adding event listeners. You can either use the addEventListener() function or you can add an event as a string attribute to the HTML element.
<button id="click-me">JavaScript</button>
<button onclick="alert('The second button was clicked')">HTML-attribute</button>
<script type="text/javascript">
var element = document.getElementById('click-me');
element.addEventListener('click', function() {
alert('The first button was clicked');
});
</script>
Similarly, events in React use attributes in JSX (props). However, there are some syntactic differences:
- React events are named using camelCase, rather than lowercase.
- With JSX you pass a function as the event handler, rather than a string.
const Button = () => (
<button onClick={() => alert('The button was clicked')}>Click me</button>
);
When using React you should generally not need to call
addEventListener
to add listeners to a DOM element after it is created.
π Use addEventListener()
to add event listeners in VDomNode.js
for each of the attributes that start with
on
.
π‘ You can use the following regex to find strings that start with on
:
const varToTest = 'onClick';
if (/^on.*$/.test(varToTest)) {
console.log('Found match ', varToTest);
}
π‘ Remember that, unlike React, events in plain JavaScript do not use camelCasing.
π Alright, you got us! You called our bluff, the way we are implementing events in this task is not true to Facebook's implementation of React. We had to cut some corners so you wouldn't be stuck here the rest of the week. React uses something called SyntheticEvents. One of the benefits of SyntheticEvent is to make React code portable, meaning that the events are not platform (React native) or browser specific. The way React does this is, in short, to append only one listener for each event on the root of the app and then delegate these further down to underlying components with a wrapper of data from the original event.
π In the event you have forgotten to run your tests npm run test9
.
Now we have created a library that supports stateless applications, well done!
Stateless applications always return the same result for every render. The next step to make this library complete is to introduce state.
Historically, stateful React components are defined using a class.
With the addition of hooks, you can use state and other React features without writing a class. This will not be covered in this workshop.
To create a class component you simply extend React.Component and
implement the render
function to specify what to render.
class Greeting extends React.Component {
render() {
return <p>Hello, {this.props.name}</p>;
}
}
If you take a look in react/
you will find that we've already created a base Component
for you.
But, using class components in our implementation of React still does not work properly β yet.
π As mentioned, the render
function is used to specify what to render. It is the only required method in a
class component and should return React elements.
To enforce that all classes that extend the Component
class implements the render
, let the
render
function in react/Component.js
throw an Error.
π We need to treat functional and class components differently. In contrast to functional components, we need
to call the render
method to determine the React elements to render.
To do this we need to know if a component is a functional or class component.
Since JavaScript classes in fact are functions,
we can not use the type of the variable to determine it. Instead add a simple flag as a
prototype data value
to our react/Component.js
.
Component.prototype.isReactComponent = true;
π Our class component does not support props
yet. Props should be accessible in all the class methods of our
class. In other words, the props should be available in the
function context
of our class. Implement a constructor
that takes the props
as an argument and assign them as a class property.
π‘ To assign the props
you can simply say: this.props = props;
π This seems like a good time to npm run test10
.
So, we now have functioning React components that support props
. But there is one problem... they don't render.
π We need to extend mount
in VCompositeNode
to not only handle functional components, but also
class components.
-
To do this we have to check which component we are about to render. Remember the
isReactComponent
flag that we introduced in the last task? It's almost scary how simple this is, but just check ifisReactComponent
istrue
on theprototype
of the component (that is thetype
property of thereactElement
). -
Instead of calling
type
as a function, in the way that we did for functional components. We callnew type
withprops
as an argument. -
We then need to call the
render
function of our newly instantiated component. -
The result of
render
returns areactElement
. To make this a virtual node we callinstantiateVNode
. -
To sum it all up, call
mount
on the virtual node we got in step 4.
π Hammer time, npm run test11
.
As mentioned, the whole point of making this Component class is to be able to create stateful components. So finally, let's add some state.
Setting the initial state of your component is really easy. Just assign an object with some properties to the property state
on your class.
Just like with props, this is now accessible through this.state
.
class Greeting extends React.Component {
constructor(props) {
super(props);
this.state = { name: 'world' };
}
render() {
return <p>Hello, {this.state.name}</p>;
}
}
Strictly speaking, your component now just has a property called state
, it doesn't really have state.
As you may know, in React you can use this.setState()
to change this property, and finally make your component stateful.
π Implement setState
in react/Component.js
.
The argument of setState
is expected to be an object, and it should be merged to the existing state.
If it is undefined
or null
you should simply do nothing - just return from the function.
π‘ To merge objects you can either use Object.assign()
or the shorthand spread syntax.
π₯ In React, setState()
can also take a function as the first parameter. If you want this functionality, you can check the type of state
in your function. If state
is a function, call state
with the current state as an argument.
π Time to check the state of things with npm run test12
.
If you try the code you currently have, you might notice that changing the state doesn't actually change anything in your DOM.
Your setState
function also needs to trigger a re-render of your DOM.
You could simply call ReactDOM.render()
in setState
after updating the state, but we want to do better than that.
If you have many components updating their state at the same time, simply calling ReactDOM.render()
would be quite the bottleneck,
as you would be rendering for every single component updating its state.
It would be very advantageous to defer the actual rendering until after we are done updating state in all components.
We can do this by wrapping ReactDOM.render()
in a setTimeout
with a zero millisecond delay.
π Implement the _reRender
function in ReactDOM and call this from the setState
function.
The re-render function should call setTimeout
with ReactDOM.render
as its callback function.
π‘ Timeouts in JS are only guaranteed to not run sooner than requested, but they may run later. A timeout of 0 ms will run its callback as soon as the browser isn't busy doing other things - like updating lots of component states.
π When you use setTimeout
the callback function is placed on the callback queue and ran at the next event loop.
There was a nice talk about this at JSConf EU 2014.
π Our implementation fails when we call _reRender
. This is because we are calling the render
function
without any arguments in _reRender
, while render
expects a reactElement
and a domContainerNode
.
To fix this we have to store reactElement
and domContainerNode
from the first render and then, if render
is
called without any arguments (i.e. reactElement
and domContainerNode
are undefined
), we use the stored instances.
π Even though we are calling to re-render in setState
the state of components does not persist between renders.
The reason for this is that we are creating new components on every render instead of keeping previously rendered
class components in memory.
To fix this, we are going to implement a class cache that saves our component instances between renders...
- Add the
classCache
toreact-dom/index.js
:
const classCache = {
index: -1,
cache: []
};
-
Call
mount
on the virtual node returned byinstantiateVNode
in therender
method ofreact-dom/index.js
, with the cache as themount
method's argument. Don't callmount
on the virtual nodes returned ininstantiateVNode
's function declaration! -
For
mount
inVDomNode
, you need to pass the cache to the next call ofmount
. -
For the
mount
function inVCompositeNode
, if the component is a class component, we have to increase the cache's index property, and get the element at that new index of thecache
array inside theclassCache
parameter. If the element is defined, use it and update itsprops
attribute. If the element is undefined, instantiate the class component as we did before. Remember to push the class instance back into the cache after updating itsprops
attribute. -
On re-render, you need to reset the cache index and remove all contents in
domContainerNode
inreact-dom/index.js
.
π Finally, for the last time, run the tests npm run test13
.
Thatβs all β we have a functional version of React now. Lets take a closer look at what we built:
- Supports HTML elements, such as
<div />
and<p />
- Supports both functional and class components.
- Handles children, state, props, events and other attributes.
- Supports initial rendering and re-rendering.
The main purpose of this workshop was to demonstrate core principles of React internal structure. However, some features were left out and this implementation serves as a foundation for you extend with these features.
In our implementation, we used a class cache to keep track of instantiated classes. However, this approach is flawed and not at all how React actually does it. If, for example, the order of components changes between renders, we will retrieve the wrong class instance from the cache.
You might also have noticed that we have some unimplemented functions in VDomNode
and VCompisteNode
. Instead of
calling mount
again for virtual nodes when re-renders, we should in fact call update
and update the nodes.
The way to handle stateful components between renders is to keep an instance of the instantiated component as a
class property in VCompositeNode
, and this is where getPublicInstance
comes in to play.
On calling the update
function in VDomNode
, when looping through children, we can retrieve and check if new
react elements are of the
same type
that they were the last time we rendered. We can then update, append, or remove nodes accordingly.
In src/solution/react-dom/react-dom
we have provided a more advanced implementation that you can look at for inspiration.
React components have several "lifecycle methods" that you can override to run code at a particular time. For instance, to run code after the component mounts, we can override Component.componentDidMount
.
Read about the lifecycle methods in the documentation and try to implement them.
Every time we change the state of one our components in our application, the DOM gets updated to reflect the new state. Frequent DOM manipulations affects performance and should be avoided. To avoid this we should minimize the number of manipulations.
There are multiple ways to reduce the number of manipulations, like reusing HTML elements, such as <div/>
, or using the key
prop of children to determine which child to update.
If an element type in the same place in the tree βmatches upβ between the previous render and the next one, React reuses the existing host instance. React only updates the properties that are absolutely necessary. For instance, if a Component's
className
prop is the only thing on the Component that changed, then that's the only thing that React needs to update. Source: https://overreacted.io/react-as-a-ui-runtime/#reconciliation
Our implementation renders the whole application regardless of which part of the application triggered the re-render.
To further improve the performance of our implementation, we can add _dirty
to the component that changed.
This way we are able to only re-render the subtree that changed.