├── .gitignore
├── README.md
├── package.json
└── src
├── .babelrc
├── __tests__
├── task-10_react-class-components.js
├── task-11_render-react-class-components.js
├── task-12_state.js
├── task-13_rerendering-state.js
├── task-1_react-createElement.js
├── task-2_render-html-elements.js
├── task-3_handle-children.js
├── task-4_primitive-types-and-empty-elements.js
├── task-5_functional-components-and-props.js
├── task-6_className.js
├── task-7_inline-styles.js
├── task-8_attributes.js
└── task-9_events.js
├── examples
├── minesweeper
│ ├── components
│ │ ├── Board.js
│ │ ├── Game.js
│ │ └── Square.js
│ ├── index.css
│ ├── index.html
│ ├── index.js
│ └── package.json
├── react-dom.js
├── react.js
└── todo
│ ├── index.css
│ ├── index.html
│ ├── index.js
│ └── package.json
├── index.css
├── index.html
├── index.js
├── polyfill.js
├── react-dom
├── VCompositeNode.js
├── VDomNode.js
└── index.js
├── react
├── Component.js
└── index.js
├── solution
├── README.md
├── react-dom
│ ├── README.md
│ ├── class-cache-react-dom
│ │ ├── README.md
│ │ ├── VCompositeNode.js
│ │ ├── VDomNode.js
│ │ └── index.js
│ ├── index.js
│ └── react-dom
│ │ ├── README.md
│ │ ├── VCompositeNode.js
│ │ ├── VDomNode.js
│ │ └── index.js
└── react
│ ├── Component.js
│ ├── README.md
│ └── index.js
└── test-utils.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 |
3 | .cache/
4 | dist/
5 | package-lock.json
6 |
7 | src/.cache/
8 | src/dist/
9 | src/package-lock.json
10 |
11 | node_modules/
12 |
13 | src/examples/minesweeper/package-lock.json
14 | src/examples/minesweeper/.cache/
15 | src/examples/minesweeper/dist/
16 | src/examples/todo/package-lock.json
17 | src/examples/todo/.cache/
18 | src/examples/todo/dist/
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Build your own React
2 |
3 | ## Table of contents
4 |
5 | :closed_book: [Introduction](#introduction)
6 |
7 | :runner: [Test your implementation](#test-your-impl)
8 |
9 | :construction_worker_man: [Tasks](#tasks)
10 |
11 | ## :closed_book: Introduction
12 |
13 | Generally, when we speak about React we talk about both [React](https://www.npmjs.com/package/react) and [ReactDOM](https://www.npmjs.com/package/react-dom).
14 | Prior to v0.14, all ReactDOM functionality was part of the React package.
15 | This may be a source of confusion, since older documentation won't mention the distinction between the React and ReactDOM packages.
16 |
17 | **ReactDOM** is the glue between React and the [DOM](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model).
18 | When you want to show your React application you need to use `ReactDOM.render()` from the ReactDOM package.
19 | This package include the [reconciliation algorithm](#reconciliation) and platform-specific code – also known as [renderers](#renderers).
20 |
21 | **React** – often referred to as React core and includes [the top-level React APIs](https://reactjs.org/docs/react-api.html#react).
22 | It only includes the APIs necessary to define components: the component base class,
23 | lifecycle functions, state, props and all the concepts we know and love.
24 |
25 | ### React elements
26 |
27 | React elements are the building blocks of React applications. React elements might be confused with the concept of
28 | React components. To clarify, React elements are generally what gets rendered on the screen, i.e. the return value of
29 | the `render()` function of a React component or the return of a functional component.
30 |
31 | ```jsx
32 | const element =
I'm an element
;
33 | ```
34 |
35 | ### Renderers
36 |
37 | React was originally created for the DOM, but the concept of renderers was introduced to support native platforms like React Native.
38 | A renderer is responsible for turning a tree of [React elements](#react-elements) into the underlying platform. In other words,
39 | if we want to support another platform all we need is a new renderer.
40 |
41 | In this workshop we are going to create a renderer that renders React components to the DOM, just like ReactDOM.
42 |
43 | ### Reconciliation
44 |
45 | Different renderers, such as ReactDOM and React Native, share a lot of logic. Rendering, custom components, state,
46 | lifecycle functions and refs should work consistently across platforms.
47 |
48 | When you use React you can think of the `render()` function as creating a tree of React elements. If props or state is
49 | changed, the `render()` function might return a different tree. The reconciler then needs to figure out how to
50 | effectively update the UI to match the most recent tree with the minimum number of operations required.
51 |
52 | > If you want to learn more about this, the [React documentation](https://reactjs.org/docs/reconciliation.html) contains an article that explains the choices made in React's diffing algorithm.
53 |
54 | ## :running: Testing your implementation
55 |
56 | First of all, run `npm install`
57 |
58 | 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.
59 |
60 | To run the tests for a specific task, you can simply specify the task (in this case task 1):
61 |
62 | ```
63 | npm run test1
64 | ```
65 |
66 | To run tests for task 2, just replace `test1` with `test2`, and so on.
67 |
68 | To run all tests:
69 |
70 | ```
71 | npm run test
72 | ```
73 |
74 | 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.
75 |
76 | ### Playground
77 |
78 | In addition to the tests, you can edit `src/index.js` to play with your implementation.
79 |
80 | To run the code:
81 |
82 | ```
83 | npm start
84 | ```
85 |
86 | The dev server should now be running on http://localhost:1234
87 |
88 | ### Examples
89 |
90 | We have provided you with e examples you can use in `src/examples`
91 |
92 | To run an example:
93 |
94 | 1. Change directory to the example `cd src/examples/`
95 | 2. Install and run the example with `npm`
96 |
97 | For instance, if you want to test the todo-example
98 |
99 | ```
100 | cd src/examples/todo
101 | npm install
102 | npm start
103 | ```
104 |
105 | ## :house: The structure
106 |
107 | If you've already looked in the `/react-dom` directory or `/react` directory, you might have noticed that they
108 | are not empty.
109 | We've taken the liberty of implementing a skeleton of empty functions for you to implement.
110 |
111 | To stay true to the virtual-DOM mindset you will find `VCompositeNode.js` and `VDomNode.js` in the `react-dom`
112 | directory. `VDomNode.js` is a "virtual" DOM-node, while the `VCompositeNode` represents a "virtual" react-component node.
113 | Everything that can be represented in the DOM, such as a `number`, `string`, `div`, `a`, `p` etc. should be a
114 | `VDomNode`. Everything else, and by that we mean stateful- or stateless-components should be a `VCompositeNode`.
115 |
116 | These "virtual" nodes can have children, which again are "virtual" nodes. This means that we get a tree-structure
117 | of nodes known as the "virtual DOM". The "virtual DOM" that we are about to implement is pretty naive. Nevertheless,
118 | the structure is there to make it easier to extend with a more advanced reconciliation algorithm that
119 | can just render portions of a sub-tree instead of rendering the whole tree every time.
120 |
121 | ## :construction_worker_man: Tasks
122 |
123 | Time to get your hands dirty.
124 |
125 | To make your life easier, we have used emojis to mark important content:
126 |
127 | :trophy: - A task.
128 |
129 | :bulb: - Tips and helpful information to solve a specific task.
130 |
131 | :fire: - An extra task if you're up for it.
132 |
133 | :books: - Some extended information you might check out some other time.
134 |
135 | :running: - We'll keep on reminding you to run the tests.
136 |
137 | ### 1. React.createElement()
138 |
139 | `createElement` creates and returns a new [React element](#react-elements) of a given type. The function signature of `createElement` takes three arguments:
140 |
141 | - `type` - the type of the element we are creating. This can be either be an [HTML element](https://developer.mozilla.org/en-US/docs/Web/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.
142 | - `props` - An object containing the properties (`props`) that get passed to the component.
143 | - `children` - The children of the component. You can pass as many children as you want.
144 |
145 | ```js
146 | React.createElement(type, props, ...children);
147 | ```
148 |
149 | The function returns an object like the one below.
150 |
151 | ```js
152 | {
153 | type: 'div',
154 | props: {
155 | className: 'my-class',
156 | randomProp: 'randomValue',
157 | children: [{
158 | type: 'button',
159 | props: { className: 'blue' }
160 | }, {
161 | type: 'button',
162 | props: { className: 'red' }
163 | }]
164 | },
165 | $$typeof: Symbol.for("react.element"),
166 | ref: null,
167 | _owner: null
168 | }
169 | ```
170 |
171 | :trophy: Implement the `createElement` function in the file named `react/index.js`
172 |
173 | :bulb: Unfamiliar with `React.createElement()`? Code written with [JSX](https://reactjs.org/docs/introducing-jsx.html) will be converted to use React.createElement(). You will not typically invoke React.createElement() directly if you are using JSX.
174 |
175 | :bulb: 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.
176 | When this is the case you need to [flatten](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flat) the array (requires polyfill for IE/Edge).
177 |
178 | :books: In this workshop, we won't make use of `$$typeof`, `ref` or `_owner`, but do take a look at [this blog post](https://overreacted.io/why-do-react-elements-have-typeof-property/) for details about what `$$typeof` is. Essentially it is to protect
179 | against XSS-attacks.
180 |
181 | :running: It's time to run some tests. If you haven't already, run `npm install` first. Then run `npm run test1`.
182 |
183 | ### 2. Render HTML elements
184 |
185 | Time to render our newly created React element!
186 |
187 | 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.
188 |
189 | 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.
190 |
191 | The following call to `ReactDOM.render()`...
192 |
193 | ```js
194 | ReactDOM.render(
195 | React.createElement('div', {}),
196 | document.getElementById('root')
197 | );
198 | ```
199 |
200 | ...should result in a `div` element within our root element.
201 |
202 | ```html
203 |
204 |
205 |
206 | ```
207 |
208 | :trophy: Create a new HTML node and append it to the DOM. Write your code in `/react-dom`.
209 |
210 | To complete our task, we need to:
211 |
212 | 1. return a `new VDomNode(reactElement)` from the `instantiateVNode` function in `react-dom/index.js`.
213 |
214 | 2. In `render`, we instantiate our virtual node with our reactElement by calling `instantiateVNode(reactElement)`. Store it in a variable named `vNode`.
215 |
216 | Now we need to mount (create a DOM-element) for our virtual node and append it to the DOM.
217 |
218 | 3. In `render` we need to mount our virtual node by calling the mount method on the virtual node. `vNode.mount()`
219 |
220 | 4. Append the result of the mount method to the `domContainerNode`.
221 |
222 | :bulb: [Node.appendChild()](https://developer.mozilla.org/en-US/docs/Web/API/Node/appendChild) function adds a node to
223 | the end of the list of children of a specified parent node.
224 |
225 | Remember to also implement the `constructor` and `mount` in `VDomNode`:
226 |
227 | 5. The `constructor` need to set the `reactElement`-argument as a class-property. For instance [like this](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes#class_declarations)
228 |
229 | 6. `mount` has to create a DOM-element from the `reactElement` class-property and return it.
230 |
231 | :bulb: [document.createElement()](https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement) can be used to create HTML elements.
232 |
233 | :running: Remember to run the tests `npm run test2` :wave:
234 |
235 | ### 3. Handle children
236 |
237 | Great, we are now able to create **one** HTML element! In order to render more than one element we need to handle children.
238 |
239 | To do so we have to extend the `mount()` function in `VDomNode.js` to iterate over possible children:
240 |
241 | The following call to `ReactDOM.render()`..
242 |
243 | ```js
244 | ReactDOM.render(
245 | React.createElement('div', {}, React.createElement('div', {})),
246 | document.getElementById('root')
247 | );
248 | ```
249 |
250 | ..should result in two nested `div` elements within our root element.
251 |
252 | ```html
253 |
254 |
255 |
256 |
257 |
258 | ```
259 |
260 | :trophy: Extend the `mount` function in `VDomNode.js` to support children.
261 |
262 | 1. Get `props.children` of the `reactElement` and map the children to `instantiateVNode`, which will create virtual
263 | DOM-nodes.
264 |
265 | :bulb: You can use this util method to get the children as an array from the props
266 |
267 | ```js
268 | function getChildrenAsArray(props) {
269 | const { children = [] } = props || {};
270 | return Array.isArray(children) ? children : [children];
271 | }
272 | ```
273 |
274 | 2. Iterate over the array of virtual child nodes, mount each of the virtual child nodes with the `.mount()` and use `appendChild` to append the result of `mount` to the element you created
275 | in the previous task.
276 |
277 | :running: Third time's the charm, run those tests! `npm run test3`
278 |
279 | ### 4. Primitive types and empty elements
280 |
281 | Your next task is to handle primitive types like `number` and `string`, as well as empty elements.
282 | Unlike HTML elements and React components, primitive types and empty elements are not represented as a standard React element.
283 | Moreover, they are not represented as an object with a `type` field. Instead, they are represented as their own value.
284 | Because of this primitive types and empty elements are always leaf nodes (i.e. children of another React element).
285 |
286 | The following call to `ReactDOM.render()`...
287 |
288 | ```js
289 | ReactDOM.render(
290 | React.createElement('div', {}, 'Hello world!'),
291 | document.getElementById('root')
292 | );
293 | ```
294 |
295 | ...should result in a `div` element with the text `Hello world!` inside it.
296 |
297 | ```html
298 |
320 | ```
321 |
322 | :trophy: Extend the `mount` function in `VDomNode` to support primitive types and empty elements.
323 |
324 | 1. Check if the `reactElement` is a empty (`null` or `undefined`)
325 |
326 | :bulb: Primitive types and empty elements are not represented with an object with a `type` field.
327 |
328 | 2. If the element is in fact empty, return an empty DOM-node.
329 |
330 | :bulb: [createTextNode](https://developer.mozilla.org/en-US/docs/Web/API/Document/createTextNode) is perfect for
331 | representing primitive types and empty nodes in the DOM. Use `createTextNode` with an empty string as an argument. Since
332 | this won't render anything to the DOM.
333 |
334 | 3. Check if the `reactElement` is a primitive type
335 |
336 | :bulb: You can use the [typeof](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof) operator to check the type of a variable, like this util-function:
337 |
338 | ```js
339 | function isPrimitive(reactElement) {
340 | return !reactElement.type &&
341 | (typeof reactElement === 'string' || typeof reactElement === 'number');
342 | }
343 | ```
344 |
345 | 4. If it is a primitive (`number` or `string`), create a new DOM-node and return it.
346 |
347 | :bulb: Primitives are always leaf-nodes and does not have children.
348 |
349 | 5. If it's not a primitive, then do the logic we implemented in the previous tasks.
350 |
351 | :running: You know what to do: `npm run test4`
352 |
353 | ### 5. Functional components and props
354 |
355 | In many ways React components are like JavaScript functions.
356 | Just like functions, they accept arbitrary input. All input values are passed to the component in a single object called `props`.
357 | Props are used to customise components, and they enable component re-use.
358 |
359 | For example, this code renders "Hello, NDC" on the page.
360 |
361 | ```jsx
362 | function Greeting(props) {
363 | return
Hello, {props.name}
;
364 | }
365 |
366 | const element = ;
367 | ReactDOM.render(element, document.getElementById('root'));
368 | ```
369 |
370 | 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.
371 |
372 | :trophy: Extend `react-dom/index.js` and `VCompositeNode.js` to handle functional components.
373 |
374 | To get functional components working, you should:
375 |
376 | 1. Extend `instantiateVNode` in `react-dom/index.js` to be able to instantiate a `VCompositeNode`.
377 | To do this, just check if the `type` attribute of `reactElement` is a `function` (use `typeof`).
378 |
379 | You also need to implement `VCompositeNode.js`:
380 |
381 | 2. The `constructor` needs to set the `reactElement` argument as a class property, just like we did for `VDomNode` in task 2.
382 |
383 | 3. The next thing we need to do is to render our component in `mount`. Call the functional component (`type`) with its `props` as the argument `type(props)`.
384 |
385 | :bulb: `this.reactElement.type` is a functional component (like `Greeting` in the snippet above).
386 |
387 | 4. Call `instantiateVNode` with the result of the rendering we did in step 3 to get a virtual node.
388 |
389 | :bulb: 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.
390 |
391 | 5. The last thing we need to do is to call `mount` on the virtual node from step 4 and return the value.
392 |
393 | :running: Don't forget the tests! `npm run test5`
394 |
395 | ### 6. className
396 |
397 | No application is complete without styling. In React, there are two main ways to style your elements – [inline styling](https://reactjs.org/docs/dom-elements.html#style) and [CSS](https://reactjs.org/docs/faq-styling.html). We'll cover CSS in this task and inline styling in task #7.
398 |
399 | 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](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/class) of the specific element.
400 |
401 | :trophy: Implement support for the `className` attribute in `VDomNode.js`
402 |
403 | :bulb: You can use the [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) property of the Element interface to set the value of the class attribute of a specific HTML element.
404 |
405 | :running: Tests FTW! `npm run test6`
406 |
407 | ### 7. Inline styles
408 |
409 | 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.
410 |
411 | > This is different from HTML where the [style attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/style) accepts a CSS-string.
412 |
413 | :trophy: Implement support for the `style` attribute in `VDomNode.js`
414 |
415 | :bulb: You can use the [style](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/style) property of the HTMLElement to set the style attribute of a specific HTML element.
416 |
417 | :running: You know the drill. `npm run test7`
418 |
419 | ### 8. Attributes
420 |
421 | 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).
422 |
423 | :trophy: Loop through the rest of the attributes (`props`) and add them to the DOM node.
424 |
425 | :bulb: You can use [setAttribute()](https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute) to set attributes.
426 |
427 | :bulb: You can use [Object.entries](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries) to loop through the keys and values of an object.
428 |
429 | :running: You know the hammer too? Just kidding. That was a tool joke. What a tool. `npm run test8`
430 |
431 | ### 9. Events
432 |
433 | With plain html and JavaScript we primarily have two ways of adding event listeners.
434 | You can either use the [addEventListener()](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener)
435 | function or you can add an event as a string attribute to the HTML element.
436 |
437 | ```html
438 |
439 |
440 |
441 |
447 | ```
448 |
449 | Similarly, [events in React](https://reactjs.org/docs/handling-events.html) use attributes in JSX (props).
450 | However, there are some syntactic differences:
451 |
452 | - React events are named using camelCase, rather than lowercase.
453 | - With JSX you pass a function as the event handler, rather than a string.
454 |
455 | ```jsx
456 | const Button = () => (
457 |
458 | );
459 | ```
460 |
461 | > When using React you should generally not need to call `addEventListener` to add listeners to a DOM element after it is created.
462 |
463 | :trophy: Use `addEventListener()` to add event listeners in `VDomNode.js` for each of the attributes that start with
464 | `on`.
465 |
466 | :bulb: You can use the following regex to find strings that start with `on`:
467 |
468 | ```js
469 | const varToTest = 'onClick';
470 |
471 | if (/^on.*$/.test(varToTest)) {
472 | console.log('Found match ', varToTest);
473 | }
474 | ```
475 |
476 | :bulb: Remember that, unlike React, events in plain JavaScript do not use camelCasing.
477 |
478 | :books: Alright, you got us! You called our bluff, the way we are implementing events in this task is not true to
479 | Facebook's implementation of React.
480 | We had to cut some corners so you wouldn't be stuck here the rest of the week. React uses something called
481 | [SyntheticEvents](https://reactjs.org/docs/events.html). One of the benefits of SyntheticEvent is to make React code
482 | portable, meaning that the events are not platform (React native) or browser specific. The way React does this is, in
483 | short, to append only one listener for each event on the root of the app and then delegate these further down to
484 | underlying components with a wrapper of data from the original event.
485 |
486 | :running: In the event you have forgotten to run your tests `npm run test9`.
487 |
488 | ### 10. React class components
489 |
490 | Now we have created a library that supports stateless applications, well done!
491 |
492 | Stateless applications always return the same result for every render. The next step to make this library complete is
493 | to introduce state.
494 |
495 | Historically, stateful React components are defined using [a class](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Classes).
496 |
497 | > With the addition of hooks, you can [use state and other React features](https://reactjs.org/docs/hooks-state.html) without writing a class. This will not be covered in this workshop.
498 |
499 | To create a class component you simply extend [React.Component](https://reactjs.org/docs/react-component.html) and
500 | implement the `render` function to specify what to render.
501 |
502 | ```jsx
503 | class Greeting extends React.Component {
504 | render() {
505 | return
Hello, {this.props.name}
;
506 | }
507 | }
508 | ```
509 |
510 | If you take a look in `react/` you will find that we've already created a base `Component` for you.
511 | But, using class components in our implementation of React still does not work properly – yet.
512 |
513 | :trophy: As mentioned, the `render` function is used to specify what to render. It is the only required method in a
514 | class component and should return [React elements](#react-elements).
515 | To enforce that all classes that extend the `Component` class implements the `render`, let the
516 | `render` function in `react/Component.js` throw an [Error](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error).
517 |
518 | :trophy: We need to treat functional and class components differently. In contrast to functional components, we need
519 | to call the `render` method to determine the React elements to render.
520 | To do this we need to know if a component is a functional or class component.
521 | Since [JavaScript classes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes) in fact are functions,
522 | we can not use the type of the variable to determine it. Instead add a simple flag as a
523 | [prototype data value](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/prototype)
524 | to our `react/Component.js`.
525 |
526 | ```js
527 | Component.prototype.isReactComponent = true;
528 | ```
529 |
530 | :trophy: Our class component does not support `props` yet. Props should be accessible in all the class methods of our
531 | class. In other words, the props should be available in the
532 | [function context](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this#Function_context)
533 | of our class. Implement a [constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes#Constructor)
534 | that takes the `props` as an argument and assign them as a class property.
535 |
536 | :bulb: To assign the `props` you can simply say: `this.props = props;`
537 |
538 | :running: This seems like a good time to `npm run test10`.
539 |
540 | ### 11. Render React class component
541 |
542 | So, we now have functioning React components that support `props`. But there is one problem... they don't render.
543 |
544 | :trophy: We need to extend `mount` in `VCompositeNode` to not only handle functional components, but also
545 | class components.
546 |
547 | 1. To do this we have to check which component we are about to render. Remember the `isReactComponent` flag that
548 | we introduced in the last task? It's almost scary how simple this is, but just check if `isReactComponent` is `true`
549 | on the `prototype` of the component (that is the `type` property of the `reactElement`).
550 |
551 | 2. Instead of calling `type` as a function, in the way that we did for functional components. We call `new type` with
552 | `props` as an argument.
553 |
554 | 3. We then need to call the `render` function of our newly instantiated component.
555 |
556 | 4. The result of `render` returns a `reactElement`. To make this a virtual node we call `instantiateVNode`.
557 |
558 | 5. To sum it all up, call `mount` on the virtual node we got in step 4.
559 |
560 | :running: Hammer time, `npm run test11`.
561 |
562 | ### 12. State
563 |
564 | As mentioned, the whole point of making this Component class is to be able to create stateful components.
565 | So finally, let's add some state.
566 |
567 | Setting the initial state of your component is really easy. Just assign an object with some properties to the property `state` on your class.
568 | Just like with props, this is now accessible through `this.state`.
569 |
570 | ```jsx
571 | class Greeting extends React.Component {
572 | constructor(props) {
573 | super(props);
574 | this.state = { name: 'world' };
575 | }
576 |
577 | render() {
578 | return
Hello, {this.state.name}
;
579 | }
580 | }
581 | ```
582 |
583 | Strictly speaking, your component now just has a property called `state`, it doesn't really _have_ state.
584 | As you may know, in React you can use `this.setState()` to change this property, and finally make your component stateful.
585 |
586 | :trophy: Implement `setState` in `react/Component.js`.
587 | The argument of `setState` is expected to be an object, and it should be merged to the existing state.
588 | If it is `undefined` or `null` you should simply do nothing - just return from the function.
589 |
590 | :bulb: To merge objects you can either use `Object.assign()` or the shorthand [spread syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax).
591 |
592 | :fire: 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.
593 |
594 | :running: Time to check the state of things with `npm run test12`.
595 |
596 | ### 13. (Re)rendering with state
597 |
598 | If you try the code you currently have, you might notice that changing the state doesn't actually change anything in your DOM.
599 | Your `setState` function also needs to trigger a re-render of your DOM.
600 |
601 | You could simply call `ReactDOM.render()` in `setState` after updating the state, but we want to do better than that.
602 |
603 | If you have many components updating their state at the same time, simply calling `ReactDOM.render()` would be quite the bottleneck,
604 | as you would be rendering for every single component updating its state.
605 | It would be very advantageous to defer the actual rendering until after we are done updating state in all components.
606 | We can do this by wrapping `ReactDOM.render()` in a `setTimeout` with a zero millisecond delay.
607 |
608 | :trophy: Implement the `_reRender` function in ReactDOM and call this from the `setState` function.
609 | The re-render function should call `setTimeout` with `ReactDOM.render` as its callback function.
610 |
611 | :bulb: Timeouts in JS are only guaranteed to not run _sooner_ than requested, but they _may_ run later.
612 | 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.
613 |
614 | :books: When you use `setTimeout` the callback function is placed on the callback queue and ran at the next event loop.
615 | There was [a nice talk about this](https://www.youtube.com/watch?v=8aGhZQkoFbQ) at JSConf EU 2014.
616 |
617 | :trophy: Our implementation fails when we call `_reRender`. This is because we are calling the `render` function
618 | without any arguments in `_reRender`, while `render` expects a `reactElement` and a `domContainerNode`.
619 | To fix this we have to store `reactElement` and `domContainerNode` from the first render and then, if `render` is
620 | called without any arguments (i.e. `reactElement` and `domContainerNode` are `undefined`), we use the stored instances.
621 |
622 | :trophy: Even though we are calling to re-render in `setState` the state of components does not persist between renders.
623 | The reason for this is that we are creating new components on every render instead of keeping previously rendered
624 | class components in memory.
625 | To fix this, we are going to implement a class cache that saves our component instances between renders...
626 |
627 | 1. Add the `classCache` to `react-dom/index.js`:
628 |
629 | ```js
630 | const classCache = {
631 | index: -1,
632 | cache: []
633 | };
634 | ```
635 |
636 | 2. Call `mount` on the virtual node returned by `instantiateVNode` in the `render` method of `react-dom/index.js`, with the cache as the `mount` method's
637 | argument. Don't call `mount` on the virtual nodes returned in `instantiateVNode`'s function declaration!
638 |
639 | 3. For `mount` in `VDomNode`, you need to pass the cache to the next call of `mount`.
640 |
641 | 4. For the `mount` function in `VCompositeNode`, if the component is a class component, we have to increase the
642 | cache's index property, and get the element at that new index of the `cache` array inside the `classCache` parameter. If the element is defined, use it and update its `props` attribute.
643 | If the element is undefined, instantiate the class component as we did before. Remember to push the class instance back into the
644 | cache after updating its `props` attribute.
645 |
646 | 5. On re-render, you need to reset the cache index and remove all contents in `domContainerNode` in `react-dom/index.js`.
647 |
648 | :running: Finally, for the last time, run the tests `npm run test13`.
649 |
650 | ## :feet: Next steps
651 |
652 | That’s all – we have a functional version of React now. Lets take a closer look at what we built:
653 |
654 | - Supports HTML elements, such as `` and ``
655 | - Supports both functional and class components.
656 | - Handles children, state, props, events and other attributes.
657 | - Supports initial rendering and re-rendering.
658 |
659 | The main purpose of this workshop was to demonstrate core principles of React internal structure. However, some
660 | features were left out and this implementation serves as a foundation for you extend with these features.
661 |
662 | ### Remove the class cache
663 |
664 | In our implementation, we used a class cache to keep track of instantiated classes. However, this approach is flawed
665 | and not at all how React actually does it. If, for example, the order of components changes between renders, we will
666 | retrieve the wrong class instance from the cache.
667 |
668 | You might also have noticed that we have some unimplemented functions in `VDomNode` and `VCompisteNode`. Instead of
669 | calling `mount` again for virtual nodes when re-renders, we should in fact call `update` and update the nodes.
670 | The way to handle stateful components between renders is to keep an instance of the instantiated component as a
671 | class property in `VCompositeNode`, and this is where `getPublicInstance` comes in to play.
672 |
673 | On calling the `update` function in `VDomNode`, when looping through children, we can retrieve and check if new
674 | react elements are of the
675 | same `type` that they were the last time we rendered. We can then update, append, or remove nodes accordingly.
676 |
677 | In `src/solution/react-dom/react-dom` we have provided a more advanced implementation that you can look at for inspiration.
678 |
679 | ### Lifecycle methods
680 |
681 | 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`.
682 |
683 | Read about the lifecycle methods in [the documentation](https://reactjs.org/docs/react-component.html#the-component-lifecycle) and try to implement them.
684 |
685 | ### More advanced reconciliation
686 |
687 | Every time we change the state of one our components in our application, the DOM gets updated to reflect the new state.
688 | Frequent DOM manipulations affects performance and should be avoided.
689 | To avoid this we should minimize the number of manipulations.
690 |
691 | There are multiple ways to reduce the number of manipulations, like reusing HTML elements, such as ``, or using the `key` prop of children to determine which child to update.
692 |
693 | > 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.
694 | > 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.
695 | > Source: https://overreacted.io/react-as-a-ui-runtime/#reconciliation
696 |
697 | Our implementation renders the whole application regardless of which part of the application triggered the re-render.
698 | To further improve the performance of our implementation, we can add `_dirty` to the component that changed.
699 | This way we are able to only re-render the subtree that changed.
700 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "src",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "parcel src/index.html",
8 | "test": "jest",
9 | "tdd": "jest --watch",
10 | "test1": "jest task-1_react-createElement.js",
11 | "test2": "jest task-[1-2]_.*.js",
12 | "test3": "jest task-[1-3]_.*.js",
13 | "test4": "jest task-[1-4]_.*.js",
14 | "test5": "jest task-[1-5]_.*.js",
15 | "test6": "jest task-[1-6]_.*.js",
16 | "test7": "jest task-[1-7]_.*.js",
17 | "test8": "jest task-[1-8]_.*.js",
18 | "test9": "jest task-[1-9]_.*.js",
19 | "test10": "jest task-[1-9][0]?_.*.js",
20 | "test11": "jest task-[1-9][0-1]?_.*.js",
21 | "test12": "jest task-[1-9][0-2]?_.*.js",
22 | "test13": "jest task-[1-9][0-3]?_.*.js"
23 | },
24 | "keywords": [],
25 | "author": "",
26 | "license": "ISC",
27 | "dependencies": {
28 | "parcel-bundler": "^1.12.4"
29 | },
30 | "devDependencies": {
31 | "@babel/core": "^7.12.9",
32 | "@babel/preset-env": "^7.12.7",
33 | "@babel/preset-react": "^7.12.7",
34 | "@testing-library/dom": "^7.28.1",
35 | "@testing-library/jest-dom": "^5.11.6",
36 | "babel-jest": "^26.6.3",
37 | "jest": "^26.6.3",
38 | "regenerator-runtime": "^0.13.7"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/env", "@babel/react"]
3 | }
4 |
--------------------------------------------------------------------------------
/src/__tests__/task-10_react-class-components.js:
--------------------------------------------------------------------------------
1 | import React from '../react';
2 | import '../test-utils';
3 |
4 | class Greeting extends React.Component {
5 | render() {
6 | return