├── .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 |
299 |
300 | Hello world! 301 |
302 |
303 | ``` 304 | 305 | ...while... 306 | 307 | ```js 308 | ReactDOM.render( 309 | React.createElement('div', {}, null), 310 | document.getElementById('root') 311 | ); 312 | ``` 313 | 314 | ...should result in just a `div`. 315 | 316 | ```html 317 |
318 |
319 |
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

Hello world

; 7 | } 8 | } 9 | 10 | class GreetingWithProps extends React.Component { 11 | render() { 12 | const { name } = this.props; 13 | return

Hello {name}

; 14 | } 15 | } 16 | 17 | test('Check Component render method returns React element', () => { 18 | const instance = new Greeting(); 19 | const element = instance.render(); 20 | 21 | expect(element.props.children).toEqual(['Hello world']); 22 | }); 23 | 24 | test('Check React Component throws error if used directly', () => { 25 | const instance = new React.Component(); 26 | 27 | expect(instance.render).not.toBeUndefined(); 28 | expect(typeof instance.render).toBe('function'); 29 | 30 | let error; 31 | try { 32 | instance.render(); 33 | } catch (e) { 34 | error = e; 35 | } 36 | 37 | expect(error instanceof Error).toBe(true); 38 | }); 39 | 40 | test('Check Component has prototype isReactComponent', () => { 41 | expect(React.Component.prototype.isReactComponent).toEqual(true); 42 | }); 43 | 44 | test('Check Component sets props', () => { 45 | const instance = new GreetingWithProps({ name: 'world' }); 46 | const element = instance.render(); 47 | 48 | expect(element.props.children).toEqual(['Hello ', 'world']); 49 | }); 50 | 51 | -------------------------------------------------------------------------------- /src/__tests__/task-11_render-react-class-components.js: -------------------------------------------------------------------------------- 1 | import { getNodeText } from '@testing-library/dom'; 2 | 3 | import React from '../react'; 4 | import ReactDOM from '../react-dom'; 5 | import { getExampleDOM } from '../test-utils'; 6 | 7 | class GreetingWithProps extends React.Component { 8 | render() { 9 | const { name } = this.props; 10 | return

Hello {name}

; 11 | } 12 | } 13 | 14 | test('Check that class components render', async () => { 15 | const container = getExampleDOM(); 16 | 17 | ReactDOM.render(, container); 18 | 19 | expect(getNodeText(container.querySelector('p'))).toBe('Hello world'); 20 | }); 21 | 22 | -------------------------------------------------------------------------------- /src/__tests__/task-12_state.js: -------------------------------------------------------------------------------- 1 | import React from '../react'; 2 | import '../test-utils'; 3 | 4 | class Greeting extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { name: 'world', moreState: 'right here' }; 8 | } 9 | 10 | render() { 11 | const { name } = this.state; 12 | return

Hello {name}

; 13 | } 14 | } 15 | 16 | test('Check Component correctly updates state', () => { 17 | const element = new Greeting(); 18 | 19 | element.setState({ name: 'universe' }); 20 | 21 | expect(element.state.name).toBe('universe'); 22 | }); 23 | 24 | test('Check if key-value pairs are preserved when another part of the state is updatede', () => { 25 | const element = new Greeting(); 26 | 27 | element.setState({ name: 'universe' }); 28 | 29 | expect(element.state.name).toBe('universe'); 30 | expect(element.state.moreState).toBe('right here'); 31 | }); 32 | 33 | test('Check Component does not change state if new state is null', () => { 34 | const element = new Greeting(); 35 | 36 | element.setState(null); 37 | 38 | expect(element.state.name).toBe('world'); 39 | expect(element.state.moreState).toBe('right here'); 40 | }); 41 | -------------------------------------------------------------------------------- /src/__tests__/task-13_rerendering-state.js: -------------------------------------------------------------------------------- 1 | import { getNodeText, fireEvent, waitFor } from '@testing-library/dom'; 2 | 3 | import React from '../react'; 4 | import ReactDOM from '../react-dom'; 5 | import { getExampleDOM } from '../test-utils'; 6 | 7 | class Greeting extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | this.state = { name: 'world' }; 11 | } 12 | 13 | render() { 14 | const { name } = this.state; 15 | const { newState } = this.props; 16 | return ( 17 |
18 |

Hello {name}

19 | 22 |
23 | ); 24 | } 25 | } 26 | 27 | test('Check state updates correctly updates the DOM (fails with timeout if ReactDOM._reRender does not correctly defer rendering)', async () => { 28 | const container = getExampleDOM(); 29 | 30 | ReactDOM.render(, container); 31 | 32 | expect(getNodeText(container.querySelector('p'))).toBe('Hello world'); 33 | 34 | fireEvent( 35 | container.querySelector('button'), 36 | new MouseEvent('click') 37 | ); 38 | 39 | await waitFor(() => { expect(getNodeText(container.querySelector('p'))).toBe('Hello universe') }); 40 | }); 41 | -------------------------------------------------------------------------------- /src/__tests__/task-1_react-createElement.js: -------------------------------------------------------------------------------- 1 | import React from '../react'; 2 | import '../test-utils'; 3 | 4 | test('Check creation of React elements', async () => { 5 | const element = React.createElement('p', { myProp: 'myValue' }, 'Hello world', 'Isn\'t this fun?'); 6 | 7 | expect(element['$$typeof']).toBe(Symbol.for('react.element')); 8 | expect(element.props.children).toEqual([ 9 | 'Hello world', 10 | 'Isn\'t this fun?', 11 | ]); 12 | expect(element.props.myProp).toBe('myValue'); 13 | }); 14 | 15 | test('Check createElement handles an array of children', async () => { 16 | const element = React.createElement('p', {}, ['Hello', 'world']); 17 | 18 | expect(element.props.children).toEqual(['Hello', 'world']); 19 | }); 20 | -------------------------------------------------------------------------------- /src/__tests__/task-2_render-html-elements.js: -------------------------------------------------------------------------------- 1 | import React from '../react'; 2 | import ReactDOM, { instantiateVNode } from '../react-dom'; 3 | import { getExampleDOM } from '../test-utils'; 4 | 5 | test('Checks that instantiateVNode returns a VDomNode', () => { 6 | expect(instantiateVNode(

).constructor.name).toBe('VDomNode'); 7 | }); 8 | 9 | test('Check rendering of html element', async () => { 10 | const container = getExampleDOM(); 11 | 12 | ReactDOM.render( 13 |

, 14 | container 15 | ); 16 | 17 | expect(container.querySelector('p')).not.toBeNull(); 18 | }); 19 | -------------------------------------------------------------------------------- /src/__tests__/task-3_handle-children.js: -------------------------------------------------------------------------------- 1 | import React from '../react'; 2 | import ReactDOM from '../react-dom'; 3 | import { getExampleDOM } from '../test-utils'; 4 | 5 | test('Check rendering of a child', async () => { 6 | const container = getExampleDOM(); 7 | 8 | ReactDOM.render( 9 |

10 | 11 |

, 12 | container 13 | ); 14 | 15 | expect(container.querySelector('p')).not.toBeNull(); 16 | expect(container.querySelector('span')).not.toBeNull(); 17 | }); 18 | -------------------------------------------------------------------------------- /src/__tests__/task-4_primitive-types-and-empty-elements.js: -------------------------------------------------------------------------------- 1 | import { getNodeText } from '@testing-library/dom'; 2 | 3 | import React from '../react'; 4 | import ReactDOM from '../react-dom'; 5 | import { getExampleDOM } from '../test-utils'; 6 | 7 | test('Check rendering of empty child', () => { 8 | const container = getExampleDOM(); 9 | 10 | ReactDOM.render( 11 |
12 | {null} 13 |
, 14 | container 15 | ); 16 | 17 | expect(container.querySelector('div').childNodes.length).toBe(1); 18 | expect(getNodeText(container.querySelector('div'))).toBe(''); 19 | }); 20 | 21 | test('Check rendering of a primitive type child', () => { 22 | const container = getExampleDOM(); 23 | 24 | ReactDOM.render( 25 |
26 | Hello universe 27 |

28 | Hello world 29 |

30 |
, 31 | container 32 | ); 33 | 34 | expect(getNodeText(container.querySelector('div'))).toBe('Hello universe'); 35 | expect(getNodeText(container.querySelector('p'))).toBe('Hello world'); 36 | }); 37 | -------------------------------------------------------------------------------- /src/__tests__/task-5_functional-components-and-props.js: -------------------------------------------------------------------------------- 1 | import { getNodeText } from '@testing-library/dom'; 2 | 3 | import React from '../react'; 4 | import ReactDOM from '../react-dom'; 5 | import { getExampleDOM } from '../test-utils'; 6 | 7 | test('Check rendering of a functional component with a prop', async () => { 8 | const container = getExampleDOM(); 9 | 10 | function Greeting(props) { 11 | return

Hello, {props.name}

; 12 | } 13 | 14 | ReactDOM.render( 15 | , 16 | container 17 | ); 18 | 19 | expect(getNodeText(container.querySelector('p'))).toBe('Hello, NDC'); 20 | }); 21 | -------------------------------------------------------------------------------- /src/__tests__/task-6_className.js: -------------------------------------------------------------------------------- 1 | import { getNodeText } from '@testing-library/dom'; 2 | 3 | import React from '../react'; 4 | import ReactDOM from '../react-dom'; 5 | import { getExampleDOM } from '../test-utils'; 6 | 7 | test('Check rendering with a CSS class', async () => { 8 | const container = getExampleDOM(); 9 | 10 | ReactDOM.render( 11 |

12 | Hello world! 13 |

, 14 | container 15 | ); 16 | 17 | expect(getNodeText(container.querySelector('.NDC'))).toBe('Hello world!'); 18 | }); 19 | -------------------------------------------------------------------------------- /src/__tests__/task-7_inline-styles.js: -------------------------------------------------------------------------------- 1 | import React from '../react'; 2 | import ReactDOM from '../react-dom'; 3 | import { getExampleDOM } from '../test-utils'; 4 | 5 | test('Check rendering with inline styling', async () => { 6 | const container = getExampleDOM(); 7 | 8 | ReactDOM.render( 9 |

10 | Hello world! 11 |

, 12 | container 13 | ); 14 | 15 | expect(container.querySelector('[style="color: red;"]')).not.toBeNull(); 16 | }); 17 | -------------------------------------------------------------------------------- /src/__tests__/task-8_attributes.js: -------------------------------------------------------------------------------- 1 | import React from '../react'; 2 | import ReactDOM from '../react-dom'; 3 | import { getExampleDOM } from '../test-utils'; 4 | 5 | test('Check rendering with an html prop', async () => { 6 | const container = getExampleDOM(); 7 | 8 | ReactDOM.render( 9 | , 10 | container 11 | ); 12 | 13 | expect(container.querySelector('input').value).toBe('Hello world'); 14 | }); 15 | -------------------------------------------------------------------------------- /src/__tests__/task-9_events.js: -------------------------------------------------------------------------------- 1 | import { fireEvent } from '@testing-library/dom'; 2 | 3 | import React from '../react'; 4 | import ReactDOM from '../react-dom'; 5 | import { getExampleDOM } from '../test-utils'; 6 | 7 | test('Check rendering with an event listener', async () => { 8 | const container = getExampleDOM(); 9 | const onClick = jest.fn(); 10 | 11 | ReactDOM.render( 12 | , 15 | container 16 | ); 17 | 18 | fireEvent( 19 | container.querySelector('button'), 20 | new MouseEvent('click') 21 | ); 22 | 23 | expect(onClick).toHaveBeenCalled(); 24 | }); 25 | -------------------------------------------------------------------------------- /src/examples/minesweeper/components/Board.js: -------------------------------------------------------------------------------- 1 | import React from '../../react'; 2 | import Square from './Square'; 3 | 4 | class Board extends React.Component { 5 | getBorderColor(displayValue) { 6 | var borderColor = '#E3E3E3'; 7 | 8 | var colors = [ 9 | '#5D1052', 10 | '#800080', 11 | '#ce325f', 12 | '#AE72FF', 13 | '#F132FF', 14 | '#ff7c80', 15 | '#DD93BD', 16 | '#fbd4b4' 17 | ]; 18 | 19 | if (this.props.isGameOver === true) { 20 | if (displayValue === 'm') { 21 | borderColor = '#C97C81'; 22 | } else { 23 | borderColor = '#E3E3E3'; 24 | } 25 | } else if (this.props.isGameWon === true) { 26 | borderColor = '#6d8fe6'; 27 | } else if (displayValue === null) { 28 | borderColor = '#E3E3E3'; 29 | } else if (displayValue === 'f') { 30 | borderColor = '#17AD90'; 31 | } else if (displayValue < 9 && displayValue > 0) 32 | borderColor = colors[8 - displayValue]; 33 | else if (displayValue === '') 34 | //No mines nearby 35 | borderColor = '#f4f4f4'; 36 | 37 | return borderColor; 38 | } 39 | 40 | getFillColor(displayValue) { 41 | var fillColor = '#E3E3E3'; //default background color 42 | 43 | if (this.props.isGameOver === true) { 44 | fillColor = '#e33912'; 45 | } else if (this.props.isGameWon === true) { 46 | fillColor = '#add8e6'; 47 | } else if (displayValue !== null) { 48 | fillColor = '#f4f4f4'; 49 | } 50 | 51 | return fillColor; 52 | } 53 | 54 | //Create one row of squares 55 | renderRow(rowIndex) { 56 | return ( 57 |
58 | {this.props.displayedSquares[rowIndex].map( 59 | (square, columnIndex) => { 60 | var displayValue = this.props.displayedSquares[ 61 | rowIndex 62 | ][columnIndex]; 63 | var borderColor = this.getBorderColor(displayValue); 64 | var fillColor = this.getFillColor(displayValue); 65 | 66 | return ( 67 | 74 | this.props.onClick(e, columnIndex, rowIndex) 75 | } 76 | key={ 77 | rowIndex * this.props.columnsNumber + 78 | columnIndex 79 | } 80 | /> 81 | ); 82 | } 83 | )} 84 |
85 | ); 86 | } 87 | 88 | render() { 89 | var boardRows = new Array(this.props.rowsNumber); 90 | 91 | for (var rowIndex = 0; rowIndex < this.props.rowsNumber; rowIndex++) { 92 | //create all rows 93 | boardRows.push(this.renderRow(rowIndex)); 94 | } 95 | 96 | return
{boardRows}
; 97 | } 98 | } 99 | 100 | export default Board; 101 | -------------------------------------------------------------------------------- /src/examples/minesweeper/components/Game.js: -------------------------------------------------------------------------------- 1 | import React from '../../react'; 2 | import Board from './Board'; 3 | 4 | class Game extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | 8 | this.rows = props.rows; 9 | this.columns = props.columns; 10 | this.minesNumber = props.mines; 11 | 12 | //two dimensional array. This will indicate whether the square contains a mine, and if not- how many mines surround it. 13 | this.squaresValues = null; 14 | 15 | //Draw random mines locations and fill all squaresValues. 16 | this.initBoard(); 17 | 18 | this.state = { 19 | isGameOver: false, 20 | isGameWon: false, 21 | remainingFlags: this.minesNumber, 22 | 23 | //two dimensional array. This will indicate the value that is currently displayed in each square. 24 | displayedSquares: this.initDisplayedSquares() 25 | }; 26 | } 27 | 28 | initBoard() { 29 | this.squaresValues = new Array(this.rows); 30 | for (var i = 0; i < this.rows; i++) { 31 | this.squaresValues[i] = new Array(this.columns).fill(0); 32 | } 33 | //generate random mines 34 | this.generateRandomMines(); 35 | 36 | //fill values for adjacent squares 37 | this.fillAdjacentValues(); 38 | } 39 | 40 | generateRandomMines() { 41 | var totalSquaresNumber = this.columns * this.rows; 42 | var minesOnBoard = 0; 43 | 44 | while (minesOnBoard < this.minesNumber) { 45 | var index = Math.floor(Math.random() * totalSquaresNumber); // returns a number between 0 and number of squares 46 | if ( 47 | this.squaresValues[Math.floor(index / this.columns)][ 48 | index % this.columns 49 | ] === 0 50 | ) { 51 | //square does not have a mine already 52 | this.squaresValues[Math.floor(index / this.columns)][ 53 | index % this.columns 54 | ] = 10; //10 value will represent a mine 55 | minesOnBoard++; 56 | } 57 | } 58 | } 59 | 60 | initDisplayedSquares() { 61 | var squaresValuesArray = new Array(this.rows); 62 | for (var i = 0; i < this.rows; i++) { 63 | squaresValuesArray[i] = new Array(this.columns).fill(null); 64 | } 65 | return squaresValuesArray; 66 | } 67 | 68 | fillAdjacentValues() { 69 | for (var i = 0; i < this.rows; i++) { 70 | //Go over all rows 71 | for (var j = 0; j < this.columns; j++) { 72 | //Go over all columns 73 | if (this.squaresValues[i][j] === 10) { 74 | //a mine 75 | //add to adjacent values 76 | this.addToAdjacentSquaresValues(i, j); 77 | } 78 | } 79 | } 80 | } 81 | 82 | //Add to the values of all sqares that are adjacent to the mine 83 | addToAdjacentSquaresValues(row, column) { 84 | for (var k = -1; k <= 1; k++) { 85 | if (row + k < this.rows && row + k >= 0) { 86 | for (var l = -1; l <= 1; l++) { 87 | if (column + l < this.columns && column + l >= 0) { 88 | if (this.squaresValues[row + k][column + l] !== 10) { 89 | this.squaresValues[row + k][column + l]++; 90 | } 91 | } 92 | } 93 | } 94 | } 95 | } 96 | 97 | handleClick(e, columnIndex, rowIndex) { 98 | //Cannot receive more clicks if game is over 99 | if (this.state.isGameOver || this.state.isGameWon) { 100 | return; 101 | } 102 | 103 | //If flag was requested 104 | if ( 105 | e.shiftKey && 106 | (this.state.displayedSquares[rowIndex][columnIndex] === null || 107 | this.state.displayedSquares[rowIndex][columnIndex] === 'f') 108 | ) { 109 | this.handleFlag(rowIndex, columnIndex); 110 | } 111 | 112 | //Check if location has already been clicked ( 113 | else if (this.state.displayedSquares[rowIndex][columnIndex] !== null) { 114 | } 115 | //Do nothing 116 | 117 | //Check if user clicked on mine 118 | else if (this.squaresValues[rowIndex][columnIndex] === 10) { 119 | this.addToDisplayedSquares(rowIndex, columnIndex, 'm'); 120 | 121 | this.setState({ isGameOver: true }); 122 | 123 | this.gameOver(); 124 | } 125 | 126 | //Show square value 127 | else { 128 | this.revealAdjacentMinesNumber(rowIndex, columnIndex); 129 | } 130 | } 131 | 132 | //Display a specific value in a specific square 133 | addToDisplayedSquares(row, column, value) { 134 | const displayedSquares = this.state.displayedSquares.slice(); 135 | displayedSquares[row][column] = value; 136 | var t0 = performance.now(); 137 | this.setState({ 138 | displayedSquares: displayedSquares 139 | }); 140 | var t1 = performance.now(); 141 | console.log( 142 | 'Call to set state displayedSquares took ' + 143 | (t1 - t0) + 144 | ' milliseconds.' 145 | ); 146 | } 147 | 148 | revealAdjacentMinesNumber(rowIndex, columnIndex) { 149 | //Already visited this cell 150 | if ( 151 | this.state.displayedSquares[rowIndex][columnIndex] === '' || 152 | this.state.displayedSquares[rowIndex][columnIndex] === 'f' 153 | ) { 154 | return; 155 | } 156 | 157 | //There are mines nearby 158 | if ( 159 | this.squaresValues[rowIndex][columnIndex] > 0 && 160 | this.squaresValues[rowIndex][columnIndex] <= 9 161 | ) { 162 | this.addToDisplayedSquares( 163 | rowIndex, 164 | columnIndex, 165 | this.squaresValues[rowIndex][columnIndex] 166 | ); 167 | return; 168 | } 169 | 170 | //If no mines nearby, check adjacent cells 171 | else if (this.squaresValues[rowIndex][columnIndex] === 0) { 172 | this.addToDisplayedSquares(rowIndex, columnIndex, ''); //This will indicate that the cell was visited 173 | this.checkNeighboursAdjacentMinesNumber(rowIndex, columnIndex); 174 | } 175 | 176 | return; 177 | } 178 | 179 | checkNeighboursAdjacentMinesNumber(row, column) { 180 | for (var i = -1; i <= 1; i++) { 181 | if (row + i < this.rows && row + i >= 0) { 182 | for (var j = -1; j <= 1; j++) { 183 | if (column + j < this.columns && column + j >= 0) { 184 | this.revealAdjacentMinesNumber(row + i, column + j); 185 | } 186 | } 187 | } 188 | } 189 | } 190 | 191 | gameOver() { 192 | //show all mines on board 193 | const displayedSquares = this.state.displayedSquares.slice(); 194 | 195 | for (var i = 0; i < this.rows; i++) { 196 | //Go over all rows 197 | for (var j = 0; j < this.columns; j++) { 198 | //Go over all columns 199 | if (this.squaresValues[i][j] === 10) { 200 | displayedSquares[i][j] = 'm'; 201 | } 202 | } 203 | } 204 | 205 | this.setState({ 206 | displayedSquares: displayedSquares 207 | }); 208 | } 209 | 210 | handleFlag(rowIndex, columnIndex) { 211 | //handle shift-click 212 | 213 | //Remove flag 214 | if (this.state.displayedSquares[rowIndex][columnIndex] === 'f') { 215 | this.addToDisplayedSquares(rowIndex, columnIndex, null); 216 | this.setState({ 217 | remainingFlags: this.state.remainingFlags + 1 218 | }); 219 | } 220 | 221 | //Add flag - if more flags are available 222 | else if (this.state.remainingFlags > 0) { 223 | this.addToDisplayedSquares(rowIndex, columnIndex, 'f'); 224 | this.setState( 225 | { 226 | remainingFlags: this.state.remainingFlags - 1 227 | }, 228 | function() { 229 | if (this.state.remainingFlags === 0) { 230 | var isGameWon = this.IsGameWon(); 231 | if (isGameWon === true) { 232 | this.setState({ isGameWon: true }); 233 | } 234 | } 235 | } 236 | ); 237 | } 238 | 239 | //No more flags available 240 | else { 241 | alert('You have no more available flags!'); 242 | } 243 | } 244 | 245 | IsGameWon() { 246 | //Check if all the mines are flagged 247 | if (this.state.remainingFlags === 0) { 248 | for (var i = 0; i < this.rows; i++) { 249 | //Go over all rows 250 | for (var j = 0; j < this.columns; j++) { 251 | //Go over all columns 252 | if ( 253 | this.squaresValues[i][j] === 10 && 254 | this.state.displayedSquares[i][j] !== 'f' 255 | ) { 256 | //Mine is not flagged 257 | return false; 258 | } 259 | } 260 | } 261 | return true; 262 | } else { 263 | return false; 264 | } 265 | } 266 | 267 | render() { 268 | var boardWidth = Math.max(35 * this.props.columns, 350); 269 | 270 | return React.createElement( 271 | 'div', 272 | { className: 'game', style: { maxWidth: boardWidth } }, 273 | React.createElement( 274 | 'div', 275 | { className: 'boardHeader' }, 276 | React.createElement( 277 | 'h4', 278 | { className: 'title' }, 279 | React.createElement( 280 | 'span', 281 | { style: { color: '#DD93BD' } }, 282 | 'M' 283 | ), 284 | React.createElement( 285 | 'span', 286 | { style: { color: '#ff7c80' } }, 287 | 'i' 288 | ), 289 | React.createElement( 290 | 'span', 291 | { style: { color: '#F132FF' } }, 292 | 'n' 293 | ), 294 | React.createElement( 295 | 'span', 296 | { style: { color: '#fbd4b4' } }, 297 | 'e' 298 | ), 299 | React.createElement( 300 | 'span', 301 | { id: 'SweeperHeader' }, 302 | ' Sweeper ' 303 | ) 304 | ), 305 | React.createElement( 306 | 'div', 307 | { className: 'flags-information' }, 308 | React.createElement('h1', null, this.state.remainingFlags), 309 | React.createElement('img', { 310 | src: 311 | 'https://cdn.rawgit.com/ofirdagan/build-your-own-react/2e8bad05/v5-examples/v5-step2-minesweeper/assets/black_flag.svg', 312 | width: '30', 313 | height: '30' 314 | }) 315 | ) 316 | ), 317 | React.createElement( 318 | 'div', 319 | null, 320 | React.createElement(Board, { 321 | displayedSquares: this.state.displayedSquares, 322 | columnsNumber: this.columns, 323 | rowsNumber: this.rows, 324 | isGameOver: this.state.isGameOver, 325 | isGameWon: this.state.isGameWon, 326 | onClick: (e, columnIndex, rowIndex) => 327 | this.handleClick(e, columnIndex, rowIndex) 328 | }) 329 | ) 330 | ); 331 | } 332 | } 333 | 334 | export default Game; 335 | -------------------------------------------------------------------------------- /src/examples/minesweeper/components/Square.js: -------------------------------------------------------------------------------- 1 | import React from '../../react'; 2 | 3 | function Square(props) { 4 | if (props.valueToDisplay === 'f') { 5 | return ( 6 | 11 | ); 12 | } else if (props.valueToDisplay === 'm') { 13 | return ( 14 | 19 | ); 20 | } else { 21 | return props.valueToDisplay; 22 | } 23 | } 24 | 25 | function SquareButton(props) { 26 | var border = '5px solid' + props.borderColor; 27 | var fill = props.fillColor; 28 | 29 | return ( 30 | 40 | ); 41 | } 42 | 43 | export default SquareButton; 44 | -------------------------------------------------------------------------------- /src/examples/minesweeper/index.css: -------------------------------------------------------------------------------- 1 | .app { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | } 6 | 7 | body { 8 | font: 14px 'Century Gothic', Futura, sans-serif; 9 | margin: 20px; 10 | } 11 | 12 | h4, 13 | h1, 14 | h2 { 15 | margin: 0px; 16 | } 17 | 18 | h2 { 19 | font-size: small; 20 | } 21 | 22 | input { 23 | font-size: small; 24 | background-color: #fefefe; 25 | border: 1px solid #cacaca; 26 | margin-left: 4px; 27 | height: 15px; 28 | width: 30px; 29 | } 30 | 31 | .game, 32 | .board { 33 | display: flex; 34 | flex-direction: column; 35 | justify-content: center; 36 | margin-left: auto; 37 | margin-right: auto; 38 | } 39 | 40 | h4.title { 41 | justify-content: flex-start; 42 | margin-left: 8px; 43 | font-size: 50px; 44 | font-weight: 800; 45 | font-family: sans-serif; 46 | align-items: flex-end; 47 | display: flex; 48 | flex-direction: row; 49 | } 50 | .properties { 51 | display: flex; 52 | flex-direction: row; 53 | margin-right: 25px; 54 | max-width: 250px; 55 | } 56 | 57 | .flags-information { 58 | align-items: flex-end; 59 | flex-direction: row; 60 | display: flex; 61 | justify-content: flex-end; 62 | padding-right: 8px; 63 | padding-bottom: 9px; 64 | } 65 | 66 | .boardHeader { 67 | display: flex; 68 | flex-direction: row; 69 | justify-content: space-between; 70 | margin-bottom: 40px; 71 | } 72 | 73 | .game-settings { 74 | background: #ff7c80; 75 | align-items: center; 76 | display: flex; 77 | padding-left: 10px; 78 | padding-right: 10px; 79 | flex-direction: row; 80 | justify-content: space-between; 81 | margin-left: auto; 82 | margin-right: auto; 83 | margin-top: 20px; 84 | max-width: 310px; 85 | height: 40px; 86 | border: 1px solid #eeeeee; 87 | border-radius: 10px; 88 | } 89 | 90 | .board-row, 91 | .property { 92 | display: flex; 93 | flex-direction: row; 94 | justify-content: center; 95 | } 96 | .property { 97 | display: flex; 98 | flex-direction: row; 99 | justify-content: center; 100 | align-items: center; 101 | margin-left: 5px; 102 | } 103 | .start-button { 104 | background-color: #f1c99f; 105 | display: flex; 106 | flex-direction: row; 107 | align-items: center; 108 | justify-content: center; 109 | border-radius: 30px; 110 | width: 30px; 111 | height: 30px; 112 | } 113 | 114 | .square { 115 | background: #e3e3e3; 116 | border: 0px solid #dcd7ab; 117 | border-radius: 6px; 118 | float: left; 119 | font-size: 25px; 120 | font-weight: bold; 121 | height: 35px; 122 | margin-top: 1px; 123 | margin-right: 1px; 124 | padding: 0; 125 | text-align: center; 126 | width: 35px; 127 | } 128 | 129 | #SweeperHeader { 130 | color: #17ad90; 131 | padding-bottom: 7px; 132 | padding-left: 4px; 133 | font-size: 25px; 134 | align-items: flex-end; 135 | } 136 | 137 | button:focus { 138 | outline: 0; 139 | } 140 | 141 | .hidden { 142 | display: none; 143 | } 144 | 145 | .author-credit { 146 | margin-top: 10px; 147 | } 148 | -------------------------------------------------------------------------------- /src/examples/minesweeper/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/examples/minesweeper/index.js: -------------------------------------------------------------------------------- 1 | import React from '../react'; 2 | import ReactDOM from '../react-dom'; 3 | 4 | import Game from './components/Game'; 5 | 6 | class MineSweeperApp extends React.Component { 7 | constructor() { 8 | super(); 9 | this.state = { 10 | rows: 10, 11 | columns: 10, 12 | mines: 10, 13 | game: React.createElement(Game, { 14 | rows: 10, 15 | columns: 10, 16 | mines: 10 17 | }) 18 | }; 19 | } 20 | 21 | render() { 22 | const ActiveGame = this.state.game; 23 | return React.createElement('div', { className: 'app' }, ActiveGame); 24 | } 25 | } 26 | var t0 = performance.now(); 27 | ReactDOM.render( 28 | React.createElement(MineSweeperApp), 29 | document.getElementById('root') 30 | ); 31 | var t1 = performance.now(); 32 | console.log('Call to first render took ' + (t1 - t0) + ' milliseconds.'); 33 | -------------------------------------------------------------------------------- /src/examples/minesweeper/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "src", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "parcel index.html", 8 | "test": "jest", 9 | "tdd": "jest --watch" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "parcel-bundler": "^1.12.4" 16 | }, 17 | "devDependencies": { 18 | "@babel/core": "^7.12.9" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/examples/react-dom.js: -------------------------------------------------------------------------------- 1 | import ReactDOM from '../react-dom'; 2 | 3 | export default ReactDOM; 4 | -------------------------------------------------------------------------------- /src/examples/react.js: -------------------------------------------------------------------------------- 1 | import React from '../react'; 2 | 3 | export default React; 4 | -------------------------------------------------------------------------------- /src/examples/todo/index.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sveinpg/build-your-own-react/60656a28fddabd1c15255537000bfb1e0614ecb7/src/examples/todo/index.css -------------------------------------------------------------------------------- /src/examples/todo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/examples/todo/index.js: -------------------------------------------------------------------------------- 1 | import React from '../react'; 2 | import ReactDOM from '../react-dom'; 3 | 4 | class TodoForm extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | 8 | this.state = { 9 | value: '' 10 | }; 11 | } 12 | 13 | render() { 14 | return ( 15 |
{ 17 | e.preventDefault(); 18 | 19 | if (this.state.value) { 20 | this.props.addTodo(this.state.value); 21 | this.setState({ value: '' }); 22 | } 23 | }} 24 | > 25 | { 30 | this.setState({value: event.target.value}); 31 | }} 32 | /> 33 | 36 |
37 | ); 38 | } 39 | } 40 | 41 | class TodoApp extends React.Component { 42 | constructor(props) { 43 | super(props); 44 | 45 | this.state = { 46 | todos: [] 47 | }; 48 | } 49 | 50 | render() { 51 | const { todos } = this.state; 52 | 53 | return ( 54 |
55 | { 57 | this.setState({ todos: [...todos, todo] }); 58 | }} 59 | /> 60 |
    61 | {this.state.todos.map((todo, index) => ( 62 |
  • 63 |

    {todo}

    64 | 76 |
  • 77 | ))} 78 |
79 |
80 | ); 81 | } 82 | } 83 | 84 | ReactDOM.render(, document.getElementById('root')); 85 | -------------------------------------------------------------------------------- /src/examples/todo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "src", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "parcel index.html", 8 | "test": "jest", 9 | "tdd": "jest --watch" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "parcel-bundler": "^1.12.4" 16 | }, 17 | "devDependencies": { 18 | "@babel/core": "^7.12.9" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sveinpg/build-your-own-react/60656a28fddabd1c15255537000bfb1e0614ecb7/src/index.css -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from './react'; 2 | import ReactDOM from './react-dom'; 3 | 4 | const Element =
Hello World!
; 5 | 6 | ReactDOM.render(, document.getElementById('root')); 7 | -------------------------------------------------------------------------------- /src/polyfill.js: -------------------------------------------------------------------------------- 1 | if (!Array.prototype.flat) { 2 | Array.prototype.flat = function() { 3 | var depth = arguments[0]; 4 | depth = depth === undefined ? 1 : Math.floor(depth); 5 | if (depth < 1) return Array.prototype.slice.call(this); 6 | return (function flat(arr, depth) { 7 | var len = arr.length >>> 0; 8 | var flattened = []; 9 | var i = 0; 10 | while (i < len) { 11 | if (i in arr) { 12 | var el = arr[i]; 13 | if (Array.isArray(el) && depth > 0) 14 | flattened = flattened.concat(flat(el, depth - 1)); 15 | else flattened.push(el); 16 | } 17 | i++; 18 | } 19 | return flattened; 20 | })(this, depth); 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /src/react-dom/VCompositeNode.js: -------------------------------------------------------------------------------- 1 | import { instantiateVNode } from './index'; 2 | 3 | export default class VCompositeNode { 4 | constructor(reactElement) {} 5 | 6 | getPublicInstance() {} 7 | 8 | update() {} 9 | 10 | mount() {} 11 | } 12 | -------------------------------------------------------------------------------- /src/react-dom/VDomNode.js: -------------------------------------------------------------------------------- 1 | import { instantiateVNode } from './index'; 2 | 3 | export default class VDomNode { 4 | constructor(reactElement) {} 5 | 6 | getPublicInstance() {} 7 | 8 | update() {} 9 | 10 | mount() {} 11 | } 12 | -------------------------------------------------------------------------------- /src/react-dom/index.js: -------------------------------------------------------------------------------- 1 | import VCompositeNode from './VCompositeNode'; 2 | import VDomNode from './VDomNode'; 3 | 4 | export function instantiateVNode(reactElement) {} 5 | 6 | function render(reactElement, domContainerNode) {} 7 | 8 | export default { 9 | _reRender: () => {}, 10 | render 11 | }; 12 | -------------------------------------------------------------------------------- /src/react/Component.js: -------------------------------------------------------------------------------- 1 | import ReactDOM from '../react-dom'; 2 | 3 | class Component { 4 | constructor(props) {} 5 | 6 | setState(state) {} 7 | 8 | render() {} 9 | } 10 | 11 | export default Component; 12 | -------------------------------------------------------------------------------- /src/react/index.js: -------------------------------------------------------------------------------- 1 | import Component from './Component'; 2 | 3 | const createElement = (type, props, ...children) => {}; 4 | 5 | export default { 6 | createElement: createElement, 7 | Component: Component 8 | }; 9 | -------------------------------------------------------------------------------- /src/solution/README.md: -------------------------------------------------------------------------------- 1 | # Solution 2 | 3 | The proposed solution of ReactDOM and React. 4 | 5 | -------------------------------------------------------------------------------- /src/solution/react-dom/README.md: -------------------------------------------------------------------------------- 1 | # ReactDOM 2 | 3 | You will find to proposed solutions of ReactDOM with some notes as to how they work in this package. 4 | -------------------------------------------------------------------------------- /src/solution/react-dom/class-cache-react-dom/README.md: -------------------------------------------------------------------------------- 1 | # ReactDOM - Class-Cache 2 | 3 | Here you will find the proposed solution to the implementation of ReactDOM with a class-cache. 4 | 5 | The reason behind the class-cache solution is to simplify the implementation. 6 | With the class-cache we can just replace every node on a render of the DOM and just pop 7 | stateful components from the cache whenever we encounter one. That way, we won't loose that state of the component 8 | when re-rendering. This is instead of having to traverse the virtual-DOM tree and compare every child-node with it's 9 | previous instance and then removing, replacing or appending that child to the node. 10 | -------------------------------------------------------------------------------- /src/solution/react-dom/class-cache-react-dom/VCompositeNode.js: -------------------------------------------------------------------------------- 1 | import { instantiateVNode } from './index'; 2 | 3 | export default class VCompositeNode { 4 | static isReactClassComponent(type) { 5 | return type.prototype && type.prototype.isReactComponent; 6 | } 7 | 8 | static isVCompositeNode(type) { 9 | return typeof type === 'function'; 10 | } 11 | 12 | constructor(reactElement) { 13 | this.currentReactElement = reactElement; 14 | this.classInstance = null; 15 | } 16 | 17 | getPublicInstance() { 18 | return this.classInstance; 19 | } 20 | 21 | mount(classCache) { 22 | const { 23 | type, 24 | props, 25 | } = this.currentReactElement; 26 | 27 | let renderedInstance; 28 | if (VCompositeNode.isReactClassComponent(type)) { 29 | const cacheIndex = classCache.index++; 30 | const cachedInstance = classCache.cache[cacheIndex]; 31 | 32 | const instance = cachedInstance ? cachedInstance : new type(props); 33 | instance.props = props; 34 | 35 | classCache.cache[cacheIndex] = instance; 36 | 37 | renderedInstance = instantiateVNode(instance.render()); 38 | this.classInstance = instance; 39 | } else { 40 | renderedInstance = instantiateVNode(type(props)); 41 | this.classInstance = null; 42 | } 43 | 44 | return renderedInstance.mount(classCache); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/solution/react-dom/class-cache-react-dom/VDomNode.js: -------------------------------------------------------------------------------- 1 | import { instantiateVNode } from './index'; 2 | 3 | export default class VDomNode { 4 | static isEmpty(reactElement) { 5 | return reactElement === undefined || reactElement == null; 6 | } 7 | 8 | static isPrimitive(reactElement) { 9 | return !reactElement.type && 10 | (typeof reactElement === 'string' || typeof reactElement === 'number'); 11 | } 12 | 13 | static getChildrenAsArray(props) { 14 | const { children = [] } = props || {}; 15 | return !Array.isArray(children) ? [children] : children; 16 | } 17 | 18 | static setAttributes(domNode, props = {}) { 19 | const { 20 | className, 21 | style, 22 | ...restProps 23 | } = props; 24 | 25 | // Set className 26 | if (className) { 27 | domNode.className = className; 28 | } 29 | 30 | // Set styles 31 | if (style) { 32 | Object.entries(style).forEach(([key, value]) => { 33 | domNode.style[key] = value; 34 | }); 35 | } 36 | 37 | // Add event listeners and other props 38 | Object.entries(restProps).forEach(([key, value]) => { 39 | if (key === 'children') { 40 | return; 41 | } 42 | 43 | if (/^on.*$/.test(key)) { 44 | domNode.addEventListener(key.substring(2).toLowerCase(), value); 45 | } else if (key === 'value') { 46 | domNode.value = value; 47 | } else { 48 | domNode.setAttribute(key, value); 49 | } 50 | }); 51 | } 52 | 53 | static buildDomNode(reactElement) { 54 | if (VDomNode.isEmpty(reactElement)) { 55 | return document.createTextNode(''); // Empty node 56 | } 57 | 58 | if (VDomNode.isPrimitive(reactElement)) { 59 | return document.createTextNode(reactElement); 60 | } 61 | 62 | const { 63 | type, 64 | props, 65 | } = reactElement; 66 | 67 | const domNode = document.createElement(type); 68 | VDomNode.setAttributes(domNode, props); 69 | 70 | return domNode; 71 | } 72 | 73 | constructor(reactElement) { 74 | this.currentReactElement = reactElement; 75 | this.domNode = null; 76 | } 77 | 78 | getPublicInstance() { 79 | return this.domNode; 80 | } 81 | 82 | mount(classCache) { 83 | const { props } = this.currentReactElement || {}; 84 | 85 | this.domNode = VDomNode.buildDomNode(this.currentReactElement); 86 | 87 | const childrenVNodes = VDomNode.getChildrenAsArray(props).map(instantiateVNode); 88 | 89 | for (const childVNode of childrenVNodes) { 90 | const childDomNode = childVNode.mount(classCache); 91 | this.domNode.appendChild(childDomNode); 92 | } 93 | 94 | return this.domNode; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/solution/react-dom/class-cache-react-dom/index.js: -------------------------------------------------------------------------------- 1 | import VCompositeNode from './VCompositeNode'; 2 | import VDomNode from './VDomNode'; 3 | 4 | const root = {}; 5 | const classCache = { 6 | index: -1, 7 | cache: [] 8 | }; 9 | 10 | export function instantiateVNode(reactElement) { 11 | const { type } = reactElement || {}; 12 | 13 | if (VCompositeNode.isVCompositeNode(type)) { 14 | return new VCompositeNode(reactElement); 15 | } 16 | 17 | return new VDomNode(reactElement); 18 | } 19 | 20 | function render( 21 | reactElement = root.reactElement, 22 | domContainerNode = root.domContainerNode 23 | ) { 24 | if (root.domContainerNode) { 25 | domContainerNode.innerHTML = ''; 26 | classCache.index = -1; 27 | } 28 | 29 | const vNode = instantiateVNode(reactElement); 30 | const domNode = vNode.mount(classCache); 31 | 32 | domContainerNode.appendChild(domNode); 33 | 34 | root.reactElement = reactElement; 35 | root.domContainerNode = domContainerNode; 36 | 37 | return vNode.getPublicInstance(); 38 | } 39 | 40 | export default { 41 | _reRender: () => setTimeout(render, 0), 42 | render 43 | }; 44 | -------------------------------------------------------------------------------- /src/solution/react-dom/index.js: -------------------------------------------------------------------------------- 1 | import ReactDOM from './react-dom'; 2 | export { instantiateVNode } from './react-dom'; 3 | 4 | export default ReactDOM; 5 | -------------------------------------------------------------------------------- /src/solution/react-dom/react-dom/README.md: -------------------------------------------------------------------------------- 1 | # ReactDOM 2 | 3 | Here you will find the proposed solution to the implementation of ReactDOM without a class-cache. 4 | 5 | This version of ReactDOM is implemented without a class-cache. The implementation is a little bit more advanced than 6 | the class-cache version. Stateful components are stored as instances on the `VCompositeNode` instead of a 7 | "centralized" cache, as we did with the class-cache. We therefore need to traverse the tree of virtual-DOM nodes and 8 | compare them to the previous virtual-DOM node. We then remove, append, replace, or update the children of nodes 9 | accordingly to the changes made since last render. This way, when the type (and order) of a child node that is 10 | stateful has not changed, we will just update the node with new props and therefor the state is kept in tact. 11 | -------------------------------------------------------------------------------- /src/solution/react-dom/react-dom/VCompositeNode.js: -------------------------------------------------------------------------------- 1 | import { instantiateVNode } from './index'; 2 | import VDomNode from './VDomNode'; 3 | 4 | export default class VCompositeNode { 5 | static isReactClassComponent(type) { 6 | return type.prototype && type.prototype.isReactComponent; 7 | } 8 | 9 | static isVCompositeNode(type) { 10 | return typeof type === 'function'; 11 | } 12 | 13 | constructor(reactElement) { 14 | this.currentReactElement = reactElement; 15 | this.classInstance = null; 16 | this.renderedInstance = null; 17 | } 18 | 19 | getPublicInstance() { 20 | return this.classInstance; 21 | } 22 | 23 | getCurrentReactElement() { 24 | return this.currentReactElement; 25 | } 26 | 27 | getDomNode() { 28 | return this.renderedInstance.getDomNode(); 29 | } 30 | 31 | update(nextReactElement) { 32 | const { 33 | type, 34 | props: nextProps, 35 | } = nextReactElement; 36 | 37 | const nextRenderedReactElement = (() => { 38 | if (VCompositeNode.isReactClassComponent(type)) { 39 | this.classInstance.props = nextProps; 40 | return this.classInstance.render(); 41 | } 42 | return type(nextProps); 43 | })(); 44 | 45 | const prevRenderedReactElement = this.renderedInstance.getCurrentReactElement(); 46 | 47 | const isTypeDefined = VDomNode.isTypeDefined(prevRenderedReactElement) 48 | && VDomNode.isTypeDefined(nextRenderedReactElement); 49 | 50 | if (isTypeDefined && prevRenderedReactElement.type === nextRenderedReactElement.type) { 51 | this.renderedInstance.update(nextRenderedReactElement); 52 | } else { 53 | const nextRenderedInstance = instantiateVNode(nextRenderedReactElement); 54 | 55 | const prevDomNode = this.getDomNode(); 56 | const nextDomNode = nextRenderedInstance.mount(); 57 | prevDomNode.parentNode.replaceChild(nextDomNode, prevDomNode); 58 | 59 | this.renderedInstance = nextRenderedInstance; 60 | } 61 | } 62 | 63 | mount() { 64 | const { 65 | type, 66 | props, 67 | } = this.currentReactElement; 68 | 69 | if (VCompositeNode.isReactClassComponent(type)) { 70 | this.classInstance = new type(props); 71 | this.renderedInstance = instantiateVNode(this.classInstance.render()); 72 | } else { 73 | this.classInstance = null; 74 | this.renderedInstance = instantiateVNode(type(props)); 75 | } 76 | 77 | return this.renderedInstance.mount(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/solution/react-dom/react-dom/VDomNode.js: -------------------------------------------------------------------------------- 1 | import { instantiateVNode } from './index'; 2 | 3 | export default class VDomNode { 4 | static isTypeDefined(reactElement) { 5 | return !VDomNode.isEmpty(reactElement) && reactElement.type; 6 | } 7 | 8 | static isEmpty(reactElement) { 9 | return reactElement === undefined || reactElement === null; 10 | } 11 | 12 | static isPrimitive(reactElement) { 13 | return !reactElement.type && 14 | (typeof reactElement === 'string' || typeof reactElement === 'number'); 15 | } 16 | 17 | static getChildrenAsArray(props) { 18 | const { children = [] } = props || {}; 19 | return !Array.isArray(children) ? [children] : children; 20 | } 21 | 22 | static setAttributes(domNode, nextProps = {}, prevProps = {}) { 23 | const { 24 | className: prevClass, 25 | style: prevStyle = {}, 26 | ...prevRestProps 27 | } = prevProps; 28 | 29 | const { 30 | className, 31 | style = {}, 32 | ...restProps 33 | } = nextProps; 34 | 35 | // Set className 36 | if (className) { 37 | domNode.className = className; 38 | } 39 | 40 | // Remove outdated styles 41 | Object.keys(prevStyle) 42 | .filter(key => !style[key]) 43 | .forEach((key) => { 44 | domNode.style[key] = ''; 45 | }); 46 | 47 | // Set styles 48 | Object.entries(style).forEach(([key, value]) => { 49 | domNode.style[key] = value; 50 | }); 51 | 52 | 53 | // Remove outdated event listeners and other props 54 | Object.entries(prevRestProps) 55 | .filter(([key]) => !restProps[key]) 56 | .forEach(([key, value]) => { 57 | if (key === 'children') { 58 | return; 59 | } 60 | 61 | if (/^on.*$/.test(key)) { 62 | domNode.removeEventListener(key.substring(2).toLowerCase(), value); 63 | } else if (key === 'value') { 64 | domNode.value = ''; 65 | } else { 66 | domNode.removeAttribute(key); 67 | } 68 | }); 69 | 70 | // Add event listeners and other props 71 | Object.entries(restProps).forEach(([key, value]) => { 72 | if (key === 'children') { 73 | return; 74 | } 75 | 76 | if (/^on.*$/.test(key)) { 77 | const event = key.substring(2).toLowerCase(); 78 | 79 | // Remove previous event listener for same event or else we will have two listeners for the same event 80 | if (prevProps[key]) { 81 | domNode.removeEventListener(event, prevRestProps[key]); 82 | } 83 | domNode.addEventListener(event, value); 84 | } else if (key === 'value') { 85 | domNode.value = value; 86 | } else { 87 | domNode.setAttribute(key, value); 88 | } 89 | }); 90 | } 91 | 92 | static buildDomNode(reactElement) { 93 | if (VDomNode.isEmpty(reactElement)) { 94 | return document.createTextNode(''); // Empty node 95 | } 96 | 97 | if (VDomNode.isPrimitive(reactElement)) { 98 | return document.createTextNode(reactElement); 99 | } 100 | 101 | const { 102 | type, 103 | props, 104 | } = reactElement; 105 | 106 | const domNode = document.createElement(type); 107 | VDomNode.setAttributes(domNode, props); 108 | 109 | return domNode; 110 | } 111 | 112 | constructor(reactElement) { 113 | this.currentReactElement = reactElement; 114 | this.domNode = null; 115 | this.childrenVNodes = []; 116 | } 117 | 118 | getPublicInstance() { 119 | return this.getDomNode(); 120 | } 121 | 122 | getDomNode() { 123 | return this.domNode; 124 | } 125 | 126 | getCurrentReactElement() { 127 | return this.currentReactElement; 128 | } 129 | 130 | update(nextReactElement) { 131 | const { 132 | props: prevProps = {}, 133 | } = this.currentReactElement || {}; 134 | 135 | const { 136 | children: currentChildren = {}, 137 | } = prevProps; 138 | 139 | const { 140 | props: nextProps = {}, 141 | } = nextReactElement || {}; 142 | 143 | const { 144 | children: nextChildren = [] 145 | } = nextProps; 146 | 147 | const nextChildrenVNodes = []; 148 | const maxSize = Math.max(nextChildren.length, currentChildren.length); 149 | for (let i=0; i setTimeout(render, 0), 47 | render 48 | }; 49 | -------------------------------------------------------------------------------- /src/solution/react/Component.js: -------------------------------------------------------------------------------- 1 | import ReactDOM from '../react-dom'; 2 | 3 | class Component { 4 | constructor(props) { 5 | this.props = props; 6 | } 7 | 8 | setState(state) { 9 | // Do not rerender if setState is called with null or undefined 10 | if (state == null) { 11 | return; 12 | } 13 | 14 | if (typeof state === 'function') { 15 | this.state = { ...this.state, ...state(this.state) }; 16 | } else { 17 | this.state = { ...this.state, ...state }; 18 | } 19 | 20 | ReactDOM._reRender(); 21 | } 22 | 23 | render() { 24 | throw new Error('React.Component may not be used directly. Create your own class which extends this class.'); 25 | } 26 | } 27 | 28 | Component.prototype.isReactComponent = true; 29 | 30 | export default Component; 31 | -------------------------------------------------------------------------------- /src/solution/react/README.md: -------------------------------------------------------------------------------- 1 | # React 2 | 3 | Here you will find the proposed solution to the implementation of React. 4 | It is fairly simple with a react-component base class and an index with `React.createElement` in it. 5 | -------------------------------------------------------------------------------- /src/solution/react/index.js: -------------------------------------------------------------------------------- 1 | import Component from './Component'; 2 | 3 | const createElement = (type, props, ...children) => ({ 4 | $$typeof: Symbol.for('react.element'), 5 | type: type, 6 | props: { 7 | children: children.flat(1), 8 | ...props 9 | }, 10 | ref: null, 11 | _owner: null 12 | }); 13 | 14 | export default { 15 | createElement: createElement, 16 | Component: Component 17 | }; 18 | -------------------------------------------------------------------------------- /src/test-utils.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/extend-expect'; 2 | import regeneratorRuntime from 'regenerator-runtime'; 3 | import './polyfill'; 4 | 5 | export function getExampleDOM() { 6 | const div = document.createElement('div'); 7 | div.id = 'root'; 8 | return div; 9 | } 10 | --------------------------------------------------------------------------------