├── .github └── FUNDING.yml ├── README.md └── manuscript ├── Book.txt ├── chapter1.md ├── chapter2.md ├── chapter3.md ├── chapter4.md ├── chapter5.md ├── chapter6.md ├── contributor.md ├── deployChapter.md ├── finalwords.md └── foreword.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: rwieruch 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with a single custom sponsorship URL 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # เส้นทางสู่การเรียนรู้ React [หนังสือ] 2 | 3 | [![Slack](https://slack-the-road-to-learn-react.wieruch.com/badge.svg)](https://slack-the-road-to-learn-react.wieruch.com/) 4 | 5 | Repository ทางการสำหรับ [เส้นทางสู่การเรียนรู้ React](https://github.com/the-road-to-learn-react/the-road-to-learn-react-thai). ถ้าต้องการที่จะรีวิว, กรุณาไปเขียนได้ที่ [Amazon](https://www.amazon.com/dp/B077HJFCQX?tag=21moves-20) หรือ [Goodreads](https://www.goodreads.com/book/show/37503118-the-road-to-learn-react). 6 | 7 | ## ภาษาอื่นๆ ของหนังสือนี้ที่ Leanpub 8 | 9 | เส้นทางสู่การเรียนรู้ React มีในภาษาอื่นๆ ด้วย ขอบคุณผู้แปลทั้งหลายสำหรับการแปลที่สุดยอดจริงๆ! 10 | 11 | - 🇨🇳 [จีน](https://leanpub.com/the-road-to-learn-react-chinese) 12 | - 🇫🇷 [ฝรั่งเศส](https://leanpub.com/the-road-to-learn-react-french) 13 | - 🇰🇷 [เกาหลี](https://leanpub.com/the-road-to-learn-react-korean) 14 | - 🇧🇷 [โปรตุเกส](https://leanpub.com/the-road-to-learn-react-portuguese) 15 | - 🇷🇺 [รัสเซีย](https://leanpub.com/the-road-to-learn-react-russian) 16 | - 🇪🇸 [เสปน](https://leanpub.com/the-road-to-learn-react-spanish) 17 | - 🇮🇹 [อิตาลี](https://leanpub.com/the-road-to-learn-react-italian) 18 | - 🇹🇭 [ไทย](https://leanpub.com/the-road-to-learn-react-thai) 19 | 20 | ## การปรับปรุง, ช่วยเหลือ และการสนับสนุน 21 | 22 | - รับทราบการปรับปรุงผ่านทาง [E-Mail](https://www.getrevue.co/profile/rwieruch) หรือ [Twitter](https://twitter.com/rwieruch) 23 | - รับการช่วยเหลือ, ระหว่างเรียนรู้ หรือสอนคนอื่นผ่านทาง [Slack Channel](https://slack-the-road-to-learn-react.wieruch.com/) 24 | - วิธีร่วม [สนันสนุน](https://www.robinwieruch.de/about/) หนังสือนี้ 25 | 26 | ## ช่วยกันทำ 27 | 28 | คุณสามารถช่วยทำหนังสือนี้ให้ดีขึ้นได้ด้วยการเปิด issue และสร้าง Pull Requests (PR) 29 | 30 | คุณสามารถเปิด PR เพื่อแก้คำผิด หรืออธิบายบางบทเรียนให้มากขึ้น เพราะเวลาเขียนหนังสือทางเทคนิค ก็มักจะมองข้ามส่วนที่ต้องการการอธิบายไป หรือบางทีก็อธิบายไปแล้ว 31 | 32 | หรือคุณก็ยังสามารถเปิด issues ถ้าคุณเจอปัญหา และเพื่อเป็นการง่ายในการแก้ปัญหานั้น คุณก็ควรให้รายละเอียดมาด้วย เช่น log, รูปภาพ, หน้าที่ไหร่, node เวอร์ชั่นอะไร (command line: `node -v`) และ link ไปยัง repository ของคุณ พวกนี้ไม่ได้จำเป็นทั้งหมด แต่ทั้งหมดนี้จะช่วยให้แก้ปัญหา หรือปรับปรุงหนังสือได้ 33 | 34 | ขอบคุณที่ช่วยกันนะ! 35 | -------------------------------------------------------------------------------- /manuscript/Book.txt: -------------------------------------------------------------------------------- 1 | {frontmatter} 2 | 3 | foreword.md 4 | 5 | {mainmatter} 6 | 7 | chapter1.md 8 | chapter2.md 9 | chapter3.md 10 | chapter4.md 11 | chapter5.md 12 | chapter6.md 13 | deployChapter.md 14 | 15 | {backmatter} 16 | 17 | finalwords.md -------------------------------------------------------------------------------- /manuscript/chapter1.md: -------------------------------------------------------------------------------- 1 | # เริ่มรู้จัก React 2 | 3 | ในบทนี้จะเป็นการแนะนำให้รู้จัก React, มันคือ JavaScript library สำหรับสร้าง interface ใน single-page แอพ และแอพบนมือถือ, โดยจะบอกว่าทำไมนักพัฒนาควรจะเลือกใช้ React เป็นเครื่องมือ เราจะเจาะลึกไปถึง React ecosystem, สร้าง React แอพจากที่ไม่มีอะไรเลยโดยไม่ต้องปรับแต่งอะไร และในระหว่างทางเราก็จะแนะนำให้รู้จัก *JSX* (syntax ของ React) และ *ReactDOM* เพื่อคุณจะได้เข้าใจการใช้งาน React สำหรับเว็บแอพสมัยใหม่ 4 | 5 | ## Hi, my name is React. 6 | 7 | Single page applications ([SPA](https://en.wikipedia.org/wiki/Single-page_application)) have become increasingly popular in recent years, as frameworks like Angular, Ember, and Backbone allow JavaScript developers to build modern web applications using techniques beyond vanilla JavaScript and jQuery. The three mentioned are among the first SPAs, each coming into its own between 2010 and 2011, but there are many more options for single-page development. The first generation of SPA frameworks arrived at the enterprise level, so their frameworks are more rigid. React, on the other hand, remains an innovative library that has been adopted by many technological leaders like [Airbnb, Netflix, and Facebook](https://github.com/facebook/react/wiki/Sites-Using-React). 8 | 9 | React was released by Facebook's web development team in 2013 as a view library, which makes it the 'V' in the [MVC](https://en.wikipedia.org/wiki/Model–view–controller) (model view controller). As a view, it allows you to render components as viewable elements in a browser, while its ecosystem lets us build single page applications. While the first generation of frameworks tried to solve many things at once, React is only used to build your view layer; specifically, it is a library wherein the view is a hierarchy of composable components. If you haven't heard about MVC before, don't bother about it, because it's just there to put React historically into context for people who come from other programming languages. 10 | 11 | In React, the focus remains on the view layer until more aspects are introduced to the application. These are the building blocks for an SPA, which are essential to build a mature application. They come with two advantages: 12 | 13 | * You can learn the building blocks one at a time without having to understand them altogether. In contrast, an SPA framework gives you every building block from the start. This book focuses on React as the first building block. More building blocks will eventually follow. 14 | 15 | * All building blocks are interchangeable, which makes the ecosystem around React highly innovative. Multiple solutions can compete with each other, and you can choose the most appealing solution for any given challenge. 16 | 17 | React is one of the best choices for building modern web applications. Again, it only delivers the view layer, but the surrounding ecosystem makes up an entirely flexible and interchangeable framework. React has a slim API, a robust and evolving ecosystem, and a great community. 18 | 19 | ### Exercises 20 | 21 | If you'd like to know more about why I chose React, or to find a more in-depth look at the topics mentioned above, these articles grant a deeper perspective: 22 | 23 | * [Why I moved from Angular to React](https://www.robinwieruch.de/reasons-why-i-moved-from-angular-to-react/) 24 | * [React's flexible ecosystem](https://www.robinwieruch.de/essential-react-libraries-framework/) 25 | * [How to learn a framework](https://www.robinwieruch.de/how-to-learn-framework/) 26 | 27 | ## Requirements 28 | 29 | To follow this book, you should be familiar with the basics of web development, i.e how to use HTML, CSS, and JavaScript. It also makes sense to understand how [APIs](https://www.robinwieruch.de/what-is-an-api-javascript/) work, as they will be covered thoroughly. Also, I encourage you to join the official [Slack Group](https://slack-the-road-to-learn-react.wieruch.com/) to be a part of a growing React community where you can learn from and help others. 30 | 31 | ### Editor and Terminal 32 | 33 | For the lessons, you will need a text editor or an IDE and terminal (command line tool). I have provided [a setup guide](https://www.robinwieruch.de/developer-setup/) if you need additional help. Optionally, we recommend you keep your projects in GitHub while conducting the exercises in this book. There is a [short guide](https://www.robinwieruch.de/git-essential-commands/) on how to use these tools. Github has excellent version control, so you can see what changes were made if you make a mistake or just want a more direct way to follow along. 34 | 35 | ### Node and NPM 36 | 37 | Finally, you will need an installation of [node and npm](https://nodejs.org/en/). Both are used to manage libraries you will need along the way. In this book, you will install external node packages via npm (node package manager). These node packages can be libraries or whole frameworks. 38 | 39 | You can verify your versions of node and npm on the command line. If you don't get any output in the terminal, you need to install node and npm first. These are my versions at the time of writing this book: 40 | 41 | {title="Command Line",lang="text"} 42 | ~~~~~~~~ 43 | node --version 44 | *v10.11.0 45 | npm --version 46 | *v6.9.0 47 | ~~~~~~~~ 48 | 49 | The additional content of this section is a crash course in node and npm. It is not exhaustive, but it will cover all of the necessary tools. If you are familiar with both of them, you can skip this section. 50 | 51 | The **node package manager** (npm) installs external node packages from the command line. These packages can be a set of utility functions, libraries, or whole frameworks, and they are the dependencies of your application. You can either install these packages to your global node package folder, or to your local project folder. 52 | 53 | Global node packages are accessible from everywhere in the terminal, and only need to be installed to the global directory once. Install a global package by typing the following into a terminal: 54 | 55 | {title="Command Line",lang="text"} 56 | ~~~~~~~~ 57 | npm install -g 58 | ~~~~~~~~ 59 | 60 | The `-g` flag tells npm to install the package globally. Local packages are used in your application by default. For our purposes, we will install React to the local directory terminal by typing: 61 | 62 | {title="Command Line",lang="text"} 63 | ~~~~~~~~ 64 | npm install react 65 | ~~~~~~~~ 66 | 67 | The installed package will automatically appear in a folder called *node_modules/* and will be listed in the *package.json* file next to your other dependencies. 68 | 69 | To initialize the *node_modules/* folder and the *package.json* file for your project, use the following npm command. Then, you can install new local packages via npm: 70 | 71 | {title="Command Line",lang="text"} 72 | ~~~~~~~~ 73 | npm init -y 74 | ~~~~~~~~ 75 | 76 | The `-y` flag initializes all the defaults in your *package.json*. After initializing your npm project, you are ready to install new packages via `npm install `. 77 | 78 | The *package.json* file allows you to share your project with other developers without sharing all the node packages. It will contain references to all node packages used in your project, called **dependencies**. Other users can copy a project without the dependencies using the references in *package.json*, where the references make it easy to install all packages using `npm install`. A `npm install` script will take all the dependencies listed in the *package.json* file and install them in the *node_modules/* folder. 79 | 80 | Finally, there's one more command to cover about npm: 81 | 82 | {title="Command Line",lang="text"} 83 | ~~~~~~~~ 84 | npm install --save-dev 85 | ~~~~~~~~ 86 | 87 | The `--save-dev` flag indicates that the node package is only used in the development environment, meaning it won't be used in when the application is deployed to a the server or used in production. It is useful for testing an application using a node package, but want to exclude it from your production environment. 88 | 89 | Some of you may want to use other package managers to work with node packages in your applications. **Yarn** is a dependency manager that works similar to **npm**. It has its own list of commands, but you still have access to the same npm registry. Yarn was created to solve issues npm couldn't, but both tools have evolved to the point where either will suffice today. 90 | 91 | ### Exercises: 92 | 93 | * Set up a throw away npm project using the terminal: 94 | * Create a new folder with `mkdir ` 95 | * Navigate into the folder with `cd ` 96 | * Execute `npm init -y` or `npm init` 97 | * Install a local package like React with `npm install react` 98 | * Check the *package.json* file and the *node_modules/* folder 99 | * Attempt to uninstall and reinstall the *react* node package 100 | * Read about [npm](https://docs.npmjs.com/) 101 | * Read about [yarn](https://yarnpkg.com/en/docs/) package manager 102 | 103 | ## Installation 104 | 105 | There are many approaches to getting started with a React application. The first we'll explore is a CDN, short for [content delivery network](https://en.wikipedia.org/wiki/Content_delivery_network). Don't worry too much about CDNs now, because you will not use them in this book, but it makes sense to explain them briefly. Many companies use CDNs to host files publicly for their consumers. Some of these files are libraries like React, since the bundled React library is just a *react.js* JavaScript file. 106 | 107 | To get started with React by using a CDN, find the ` 115 | 116 | 120 | ~~~~~~~~ 121 | 122 | You can also get React into your application by initializing it as node project. With a *package.json* file, you can install *react* and *react-dom* from the command line. However, the folder must be initialized as a npm project using `npm init -y` with a *package.json* file. You can install multiple node packages with npm: 123 | 124 | {title="Command Line",lang="text"} 125 | ~~~~~~~~ 126 | npm install react react-dom 127 | ~~~~~~~~ 128 | 129 | This approach is often used to add React to an existing application managed with npm. 130 | 131 | You may also have to deal with [Babel](http://babeljs.io/) to make your application aware of JSX (the React syntax) and JavaScript ES6. Babel transpiles your code--that is, it converts it to vanilla JavaScript--so most modern browsers can interpret JavaScript ES6 and JSX. Because of this difficult setup, Facebook introduced *create-react-app* as a zero-configuration React solution. The next section will show you how to setup your application using this bootstrapping tool. 132 | 133 | ### Exercises: 134 | 135 | * Read about [React installations](https://reactjs.org/docs/getting-started.html) 136 | 137 | ## Zero-Configuration Setup 138 | 139 | In the Road to learn React, we will be using [create-react-app](https://github.com/facebookincubator/create-react-app) to bootstrap your application. It's an opinionated yet zero-configuration starter kit for React introduced by Facebook in 2016, [recommended for beginners by 96% of React users](https://twitter.com/dan_abramov/status/806985854099062785). In *create-react-app* the tooling and configuration evolve in the background, while the focus is on the application implementation. 140 | 141 | To get started, install the package to your global node packages, which keeps it available on the command line to bootstrap new React applications: 142 | 143 | {title="Command Line",lang="text"} 144 | ~~~~~~~~ 145 | npm install -g create-react-app 146 | ~~~~~~~~ 147 | 148 | You can check the version of *create-react-app* to verify a successful installation on your command line: 149 | 150 | {title="Command Line",lang="text"} 151 | ~~~~~~~~ 152 | create-react-app --version 153 | *v2.0.2 154 | ~~~~~~~~ 155 | 156 | Now you are ready to bootstrap your first React application. The example will be referred to as *hackernews*, but you may choose any name you like. First, navigate into the folder: 157 | 158 | {title="Command Line",lang="text"} 159 | ~~~~~~~~ 160 | create-react-app hackernews 161 | cd hackernews 162 | ~~~~~~~~ 163 | 164 | Now you can open the application in your editor. The following folder structure, or a variation of it depending on the *create-react-app* version, should be presented to you: 165 | 166 | {title="Folder Structure",lang="text"} 167 | ~~~~~~~~ 168 | hackernews/ 169 | README.md 170 | node_modules/ 171 | package.json 172 | .gitignore 173 | public/ 174 | favicon.ico 175 | index.html 176 | manifest.json 177 | src/ 178 | App.css 179 | App.js 180 | App.test.js 181 | index.css 182 | index.js 183 | logo.svg 184 | serviceWorker.js 185 | ~~~~~~~~ 186 | 187 | This is a breakdown of the folders and files: 188 | 189 | * **README.md:** The .md extension indicates the file is a markdown file. Markdown is used as a lightweight markup language with plain text formatting syntax. Many source code projects come with a *README.md* file to give you initial instructions about the project. When pushing your project to a platform such as GitHub, the *README.md* file usually displays information about the content contained in the repository. Because you used *create-react-app*, your *README.md* should be the same as the official [create-react-app GitHub repository](https://github.com/facebookincubator/create-react-app). 190 | 191 | * **node_modules/:** This folder contains all node packages that have been installed via npm. Since you used *create-react-app*, there should already be a couple of node modules installed for you. You will rarely touch this folder, because node packages are generally installed and uninstalled with npm from the command line. 192 | 193 | * **package.json:** This file shows you a list of node package dependencies and other project configurations. 194 | 195 | * **.gitignore:** This file displays all files and folders that shouldn't be added to your git repository when using git; such files and folders should only be located in your local project. The *node_modules/* folder is one example. It is enough to share the *package.json* file with others, so they can install dependencies on their end with `npm install` without your dependency folder. 196 | 197 | * **public/:** This folder holds development files, such as *public/index.html*. The index is displayed on localhost:3000 when developing your app. The boilerplate takes care of relating this index with all the scripts in *src/*. 198 | 199 | * **build/** This folder is created when you build the project for production, as it holds all of the production files. When building your project for production, all the source code in the *src/* and *public/* folders are bundled and placed in the build folder. 200 | 201 | * **manifest.json** and **serviceWorker.js:** These files won't be used for this project, so you can ignore them for now. 202 | 203 | In the beginning, everything you need is located in the *src/* folder. The main focus lies on the *src/App.js* file which is used to implement React components. It will be used to implement your application, but later you might want to split up your components into multiple files, where each file maintains one or more components on its own. 204 | 205 | Additionally, you will find a *src/App.test.js* file for your tests, and a *src/index.js* as an entry point to the React world. You will get to know both files intimately in a later chapter. There is also a *src/index.css* and a *src/App.css* file to style your general application and components, which comes with the default style when you open them. You will modify them later as well. 206 | 207 | The *create-react-app* application is a npm project you can use to install and uninstall node packages. It comes with the following npm scripts for your command line: 208 | 209 | {title="Command Line",lang="text"} 210 | ~~~~~~~~ 211 | # Runs the application in http://localhost:3000 212 | npm start 213 | 214 | # Runs the tests 215 | npm test 216 | 217 | # Builds the application for production 218 | npm run build 219 | ~~~~~~~~ 220 | 221 | The scripts are defined in your *package.json*, and your basic React application is bootstrapped. The following exercises will finally allow you to run your bootstrapped application in a browser. 222 | 223 | ### Exercises: 224 | 225 | * Confirm your [source code for the last section](http://bit.ly/2Tt3Vd8) 226 | * Confirm the [changes from the last section](http://bit.ly/2Cc5CRw) 227 | * `npm start` your application and visit the application in your browser (Exit the command by pressing Control + C) 228 | * Run the `npm test` script 229 | * Run the `npm run build` script and verify that a *build/* folder was added to your project (you can remove it afterward. Note that the build folder can be used later on to [deploy your application](https://www.robinwieruch.de/deploy-applications-digital-ocean/)) 230 | * Familiarize yourself with the folder structure 231 | * Check the content of the files 232 | * Read about [npm scripts and create-react-app](https://github.com/facebookincubator/create-react-app) 233 | 234 | ## Introduction to JSX 235 | 236 | Now we will get to know JSX, the syntax in React. As mentioned before, *create-react-app* has already bootstrapped a basic application for you, and all files come with their own default implementations. For now, the only file we will modify is the *src/App.js* file. 237 | 238 | {title="src/App.js",lang="javascript"} 239 | ~~~~~~~~ 240 | import React, { Component } from 'react'; 241 | import logo from './logo.svg'; 242 | import './App.css'; 243 | 244 | class App extends Component { 245 | render() { 246 | return ( 247 |
248 |
249 | logo 250 |

251 | Edit src/App.js and save to reload. 252 |

253 | 259 | Learn React 260 | 261 |
262 |
263 | ); 264 | } 265 | } 266 | 267 | export default App; 268 | ~~~~~~~~ 269 | 270 | Don't worry if you're confused by the import/export statements and class declaration now. These are features of JavaScript ES6 we will revisit in a later chapter. 271 | 272 | In the file you should see a **React ES6 class component** with the name App. This is a component declaration. After you have declared a component, you can use it as an element anywhere in your application. It will produce an **instance** of your **component** or, in other words, the component gets instantiated. 273 | 274 | {title="Code Playground",lang="javascript"} 275 | ~~~~~~~~ 276 | // component declaration 277 | class App extends Component { 278 | ... 279 | } 280 | 281 | // component usage (also called instantiation for a class) 282 | // creates an instance of the component 283 | 284 | ~~~~~~~~ 285 | 286 | The returned **element** is specified in the `render()` method. The components you instantiated earlier are made up of elements, so it is important to understand the differences between a component, an instance of a component, and an element. 287 | 288 | You should see where the App component is instantiated, else you couldn't see the rendered output in a browser. The App component is only the declaration, but not the usage. You can instantiate the component anywhere in your JSX with ``. You will see later where this happens in this application. 289 | 290 | The content in the render block may look similar to HTML, but it is actually JSX. JSX allows you to mix HTML and JavaScript. It is powerful, but it can be confusing when you are used to separating the two languages. It is a good idea to start by using basic HTML in your JSX. Open the `App.js` file and remove all unnecessary HTML code as shown: 291 | 292 | {title="src/App.js",lang="javascript"} 293 | ~~~~~~~~ 294 | import React, { Component } from 'react'; 295 | import './App.css'; 296 | 297 | class App extends Component { 298 | render() { 299 | return ( 300 |
301 |

Welcome to the Road to learn React

302 |
303 | ); 304 | } 305 | } 306 | 307 | export default App; 308 | ~~~~~~~~ 309 | 310 | Now, you only return HTML in your `render()` method without any JavaScript. Let's define the "Welcome to the Road to learn React" as a variable. A variable is set in JSX by curly braces. 311 | 312 | {title="src/App.js",lang="javascript"} 313 | ~~~~~~~~ 314 | import React, { Component } from 'react'; 315 | import './App.css'; 316 | 317 | class App extends Component { 318 | render() { 319 | # leanpub-start-insert 320 | var helloWorld = 'Welcome to the Road to learn React'; 321 | # leanpub-end-insert 322 | return ( 323 |
324 | # leanpub-start-insert 325 |

{helloWorld}

326 | # leanpub-end-insert 327 |
328 | ); 329 | } 330 | } 331 | 332 | export default App; 333 | ~~~~~~~~ 334 | 335 | Start your application on the command line with `npm start` to verify the changes you've made. 336 | 337 | You might have noticed the `className` attribute. It reflects the standard `class` attribute in HTML. JSX had replaced a handful of internal HTML attributes, but you can find all the [supported HTML attributes in React's documentation](https://reactjs.org/docs/dom-elements.html#all-supported-html-attributes), which all follow the camelCase convention. On your way to learn React, expect to run across more JSX specific attributes. 338 | 339 | ### Exercises: 340 | 341 | * Confirm your [source code for the last section](http://bit.ly/2H8H14h) 342 | * Confirm the [changes from the last section](http://bit.ly/2H9KwHA) 343 | * Define more variables and render them in JSX 344 | * Use a complex object to represent a user with a first name and last name 345 | * Render the user properties in JSX 346 | * Read about [JSX](https://reactjs.org/docs/introducing-jsx.html) 347 | * Read about [React components, elements and instances](https://reactjs.org/blog/2015/12/18/react-components-elements-and-instances.html) 348 | 349 | ## ES6 const and let 350 | 351 | Notice that we declared the variable `helloWorld` with a `var` statement. JavaScript ES6 comes with two more ways to declare variables: `const` and `let`. In JavaScript ES6, you will rarely find `var` anymore. A variable declared with `const` cannot be re-assigned or re-declared, and cannot be changed or modified. Once the variable is assigned, you cannot change it: 352 | 353 | {title="Code Playground",lang="javascript"} 354 | ~~~~~~~~ 355 | // not allowed 356 | const helloWorld = 'Welcome to the Road to learn React'; 357 | helloWorld = 'Bye Bye React'; 358 | ~~~~~~~~ 359 | 360 | Conversely, a variable declared with `let` can be modified: 361 | 362 | {title="Code Playground",lang="javascript"} 363 | ~~~~~~~~ 364 | // allowed 365 | let helloWorld = 'Welcome to the Road to learn React'; 366 | helloWorld = 'Bye Bye React'; 367 | ~~~~~~~~ 368 | 369 | **TIP:** Declare variables with `let` if you think you'll want to re-assign it later on. 370 | 371 | Note that a variable declared directly with `const` cannot be modified. However, when the variable is an array or object, the values it holds can get updated through indirect means: 372 | 373 | {title="Code Playground",lang="javascript"} 374 | ~~~~~~~~ 375 | // allowed 376 | const helloWorld = { 377 | text: 'Welcome to the Road to learn React' 378 | }; 379 | helloWorld.text = 'Bye Bye React'; 380 | ~~~~~~~~ 381 | 382 | There are varying opinions about when to use *const* and when to use *let*. I would recommend using `const` whenever possible to show the intent of keeping your data structures immutable, so you only have to worry about the values contained in objects and arrays. Immutability is embraced in the React ecosystem, so `const` should be your default choice when you define a variable, though it's not really about immutability, but about assigning variables only once. It shows the intent of not changing (re-assigning) the variable even though its content can be changed. 383 | 384 | {title="src/App.js",lang="javascript"} 385 | ~~~~~~~~ 386 | import React, { Component } from 'react'; 387 | import './App.css'; 388 | 389 | class App extends Component { 390 | render() { 391 | # leanpub-start-insert 392 | const helloWorld = 'Welcome to the Road to learn React'; 393 | # leanpub-end-insert 394 | return ( 395 |
396 |

{helloWorld}

397 |
398 | ); 399 | } 400 | } 401 | 402 | export default App; 403 | ~~~~~~~~ 404 | 405 | In your application, we will use `const` and `let` over `var` for the rest of the book. 406 | 407 | ### Exercises: 408 | 409 | * Confirm your [source code for the last section](http://bit.ly/2H9AqH2) 410 | * Confirm the [changes from the last section](http://bit.ly/2H61Vkw) 411 | * Read about [ES6 const](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const) 412 | * Read about [ES6 let](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let) 413 | * Gain an understanding of immutable data structures: 414 | * Why do they make sense in programming? 415 | * Why are they embraced in React and its ecosystem? 416 | 417 | ## ReactDOM 418 | 419 | The App component is located in your entry point to the React world: the *src/index.js* file. 420 | 421 | {title="src/index.js",lang="javascript"} 422 | ~~~~~~~~ 423 | import React from 'react'; 424 | import ReactDOM from 'react-dom'; 425 | import App from './App'; 426 | import './index.css'; 427 | 428 | ReactDOM.render( 429 | , 430 | document.getElementById('root') 431 | ); 432 | ~~~~~~~~ 433 | 434 | `ReactDOM.render()` uses a DOM node in your HTML to replace it with JSX. It's a way to integrate React in any foreign application easily, and you can use `ReactDOM.render()` multiple times across your application. You can use it to bootstrap simple JSX syntax, a React component, multiple React components, or an entire application. In a plain React application, you would only use it once to bootstrap the component tree. 435 | 436 | `ReactDOM.render()` expects two arguments. The first argument is for rendering the JSX. The second argument specifies the place where the React application hooks into your HTML. It expects an element with an `id='root'`, found in the *public/index.html* file. 437 | 438 | {title="Code Playground",lang="javascript"} 439 | ~~~~~~~~ 440 | ReactDOM.render( 441 |

Hello React World

, 442 | document.getElementById('root') 443 | ); 444 | ~~~~~~~~ 445 | 446 | During implementation, `ReactDOM.render()` takes your App component, though it can also pass simple JSX. It doesn't require a component instance. 447 | 448 | ### Exercises: 449 | 450 | * Open the *public/index.html* to see where the React application hooks into your HTML 451 | * Read about [rendering elements in React](https://reactjs.org/docs/rendering-elements.html) 452 | 453 | ## Hot Module Replacement 454 | 455 | Hot Module Replacement can be used in the *src/index.js* file to improve your experience as a developer. By default, *create-react-app* will cause the browser to refresh the page whenever its source code is modified. Try it by changing the `helloWorld` variable in your *src/App.js* file, which should cause the browser to refresh the page. There is a better way of handling source code changes during development, however. 456 | 457 | Hot Module Replacement (HMR) is a tool for reloading your application in the browser without the page refresh. You can activate it in *create-react-app* by adding the following configuration to your *src/index.js* file: 458 | 459 | {title="src/index.js",lang="javascript"} 460 | ~~~~~~~~ 461 | import React from 'react'; 462 | import ReactDOM from 'react-dom'; 463 | import './index.css'; 464 | import App from './App'; 465 | 466 | ReactDOM.render( 467 | , 468 | document.getElementById('root') 469 | ); 470 | 471 | # leanpub-start-insert 472 | if (module.hot) { 473 | module.hot.accept(); 474 | } 475 | # leanpub-end-insert 476 | ~~~~~~~~ 477 | 478 | Again, change the `helloWorld` variable in your *src/App.js* file. The browser shouldn't refresh, but the application will reload and show the correct output. HMR comes with multiple advantages: 479 | 480 | Imagine you are debugging your code with `console.log()` statements. These statements will stay in your developer console, even though you changed your code, because the browser doesn't refresh the page anymore. In a growing application, page refreshes delay productivity; HMR removes this obstacle by eliminating the incremental time loss it takes for a browser to reload. 481 | 482 | The most useful benefit of HMR is that you can keep the application state after the application reloads. For instance, assume you have a dialog or wizard in your application with multiple steps, and you are on step 3. Without HMR, you make changes to the source code and your browser refreshes the page. You would then have to open the dialog again and navigate from step 1 to step 3 each time. With HMR your dialog stays open at step 3, so you can debug from the exact point you're working on. With the time saved from page loads, this makes HMR an invaluable tool for React developers. 483 | 484 | ### Exercises: 485 | 486 | * Confirm your [source code for the last section](http://bit.ly/2H6pn1j) 487 | * Confirm the [changes from the last section](http://bit.ly/2H8C6jW) 488 | * Change your *src/App.js* source code a few times to see HMR in action 489 | * Watch the first 10 minutes of [Live React: Hot Reloading with Time Travel](https://www.youtube.com/watch?v=xsSnOQynTHs) by Dan Abramov 490 | 491 | ## Complex JavaScript in JSX 492 | 493 | So far, you have rendered a few primitive variables in your JSX. Now, we will render a list of items. The list will contain sample data in the beginning, but later we will learn how to fetch the data from an external API. 494 | 495 | First you have to define the list of items: 496 | 497 | {title="src/App.js",lang="javascript"} 498 | ~~~~~~~~ 499 | import React, { Component } from 'react'; 500 | import './App.css'; 501 | 502 | # leanpub-start-insert 503 | const list = [ 504 | { 505 | title: 'React', 506 | url: 'https://reactjs.org/', 507 | author: 'Jordan Walke', 508 | num_comments: 3, 509 | points: 4, 510 | objectID: 0, 511 | }, 512 | { 513 | title: 'Redux', 514 | url: 'https://redux.js.org/', 515 | author: 'Dan Abramov, Andrew Clark', 516 | num_comments: 2, 517 | points: 5, 518 | objectID: 1, 519 | }, 520 | ]; 521 | # leanpub-end-insert 522 | 523 | class App extends Component { 524 | ... 525 | } 526 | ~~~~~~~~ 527 | 528 | The sample data represents information we will fetch from an API later on. Items in this list each have a title, a url, and an author, as well an identifier, points (which indicate how popular an article is), and a count of comments. 529 | 530 | Now you can use the [built-in JavaScript map functionality](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) in JSX, which iterates over a list of items to display them according to specific attributes. Again, we use curly braces to encapsulate the JavaScript expression in JSX: 531 | 532 | {title="src/App.js",lang="javascript"} 533 | ~~~~~~~~ 534 | class App extends Component { 535 | render() { 536 | return ( 537 |
538 | # leanpub-start-insert 539 | {list.map(function(item) { 540 | return
{item.title}
; 541 | })} 542 | # leanpub-end-insert 543 |
544 | ); 545 | } 546 | } 547 | 548 | export default App; 549 | ~~~~~~~~ 550 | 551 | Using JavaScript alongside HTML in JSX is very powerful. For a different task you may have used `map` to convert one list of items to another. This time, we used `map` to convert a list of items to HTML elements. 552 | 553 | {title="Code Playground",lang="javascript"} 554 | ~~~~~~~~ 555 | const array = [1, 4, 9, 16]; 556 | 557 | // pass a function to map 558 | const newArray = array.map(function (x) { return x * 2; }); 559 | 560 | console.log(newArray); 561 | // expected output: Array [2, 8, 18, 32] 562 | ~~~~~~~~ 563 | 564 | So far, only the `title` is displayed for each item. Let's experiment with more of the item's properties: 565 | 566 | {title="src/App.js",lang="javascript"} 567 | ~~~~~~~~ 568 | class App extends Component { 569 | render() { 570 | return ( 571 |
572 | # leanpub-start-insert 573 | {list.map(function(item) { 574 | return ( 575 |
576 | 577 | {item.title} 578 | 579 | {item.author} 580 | {item.num_comments} 581 | {item.points} 582 |
583 | ); 584 | })} 585 | # leanpub-end-insert 586 |
587 | ); 588 | } 589 | } 590 | 591 | export default App; 592 | ~~~~~~~~ 593 | 594 | Note how the `map` function is inlined in your JSX. Each item property is displayed with a `` tag, and the url property of the item is in the `href` attribute of the anchor tag. 595 | 596 | React will display each item, but you can still do more to help React embrace its full potential. By assigning a key attribute to each list element, React can identify modified items when the list changes. These sample list items come with an identifier: 597 | 598 | {title="src/App.js",lang="javascript"} 599 | ~~~~~~~~ 600 | {list.map(function(item) { 601 | return ( 602 | # leanpub-start-insert 603 |
604 | # leanpub-end-insert 605 | 606 | {item.title} 607 | 608 | {item.author} 609 | {item.num_comments} 610 | {item.points} 611 |
612 | ); 613 | })} 614 | ~~~~~~~~ 615 | 616 | Make sure that the key attribute is a stable identifier. Avoid using the index of the item in the array, because the array index is not stable. If the list changes its order, for example, React will not be able to identify the items properly. 617 | 618 | {title="src/App.js",lang="javascript"} 619 | ~~~~~~~~ 620 | // don't do this 621 | {list.map(function(item, key) { 622 | return ( 623 |
624 | ... 625 |
626 | ); 627 | })} 628 | ~~~~~~~~ 629 | 630 | Start your app in a browser, and you should see both items of the list displayed. 631 | 632 | ### Exercises: 633 | 634 | * Confirm your [source code for the last section](http://bit.ly/2H7jMHT) 635 | * Confirm the [changes from the last section](http://bit.ly/2H6LKnb) 636 | * Read about [React lists and keys](https://reactjs.org/docs/lists-and-keys.html) 637 | * Recap the [standard built-in array functionalities in JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/) and [discover a couple of use cases](https://www.robinwieruch.de/javascript-map-array/) 638 | * Use more JavaScript expressions on your own in JSX 639 | 640 | ## ES6 Arrow Functions 641 | 642 | JavaScript ES6 introduced arrow functions expressions, which are shorter than a function expressions. 643 | 644 | {title="Code Playground",lang="javascript"} 645 | ~~~~~~~~ 646 | // function declaration 647 | function () { ... } 648 | 649 | // arrow function declaration 650 | () => { ... } 651 | ~~~~~~~~ 652 | 653 | You can remove the parentheses in an arrow function expression if it only has one argument, but you have to keep the parentheses if it gets multiple arguments: 654 | 655 | {title="Code Playground",lang="javascript"} 656 | ~~~~~~~~ 657 | // allowed 658 | item => { ... } 659 | 660 | // allowed 661 | (item) => { ... } 662 | 663 | // not allowed 664 | item, key => { ... } 665 | 666 | // allowed 667 | (item, key) => { ... } 668 | ~~~~~~~~ 669 | 670 | You can also write `map` functions more concisely with an ES6 arrow function: 671 | 672 | {title="src/App.js",lang="javascript"} 673 | ~~~~~~~~ 674 | # leanpub-start-insert 675 | {list.map(item => { 676 | # leanpub-end-insert 677 | return ( 678 |
679 | 680 | {item.title} 681 | 682 | {item.author} 683 | {item.num_comments} 684 | {item.points} 685 |
686 | ); 687 | })} 688 | ~~~~~~~~ 689 | 690 | You can remove the *block body*, the curly braces, with the ES6 arrow function. In a *concise body*, an implicit return is attached; thus, you can remove the `return` statement. This will happen often in this book, so be sure to understand the difference between a block body and a concise body when using arrow functions. 691 | 692 | {title="src/App.js",lang="javascript"} 693 | ~~~~~~~~ 694 | # leanpub-start-insert 695 | {list.map(item => 696 | # leanpub-end-insert 697 |
698 | 699 | {item.title} 700 | 701 | {item.author} 702 | {item.num_comments} 703 | {item.points} 704 |
705 | # leanpub-start-insert 706 | )} 707 | # leanpub-end-insert 708 | ~~~~~~~~ 709 | 710 | Your JSX should look more concise and readable now, as it omits the `function` statement, the curly braces, and the return statement. 711 | 712 | ### Exercises: 713 | 714 | * Confirm your [source code for the last section](http://bit.ly/2H6Mfh3) 715 | * Confirm the [changes from the last section](http://bit.ly/2H92076) 716 | * Read about [ES6 arrow functions](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Functions/Arrow_functions) 717 | 718 | ## ES6 Classes 719 | 720 | JavaScript ES6 introduced classes, which are commonly used in object-oriented programming languages. JavaScript, always flexible in its programming paradigms, allows functional programming and object-oriented programming to work side-by-side. 721 | 722 | While React embraces functional programming, e.g. immutable data structures and function compositions, classes are used to declare ES6 class components. React mixes the good parts of both programming paradigms. 723 | 724 | Consider the following Developer class to examine a JavaScript ES6 class without a component. 725 | 726 | {title="Code Playground",lang="javascript"} 727 | ~~~~~~~~ 728 | class Developer { 729 | constructor(firstname, lastname) { 730 | this.firstname = firstname; 731 | this.lastname = lastname; 732 | } 733 | 734 | getName() { 735 | return this.firstname + ' ' + this.lastname; 736 | } 737 | } 738 | ~~~~~~~~ 739 | 740 | A class has a constructor to make it instantiable. The constructor takes arguments and assigns them to the class instance. A class can also define functions. Because the function is associated with a class, it is called a method, or a class method. 741 | 742 | The Developer class is the only class declaration we use here, as you can create multiple instances of a class by invoking it. It is similar to the ES6 class component, which has a declaration, but you have to use it somewhere else to instantiate it: 743 | 744 | {title="Code Playground",lang="javascript"} 745 | ~~~~~~~~ 746 | const robin = new Developer('Robin', 'Wieruch'); 747 | console.log(robin.getName()); 748 | // output: Robin Wieruch 749 | ~~~~~~~~ 750 | 751 | React uses JavaScript ES6 classes for ES6 class components, which you have already used at least once so far: 752 | 753 | {title="src/App.js",lang="javascript"} 754 | ~~~~~~~~ 755 | import React, { Component } from 'react'; 756 | 757 | ... 758 | 759 | class App extends Component { 760 | render() { 761 | ... 762 | } 763 | } 764 | ~~~~~~~~ 765 | 766 | When you declare the App component it extends from another component. In object-oriented programming, the term "extends" refers to the principle of inheritance, which means that functionality can be passed from one class to another. The App class extends from the Component class, meaning it inherits functionality from the Component class. The Component class is used to extend a basic ES6 class to a ES6 component class. It has all the functionalities that a component in React needs. The render method is one function you have already used. You will learn about other component class methods as we move along. 767 | 768 | The `Component` class encapsulates all the implementation details of a React component, which allows developers to use classes as components in React. 769 | 770 | Methods exposed by a React `Component` are its public interface. One of these methods must be overridden, while the others don't need to be overridden. You will learn about these when we discuss lifecycle methods later. The `render()` method has to be overridden, because it defines the output of a React `Component`, so it must be defined. These are the basics of JavaScript ES6 classes, and how they are used in React to extend them to components. 771 | 772 | ### Exercises: 773 | 774 | * Read about [JavaScript fundamentals before learning React](https://www.robinwieruch.de/javascript-fundamentals-react-requirements/) 775 | * Read about [ES6 classes](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Classes) 776 | 777 | {pagebreak} 778 | 779 | Congratulations, you have learned to bootstrap your first React application! Let's recap: 780 | 781 | * **React** 782 | * Create-react-app bootstraps a React application 783 | * JSX mixes up HTML and JavaScript to define the output of React components in their render methods 784 | * Components, instances, and elements are different items in React 785 | * `ReactDOM.render()` is an entry point for a React application to hook React into the DOM 786 | * Built-in JavaScript functionalities can be used in JSX 787 | * Map can be used to render a list of items as HTML elements 788 | * **ES6** 789 | * Variable declarations with `const` and `let` can be used for specific use cases 790 | * Use const over let in React applications 791 | * Arrow functions can be used to keep your functions concise 792 | * Classes are used to define components in React by extending them 793 | 794 | Now that you've completed the first chapter, it's advisable to experiment with the source code you have written so far and see what changes you can make on your own. You can find the source code in the [official repository](http://bit.ly/2H6Mfh3). 795 | -------------------------------------------------------------------------------- /manuscript/chapter3.md: -------------------------------------------------------------------------------- 1 | # Getting Real with APIs 2 | 3 | Now it's time to get real with APIs and move past sample data. If you are not familiar, I encourage you [to read my article on how I got to know APIs](https://www.robinwieruch.de/what-is-an-api-javascript/). 4 | 5 | For our first foray into the concept, we will be using [Hacker News](https://news.ycombinator.com/), a solid news aggregator about tech topics. In this exercise, we will use the Hacker News API to fetch trending stories. There are [basic](https://github.com/HackerNews/API) and [search](https://hn.algolia.com/api) APIs to get data from the platform. Search makes sense in this application, because we want to be able to search Hacker News stories. Visit the API specification to get an understanding of the data structure. 6 | 7 | ## Lifecycle Methods 8 | 9 | You may remember lifecycle methods were mentioned briefly in the last chapter, as a hook into the lifecycle of a React component. They can be used in ES6 class components, but not in functional stateless components. Besides the `render()` method, there are several methods that can be overridden in a React ES6 class component. All of these are the lifecycle methods. 10 | 11 | We have already covered two lifecycle methods that can be used in an ES6 class component: 12 | 13 | * The constructor is only called when an instance of the component is created and inserted in the DOM. The component gets instantiated in a process called mounting. 14 | * The `render()` method is called during the mount process too, but also when the component updates. Each time the state or the props of a component changes, the `render()` method is called. 15 | 16 | There are two more lifecycle methods when mounting a component: `getDerivedStateFromProps()` and `componentDidMount()`. The constructor is called first, `getDerivedStateFromProps()` is called before the `render()` method, and `componentDidMount()` is called after the `render()` method. 17 | 18 | Overall, the mounting process has 4 lifecycle methods, invoked in the following order: 19 | 20 | * constructor() 21 | * getDerivedStateFromProps() 22 | * render() 23 | * componentDidMount() 24 | 25 | For the update lifecycle of a component when the state or the props change, there are 5 lifecycle methods, in the following order: 26 | 27 | * getDerivedStateFromProps() 28 | * shouldComponentUpdate() 29 | * render() 30 | * getSnapshotBeforeUpdate() 31 | * componentDidUpdate() 32 | 33 | Lastly, there is the unmounting lifecycle. It has only one lifecycle method: `componentWillUnmount()`. 34 | 35 | You don't need to know all the lifecycle methods from the beginning, and even in a large React application you'll only use a few of them besides the `constructor()` and the `render()` methods. Still, it is good to know each lifecycle method can be used for specific purposes: 36 | 37 | * **constructor(props)** is called when the component gets initialized. You can set an initial component state and bind class methods during that lifecycle method. 38 | 39 | * **static getDerivedStateFromProps(props, state)** is called before the `render()` lifecycle method, both on the initial mount and on the subsequent updates. It should return an object to update the state, or null to update nothing. It exists for **rare** use cases where the state depends on changes in props over time. It is important to know that this is a static method and it doesn't have access to the component instance. 40 | 41 | * **render()** is a mandatory lifecycle method that returns elements as an output of the component. The method should be pure, so it shouldn't modify the component state. It gets an input as props and state, and returns an element. 42 | 43 | * **componentDidMount()** is called once, when the component mounted. That's the perfect time to do an asynchronous request to fetch data from an API. The fetched data is stored in the local component state to display it in the `render()` lifecycle method. 44 | 45 | * **shouldComponentUpdate(nextProps, nextState)** is always called when the component updates due to state or props changes. You will use it in mature React applications for performance optimization. Depending on a boolean that you return from this lifecycle method, the component and all its children will render or will not render on an update lifecycle. You can prevent the render lifecycle method of a component. 46 | 47 | * **getSnapshotBeforeUpdate(prevProps, prevState)** is a lifecycle method, invoked before the most recently rendered output is committed to the DOM. In rare cases, the component needs to capture information from the DOM before it is potentially changed. This lifecycle method enables the component to do it. Another method (`componentDidUpdate()`) will receive any value returned by `getSnapshotBeforeUpdate()` as a parameter. 48 | 49 | * **componentDidUpdate(prevProps, prevState, snapshot)** is a lifecycle method that is invoked immediately after updating, but not for the initial render. You can use it to perform DOM operations or to perform more asynchronous requests. If your component implements the `getSnapshotBeforeUpdate()` method, the value it returns will be received as the `snapshot` parameter. 50 | 51 | * **componentWillUnmount()** is called before you destroy your component. You can use this lifecycle method to perform any clean up tasks. 52 | 53 | As you may have gathered, the `constructor()` and `render()` lifecycle methods are the most commonly used lifecycle methods for ES6 class components. The `render()` method is always required to return a component instance. 54 | 55 | Lastly, `componentDidCatch(error, info)` was introduced in [React 16](https://www.robinwieruch.de/what-is-new-in-react-16/) as a way to catch errors in components. For instance, displaying the sample list in your application works fine, but there could be a time when a list in the local state is set to `null` by accident (e.g. when fetching the list from an external API, but the request failed and you set the local state of the list to null). It becomes impossible to filter and map the list, because it is `null` and not an empty list. The component would be broken, and the whole application would fail. Using `componentDidCatch()`, you can catch the error, store it in your local state, and show a message to the user. 56 | 57 | ### Exercises: 58 | 59 | * Read about [lifecycle methods in React](https://reactjs.org/docs/react-component.html) 60 | * Read about [the state related to lifecycle methods in React](https://reactjs.org/docs/state-and-lifecycle.html) 61 | * Read about [error handling in components](https://reactjs.org/blog/2017/07/26/error-handling-in-react-16.html) 62 | 63 | ## Fetching Data 64 | 65 | Now we're prepared to fetch data from the Hacker News API. There was one lifecycle method mentioned that can be used to fetch data: `componentDidMount()`. Before we use it, let's set up the URL constants and default parameters to break the URL endpoint for the API request into smaller pieces. 66 | 67 | {title="src/App.js",lang="javascript"} 68 | ~~~~~~~~ 69 | import React, { Component } from 'react'; 70 | import './App.css'; 71 | 72 | # leanpub-start-insert 73 | const DEFAULT_QUERY = 'redux'; 74 | 75 | const PATH_BASE = 'https://hn.algolia.com/api/v1'; 76 | const PATH_SEARCH = '/search'; 77 | const PARAM_SEARCH = 'query='; 78 | # leanpub-end-insert 79 | 80 | ... 81 | ~~~~~~~~ 82 | 83 | In JavaScript ES6, you can use [template literals](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Template_literals) for string concatenation or interpolation. You will use it to concatenate your URL for the API endpoint. 84 | 85 | {title="Code Playground",lang="javascript"} 86 | ~~~~~~~~ 87 | // ES6 88 | const url = `${PATH_BASE}${PATH_SEARCH}?${PARAM_SEARCH}${DEFAULT_QUERY}`; 89 | 90 | // ES5 91 | var url = PATH_BASE + PATH_SEARCH + '?' + PARAM_SEARCH + DEFAULT_QUERY; 92 | 93 | console.log(url); 94 | // output: https://hn.algolia.com/api/v1/search?query=redux 95 | ~~~~~~~~ 96 | 97 | This will keep your URL composition flexible in the future. Below, the entire data fetch process will be presented, and each step will be explained afterward. 98 | 99 | {title="src/App.js",lang="javascript"} 100 | ~~~~~~~~ 101 | ... 102 | 103 | class App extends Component { 104 | 105 | constructor(props) { 106 | super(props); 107 | 108 | this.state = { 109 | # leanpub-start-insert 110 | result: null, 111 | searchTerm: DEFAULT_QUERY, 112 | # leanpub-end-insert 113 | }; 114 | 115 | # leanpub-start-insert 116 | this.setSearchTopStories = this.setSearchTopStories.bind(this); 117 | # leanpub-end-insert 118 | this.onSearchChange = this.onSearchChange.bind(this); 119 | this.onDismiss = this.onDismiss.bind(this); 120 | } 121 | 122 | # leanpub-start-insert 123 | setSearchTopStories(result) { 124 | this.setState({ result }); 125 | } 126 | 127 | componentDidMount() { 128 | const { searchTerm } = this.state; 129 | 130 | fetch(`${PATH_BASE}${PATH_SEARCH}?${PARAM_SEARCH}${searchTerm}`) 131 | .then(response => response.json()) 132 | .then(result => this.setSearchTopStories(result)) 133 | .catch(error => error); 134 | } 135 | # leanpub-end-insert 136 | 137 | ... 138 | } 139 | ~~~~~~~~ 140 | 141 | First, we remove the sample list of items, because we will return a real list from the Hacker News API, so the sample data is no longer used. The initial state of your component has an empty result and default search term now. The same default search term is used in the input field of the Search component, and in your first request. 142 | 143 | Second, you use the `componentDidMount()` lifecycle method to fetch the data after the component mounted. The first fetch uses default search term from the local state. It will fetch "redux" related stories, because that is the default parameter. 144 | 145 | Third, the native fetch API is used. The JavaScript ES6 template strings allow it to compose the URL with the `searchTerm`. The URL is the argument for the native fetch API function. The response is transformed to a JSON data structure, a mandatory step in a native fetch with JSON data structures, after which it can be set as result in the local component state. If an error occurs during the request, the function will run into the catch block instead of the then block. 146 | 147 | Last, remember to bind your new component method in the constructor. 148 | 149 | Now you can use the fetched data instead of the sample list. Note that the result is not only a list of data, [but a complex object with meta information and a list of hits that are news stories](https://hn.algolia.com/api). You can output the local state with `console.log(this.state);` in your `render()` method to visualize it. 150 | 151 | In the next step, we use the result to render it. But we will prevent it from rendering anything, so we will return null, when there is no result in the first place. Once the request to the API has succeeded, the result is saved to the state and the App component will re-render with the updated state. 152 | 153 | {title="src/App.js",lang="javascript"} 154 | ~~~~~~~~ 155 | class App extends Component { 156 | 157 | ... 158 | 159 | render() { 160 | # leanpub-start-insert 161 | const { searchTerm, result } = this.state; 162 | 163 | if (!result) { return null; } 164 | 165 | # leanpub-end-insert 166 | return ( 167 |
168 | ... 169 | 176 | 177 | ); 178 | } 179 | } 180 | ~~~~~~~~ 181 | 182 | Now, let's recap what happens during the component lifecycle. Your component is initialized by the constructor, after which it renders for the first time. We prevented it from displaying anything, because the result in the local state is null. It is allowed to return null for a component to display nothing. Then the `componentDidMount()` lifecycle method fetches the data from the Hacker News API asynchronously. Once the data arrives, it changes your local component state in `setSearchTopStories()`. The update lifecycle activates because the local state was updated. The component runs the `render()` method again, but this time with populated result in your local component state. The component and the Table component will be rendered with its content. 183 | 184 | We used the native fetch API supported by most browsers to perform an asynchronous request to an API. The *create-react-app* configuration makes sure it is supported by all browsers. There are also third-party node packages that you can use to substitute the native fetch API: [axios](https://github.com/mzabriskie/axios). You will use axios later in this book. 185 | 186 | In this book, we build on JavaScript's shorthand notation for truthfulness checks. In the previous example, `if (!result)` was used in favor of `if (result === null)`. The same applies for other cases as well. For instance, `if (!list.length)` is used in favor of `if (list.length === 0)` or `if (someString)` is used in favor of `if (someString !== '')`. 187 | 188 | The list of hits should now be visible in our application; however, two regression bugs have appeared. First, the "Dismiss" button is broken, because it doesn't know about the complex result object, but it still operates on the plain list from the sample data when dismissing an item. Second, when the list is displayed and you try to search for something else, it gets filtered on the client-side, though the initial search was made by searching for stories on the server-side. The perfect behavior would be to fetch another result object from the API when using the Search component. Both regression bugs will be fixed in the following chapters. 189 | 190 | ### Exercises: 191 | 192 | * Confirm your [source code for the last section](http://bit.ly/2HkOIDO) 193 | * Confirm the [changes from the last section](http://bit.ly/2HkOHzK) 194 | * Read about [ES6 template literals](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Template_literals) 195 | * Read about [the native fetch API](https://developer.mozilla.org/en/docs/Web/API/Fetch_API) 196 | * Read about [data fetching in React](https://www.robinwieruch.de/react-fetching-data/) 197 | 198 | ## ES6 Spread Operators 199 | 200 | The "Dismiss" button doesn't work because the `onDismiss()` method is not aware of the complex result object. It only knows about a plain list in the local state. But it isn't a plain list anymore. Let's change it to operate on the result object instead of the list itself. 201 | 202 | {title="src/App.js",lang="javascript"} 203 | ~~~~~~~~ 204 | onDismiss(id) { 205 | const isNotId = item => item.objectID !== id; 206 | # leanpub-start-insert 207 | const updatedHits = this.state.result.hits.filter(isNotId); 208 | this.setState({ 209 | ... 210 | }); 211 | # leanpub-end-insert 212 | } 213 | ~~~~~~~~ 214 | 215 | In `setState()`, the result is now a complex object, and the list of hits is only one of multiple properties in the object. Only the list gets updated when an item gets removed in the result object, though, while the other properties stay the same. 216 | 217 | We could alleviate this challenge by mutating the hits in the result object. It is demonstrated below for the sake of understanding, but we'll end up using another way. 218 | 219 | {title="Code Playground",lang="javascript"} 220 | ~~~~~~~~ 221 | // don't do this 222 | this.state.result.hits = updatedHits; 223 | ~~~~~~~~ 224 | 225 | As we know, React embraces immutable data structures, so we don't want to mutate an object (or mutate the state directly). We want to generate a new object based on the information given, so none of the objects get altered and we keep the immutable data structures. You will always return a new object, but never alter the original object. 226 | 227 | For this, we use JavaScript ES6's `Object.assign()`. It takes a target object as first argument. All following arguments are source objects, which are merged into the target object. The target object can be an empty object. It embraces immutability, because no source object gets mutated. 228 | 229 | {title="Code Playground",lang="javascript"} 230 | ~~~~~~~~ 231 | const updatedHits = { hits: updatedHits }; 232 | const updatedResult = Object.assign({}, this.state.result, updatedHits); 233 | ~~~~~~~~ 234 | 235 | Source objects will override previously merged objects with the same property names. Let's do this on the `onDismiss()` method: 236 | 237 | {title="src/App.js",lang="javascript"} 238 | ~~~~~~~~ 239 | onDismiss(id) { 240 | const isNotId = item => item.objectID !== id; 241 | const updatedHits = this.state.result.hits.filter(isNotId); 242 | this.setState({ 243 | # leanpub-start-insert 244 | result: Object.assign({}, this.state.result, { hits: updatedHits }) 245 | # leanpub-end-insert 246 | }); 247 | } 248 | ~~~~~~~~ 249 | 250 | That's one solution, but there is a simpler way in JavaScript ES6, called the *spread operator*. The spread operator is shown with three dots: `...` When it is used, every value from an array or object gets copied to another array or object. 251 | 252 | We'll examine the ES6 **array** spread operator even though you don't need it yet: 253 | 254 | {title="Code Playground",lang="javascript"} 255 | ~~~~~~~~ 256 | const userList = ['Robin', 'Andrew', 'Dan']; 257 | const additionalUser = 'Jordan'; 258 | const allUsers = [ ...userList, additionalUser ]; 259 | 260 | console.log(allUsers); 261 | // output: ['Robin', 'Andrew', 'Dan', 'Jordan'] 262 | ~~~~~~~~ 263 | 264 | The `allUsers` variable is a completely new array. The other variables `userList` and `additionalUser` stay the same. You can even merge two arrays into a new array. 265 | 266 | {title="Code Playground",lang="javascript"} 267 | ~~~~~~~~ 268 | const oldUsers = ['Robin', 'Andrew']; 269 | const newUsers = ['Dan', 'Jordan']; 270 | const allUsers = [ ...oldUsers, ...newUsers ]; 271 | 272 | console.log(allUsers); 273 | // output: ['Robin', 'Andrew', 'Dan', 'Jordan'] 274 | ~~~~~~~~ 275 | 276 | Now let's have a look at the object spread operator, which is not JavaScript ES6. It is part of a [proposal for a next JavaScript version](https://github.com/sebmarkbage/ecmascript-rest-spread) that is already being used by the React community, which is why *create-react-app* incorporated the feature in the configuration. The object spread operator works just like the JavaScript ES6 array spread operator, except with objects. Each key value pair is copied into a new object: 277 | 278 | {title="Code Playground",lang="javascript"} 279 | ~~~~~~~~ 280 | const userNames = { firstname: 'Robin', lastname: 'Wieruch' }; 281 | const age = 28; 282 | const user = { ...userNames, age }; 283 | 284 | console.log(user); 285 | // output: { firstname: 'Robin', lastname: 'Wieruch', age: 28 } 286 | ~~~~~~~~ 287 | 288 | Multiple objects can be spread, as with the array spread example. 289 | 290 | {title="Code Playground",lang="javascript"} 291 | ~~~~~~~~ 292 | const userNames = { firstname: 'Robin', lastname: 'Wieruch' }; 293 | const userAge = { age: 28 }; 294 | const user = { ...userNames, ...userAge }; 295 | 296 | console.log(user); 297 | // output: { firstname: 'Robin', lastname: 'Wieruch', age: 28 } 298 | ~~~~~~~~ 299 | 300 | It can be used to replace `Object.assign()`: 301 | 302 | {title="src/App.js",lang="javascript"} 303 | ~~~~~~~~ 304 | onDismiss(id) { 305 | const isNotId = item => item.objectID !== id; 306 | const updatedHits = this.state.result.hits.filter(isNotId); 307 | this.setState({ 308 | # leanpub-start-insert 309 | result: { ...this.state.result, hits: updatedHits } 310 | # leanpub-end-insert 311 | }); 312 | } 313 | ~~~~~~~~ 314 | 315 | The "Dismiss" button should work now, because the `onDismiss()` method is aware of the complex result object, and how to update it after dismissing an item from the list. 316 | 317 | ### Exercises: 318 | 319 | * Confirm your [source code for the last section](http://bit.ly/2HmzEpb) 320 | * Confirm the [changes from the last section](http://bit.ly/2HpC32j) 321 | * Read about the [ES6 Object.assign()](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) 322 | * Read about the [ES6 array (and object) spread operator](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Spread_operator) 323 | 324 | ## Conditional Rendering 325 | 326 | Conditional rendering is usually introduced early in React applications, though not in our working example. It happens when you decide to render either of two elements, which sometimes is a choice between rendering an element or nothing. The simplest usage of a conditional rendering can be expressed by an if-else statement in JSX. 327 | 328 | The `result` object in the local component state is `null` in the beginning. So far, the App component returned no elements when the `result` hasn't arrived from the API. That's already a conditional rendering, because you return earlier from the `render()` lifecycle method for a certain condition. The App component either renders nothing or its elements. 329 | 330 | But let's go one step further. It makes more sense to wrap the Table component, which is the only component that depends on `result` in an independent conditional rendering. Everything else should be displayed, even though there is no `result` yet. You can simply use a [ternary operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_Operator) in the JSX: 331 | 332 | {title="src/App.js",lang="javascript"} 333 | ~~~~~~~~ 334 | class App extends Component { 335 | 336 | ... 337 | 338 | render() { 339 | # leanpub-start-insert 340 | const { searchTerm, result } = this.state; 341 | # leanpub-end-insert 342 | return ( 343 |
344 |
345 | 349 | Search 350 | 351 |
352 | # leanpub-start-insert 353 | { result 354 | ?
359 | : null 360 | } 361 | # leanpub-end-insert 362 | 363 | ); 364 | } 365 | } 366 | ~~~~~~~~ 367 | 368 | That's your second option to express a conditional rendering. A third option is the logical `&&` operator. In JavaScript a `true && 'Hello World'` always evaluates to 'Hello World'. A `false && 'Hello World'` always evaluates to false. 369 | 370 | {title="Code Playground",lang="javascript"} 371 | ~~~~~~~~ 372 | const result = true && 'Hello World'; 373 | console.log(result); 374 | // output: Hello World 375 | 376 | const result = false && 'Hello World'; 377 | console.log(result); 378 | // output: false 379 | ~~~~~~~~ 380 | 381 | In React, you can make use of that behavior. If the condition is true, the expression after the logical `&&` operator will be the output. If the condition is false, React ignores the expression. It is applicable in the Table component's conditional rendering case, because it should either return a Table or nothing. 382 | 383 | {title="src/App.js",lang="javascript"} 384 | ~~~~~~~~ 385 | { result && 386 |
391 | } 392 | ~~~~~~~~ 393 | 394 | These were a few approaches to use conditional rendering in React. You can read about [more alternatives in an exhaustive list of examples](https://www.robinwieruch.de/conditional-rendering-react/). Moreover, you will get to know their different uses and when to apply them. 395 | 396 | You should be able to see the fetched data in your application by now. Everything except the Table is displayed when data fetching is pending. Once the request resolves the result and stores it into the local state, the Table is displayed because the `render()` method runs again, and the condition in the conditional rendering resolves in favor of displaying the Table component. 397 | 398 | ### Exercises: 399 | 400 | * Confirm your [source code for the last section](http://bit.ly/2HnZObc) 401 | * Confirm the [changes from the last section](http://bit.ly/2Ho99jl) 402 | * Read about [different ways for conditional renderings](https://www.robinwieruch.de/conditional-rendering-react/) 403 | * Read about [React conditional rendering](https://reactjs.org/docs/conditional-rendering.html) 404 | 405 | ## Client- or Server-side Search 406 | 407 | Now, when you use the Search component with its input field, you will filter the list. That's happening on the client-side, though. We want to use the Hacker News API to search on the server-side. Otherwise you would only deal with the first API response you got on `componentDidMount()`, the default search term parameter. 408 | 409 | You can define an `onSearchSubmit()` method in your App component, which fetches results from the Hacker News API when executing a search in the Search component. 410 | 411 | {title="src/App.js",lang="javascript"} 412 | ~~~~~~~~ 413 | class App extends Component { 414 | 415 | constructor(props) { 416 | super(props); 417 | 418 | this.state = { 419 | result: null, 420 | searchTerm: DEFAULT_QUERY, 421 | }; 422 | 423 | this.setSearchTopStories = this.setSearchTopStories.bind(this); 424 | this.onSearchChange = this.onSearchChange.bind(this); 425 | # leanpub-start-insert 426 | this.onSearchSubmit = this.onSearchSubmit.bind(this); 427 | # leanpub-end-insert 428 | this.onDismiss = this.onDismiss.bind(this); 429 | } 430 | 431 | ... 432 | 433 | # leanpub-start-insert 434 | onSearchSubmit() { 435 | const { searchTerm } = this.state; 436 | } 437 | # leanpub-end-insert 438 | 439 | ... 440 | } 441 | ~~~~~~~~ 442 | 443 | The `onSearchSubmit()` method should use the same functionality as the `componentDidMount()` lifecycle method, but this time with a modified search term from the local state and not with the initial default search term. Thus you can extract the functionality as a reusable class method. 444 | 445 | {title="src/App.js",lang="javascript"} 446 | ~~~~~~~~ 447 | class App extends Component { 448 | 449 | constructor(props) { 450 | super(props); 451 | 452 | this.state = { 453 | result: null, 454 | searchTerm: DEFAULT_QUERY, 455 | }; 456 | 457 | this.setSearchTopStories = this.setSearchTopStories.bind(this); 458 | # leanpub-start-insert 459 | this.fetchSearchTopStories = this.fetchSearchTopStories.bind(this); 460 | # leanpub-end-insert 461 | this.onSearchChange = this.onSearchChange.bind(this); 462 | this.onSearchSubmit = this.onSearchSubmit.bind(this); 463 | this.onDismiss = this.onDismiss.bind(this); 464 | } 465 | 466 | ... 467 | 468 | # leanpub-start-insert 469 | fetchSearchTopStories(searchTerm) { 470 | fetch(`${PATH_BASE}${PATH_SEARCH}?${PARAM_SEARCH}${searchTerm}`) 471 | .then(response => response.json()) 472 | .then(result => this.setSearchTopStories(result)) 473 | .catch(error => error); 474 | } 475 | # leanpub-end-insert 476 | 477 | componentDidMount() { 478 | const { searchTerm } = this.state; 479 | # leanpub-start-insert 480 | this.fetchSearchTopStories(searchTerm); 481 | # leanpub-end-insert 482 | } 483 | 484 | ... 485 | 486 | onSearchSubmit() { 487 | const { searchTerm } = this.state; 488 | # leanpub-start-insert 489 | this.fetchSearchTopStories(searchTerm); 490 | # leanpub-end-insert 491 | } 492 | 493 | ... 494 | } 495 | ~~~~~~~~ 496 | 497 | Now the Search component has to add an additional button to trigger the search request explicitly. Otherwise, we'd fetch data from the Hacker News API every time the input field changes. We want to do it explicitly in a `onClick()` handler. 498 | 499 | As an alternative, you could debounce (delay) the `onChange()` function and spare the button, but it would add more complexity, and we want to keep things simple for now. 500 | 501 | First, pass the `onSearchSubmit()` method to your Search component. 502 | 503 | {title="src/App.js",lang="javascript"} 504 | ~~~~~~~~ 505 | class App extends Component { 506 | 507 | ... 508 | 509 | render() { 510 | const { searchTerm, result } = this.state; 511 | return ( 512 |
513 |
514 | 521 | Search 522 | 523 |
524 | { result && 525 |
530 | } 531 | 532 | ); 533 | } 534 | } 535 | ~~~~~~~~ 536 | 537 | Second, introduce a button in your Search component. The button has the `type="submit"` and the form uses its `onSubmit` attribute to pass the `onSubmit()` method. You can reuse the `children` property, but this time it will be used as the content of the button. 538 | 539 | {title="src/App.js",lang="javascript"} 540 | ~~~~~~~~ 541 | # leanpub-start-insert 542 | const Search = ({ 543 | value, 544 | onChange, 545 | onSubmit, 546 | children 547 | }) => 548 | 549 | 554 | 557 | 558 | # leanpub-end-insert 559 | ~~~~~~~~ 560 | 561 | In the Table, you can remove the filter functionality, because there will be no client-side filter (search) anymore. Don't forget to remove the `isSearched()` function as well, as it won't be used anymore. Now, the result comes directly from the Hacker News API when the user clicks the "Search" button. 562 | 563 | {title="src/App.js",lang="javascript"} 564 | ~~~~~~~~ 565 | class App extends Component { 566 | 567 | ... 568 | 569 | render() { 570 | const { searchTerm, result } = this.state; 571 | return ( 572 |
573 | ... 574 | { result && 575 |
581 | } 582 | 583 | ); 584 | } 585 | } 586 | 587 | ... 588 | 589 | # leanpub-start-insert 590 | const Table = ({ list, onDismiss }) => 591 | # leanpub-end-insert 592 |
593 | # leanpub-start-insert 594 | {list.map(item => 595 | # leanpub-end-insert 596 | ... 597 | )} 598 |
599 | ~~~~~~~~ 600 | 601 | Now when you try to search, you will notice the browser reloads. That's a native browser behavior for a submit callback in an HTML form. In React, you will often come across the `preventDefault()` event method to suppress the native browser behavior. 602 | 603 | {title="src/App.js",lang="javascript"} 604 | ~~~~~~~~ 605 | # leanpub-start-insert 606 | onSearchSubmit(event) { 607 | # leanpub-end-insert 608 | const { searchTerm } = this.state; 609 | this.fetchSearchTopStories(searchTerm); 610 | # leanpub-start-insert 611 | event.preventDefault(); 612 | # leanpub-end-insert 613 | } 614 | ~~~~~~~~ 615 | 616 | You should be able to search different Hacker News stories now. We have interacted with a real API, and there should be no more client-side searches. 617 | 618 | ### Exercises: 619 | 620 | * Confirm your [source code for the last section](http://bit.ly/2HtekOK) 621 | * Confirm the [changes from the last section](http://bit.ly/2HnYg0J) 622 | * Read about [synthetic events in React](https://reactjs.org/docs/events.html) 623 | * Experiment with the [Hacker News API](https://hn.algolia.com/api) 624 | 625 | ## Paginated Fetch 626 | 627 | Take a closer look at the data structure and observe how the [Hacker News API](https://hn.algolia.com/api) returns more than a list of hits. Precisely it returns a paginated list. The page property, which is `0` in the first response, can be used to fetch more paginated sublists as results. You only need to pass the next page with the same search term to the API. 628 | 629 | Let's extend the composable API constants so it can deal with paginated data: 630 | 631 | {title="src/App.js",lang="javascript"} 632 | ~~~~~~~~ 633 | const DEFAULT_QUERY = 'redux'; 634 | 635 | const PATH_BASE = 'https://hn.algolia.com/api/v1'; 636 | const PATH_SEARCH = '/search'; 637 | const PARAM_SEARCH = 'query='; 638 | # leanpub-start-insert 639 | const PARAM_PAGE = 'page='; 640 | # leanpub-end-insert 641 | ~~~~~~~~ 642 | 643 | Now you can use the new constant to add the page parameter to your API request: 644 | 645 | {title="Code Playground",lang="javascript"} 646 | ~~~~~~~~ 647 | const url = `${PATH_BASE}${PATH_SEARCH}?${PARAM_SEARCH}${DEFAULT_QUERY}&${PARAM_PAGE}`; 648 | 649 | console.log(url); 650 | // output: https://hn.algolia.com/api/v1/search?query=redux&page= 651 | ~~~~~~~~ 652 | 653 | The `fetchSearchTopStories()` method will take the page as second argument. If you don't provide the second argument, it will fallback to the `0` page for the initial request. Thus the `componentDidMount()` and `onSearchSubmit()` methods fetch the first page on the first request. Every additional fetch should fetch the next page by providing the second argument. 654 | 655 | {title="src/App.js",lang="javascript"} 656 | ~~~~~~~~ 657 | class App extends Component { 658 | 659 | ... 660 | 661 | # leanpub-start-insert 662 | fetchSearchTopStories(searchTerm, page = 0) { 663 | fetch(`${PATH_BASE}${PATH_SEARCH}?${PARAM_SEARCH}${searchTerm}&${PARAM_PAGE}${page}`) 664 | # leanpub-end-insert 665 | .then(response => response.json()) 666 | .then(result => this.setSearchTopStories(result)) 667 | .catch(error => error); 668 | } 669 | 670 | ... 671 | 672 | } 673 | ~~~~~~~~ 674 | 675 | The page argument uses the JavaScript ES6 default parameter to introduce the fallback to page `0` in case no defined page argument is provided for the function. 676 | 677 | Now you can use the current page from the API response in `fetchSearchTopStories()`. You can use this method in a button to fetch more stories with an `onClick` button handler. Let's use the Button to fetch more paginated data from the Hacker News API. For this, we'll define the `onClick()` handler, which takes the current search term and the next page (current page + 1). 678 | 679 | {title="src/App.js",lang="javascript"} 680 | ~~~~~~~~ 681 | class App extends Component { 682 | 683 | ... 684 | 685 | render() { 686 | const { searchTerm, result } = this.state; 687 | # leanpub-start-insert 688 | const page = (result && result.page) || 0; 689 | # leanpub-end-insert 690 | return ( 691 |
692 |
693 | ... 694 | { result && 695 |
699 | } 700 | # leanpub-start-insert 701 |
702 | 705 |
706 | # leanpub-end-insert 707 | 708 | ); 709 | } 710 | } 711 | ~~~~~~~~ 712 | 713 | In your `render()` method, make sure to default to page 0 when there is no result. Remember, the `render()` method is called before the data is fetched asynchronously in the `componentDidMount()` lifecycle method. 714 | 715 | There is still one step missing, because fetching the next page of data will override your previous page of data. We want to concatenate the old and new list of hits from the local state and new result object, so we'll adjust its functionality to add new data rather than override it. 716 | 717 | {title="src/App.js",lang="javascript"} 718 | ~~~~~~~~ 719 | setSearchTopStories(result) { 720 | # leanpub-start-insert 721 | const { hits, page } = result; 722 | 723 | const oldHits = page !== 0 724 | ? this.state.result.hits 725 | : []; 726 | 727 | const updatedHits = [ 728 | ...oldHits, 729 | ...hits 730 | ]; 731 | 732 | this.setState({ 733 | result: { hits: updatedHits, page } 734 | }); 735 | # leanpub-end-insert 736 | } 737 | ~~~~~~~~ 738 | 739 | A couple things happen in the `setSearchTopStories()` method now. First, you get the hits and page from the result. 740 | 741 | Second, you have to check if there are already old hits. When the page is 0, it is a new search request from `componentDidMount()` or `onSearchSubmit()`. The hits are empty. But when you click the "More" button to fetch paginated data the page isn't 0. The old hits are already stored in your state and thus can be used. 742 | 743 | Third, you don't want to override the old hits. You can merge old and new hits from the recent API request, which can be done with a JavaScript ES6 array spread operator. 744 | 745 | Fourth, you set the merged hits and page in the local component state. 746 | 747 | Now we'll make one last adjustment. When you try the "More" button it only fetches a few list items. The API URL can be extended to fetch more list items with each request, so we add even more composable path constants: 748 | 749 | {title="src/App.js",lang="javascript"} 750 | ~~~~~~~~ 751 | const DEFAULT_QUERY = 'redux'; 752 | # leanpub-start-insert 753 | const DEFAULT_HPP = '100'; 754 | # leanpub-end-insert 755 | 756 | const PATH_BASE = 'https://hn.algolia.com/api/v1'; 757 | const PATH_SEARCH = '/search'; 758 | const PARAM_SEARCH = 'query='; 759 | const PARAM_PAGE = 'page='; 760 | # leanpub-start-insert 761 | const PARAM_HPP = 'hitsPerPage='; 762 | # leanpub-end-insert 763 | ~~~~~~~~ 764 | 765 | Now you can use the constants to extend the API URL. 766 | 767 | {title="src/App.js",lang="javascript"} 768 | ~~~~~~~~ 769 | fetchSearchTopStories(searchTerm, page = 0) { 770 | // be careful with the "\" which shows up in the PDF/print version of the book 771 | // it's only a line break a should not be in the actual code 772 | // https://github.com/the-road-to-learn-react/the-road-to-learn-react/issues/43 773 | # leanpub-start-insert 774 | fetch(`${PATH_BASE}${PATH_SEARCH}?${PARAM_SEARCH}${searchTerm}&${PARAM_PAGE}${page}&${PARAM_HPP}${DEFAULT_HPP}`) 775 | # leanpub-end-insert 776 | .then(response => response.json()) 777 | .then(result => this.setSearchTopStories(result)) 778 | .catch(error => error); 779 | } 780 | ~~~~~~~~ 781 | 782 | The request to the Hacker News API fetches more list items in one request than before. As you can see, a powerful API such as the Hacker News API gives plenty of ways to experiment with real world data. You should make use of it to make your endeavours when learning something new more exciting. That's [how I learned about the empowerment that APIs provide](https://www.robinwieruch.de/what-is-an-api-javascript/) when learning a new programming language or library. 783 | 784 | ### Exercises: 785 | 786 | * Confirm your [source code for the last section](http://bit.ly/2Hotq8c) 787 | * Confirm the [changes from the last section](http://bit.ly/2Hmrj52) 788 | * Read about [ES6 default parameters](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Functions/Default_parameters) 789 | * Experiment with the [Hacker News API parameters](https://hn.algolia.com/api) 790 | 791 | ## Client Cache 792 | 793 | Each search submit makes a request to the Hacker News API. You might search for "redux", followed by "react" and eventually "redux" again. In total it makes 3 requests. But you searched for "redux" twice and both times it took a whole asynchronous roundtrip to fetch the data. In a client-sided cache, you would store each result. When a request to the API is made, it checks if a result is already there and uses the cache if it is. Otherwise an API request is made to fetch the data. 794 | 795 | To have a client cache for each result, you have to store multiple `results` rather than one `result` in your local component state. The results object will be a map with the search term as key and the result as value. Each result from the API will be saved by the search term (key). 796 | 797 | At the moment, your result in the local state looks like this: 798 | 799 | {title="Code Playground",lang="javascript"} 800 | ~~~~~~~~ 801 | result: { 802 | hits: [ ... ], 803 | page: 2, 804 | } 805 | ~~~~~~~~ 806 | 807 | Imagine you have made two API requests. One for the search term "redux" and another one for "react". The results object should look like the following: 808 | 809 | {title="Code Playground",lang="javascript"} 810 | ~~~~~~~~ 811 | results: { 812 | redux: { 813 | hits: [ ... ], 814 | page: 2, 815 | }, 816 | react: { 817 | hits: [ ... ], 818 | page: 1, 819 | }, 820 | ... 821 | } 822 | ~~~~~~~~ 823 | 824 | Let's implement a client-side cache with React `setState()`. First, rename the `result` object to `results` in the initial component state. Second, define a temporary `searchKey` to store each `result`. 825 | 826 | {title="src/App.js",lang="javascript"} 827 | ~~~~~~~~ 828 | class App extends Component { 829 | 830 | constructor(props) { 831 | super(props); 832 | 833 | this.state = { 834 | # leanpub-start-insert 835 | results: null, 836 | searchKey: '', 837 | # leanpub-end-insert 838 | searchTerm: DEFAULT_QUERY, 839 | }; 840 | 841 | ... 842 | 843 | } 844 | 845 | ... 846 | 847 | } 848 | ~~~~~~~~ 849 | 850 | The `searchKey` has to be set before each request is made. It reflects the `searchTerm`. Understanding why we don't use `searchTerm` here is a crucial part to understand before continuing with the implementation. The `searchTerm` is a fluctuant variable, because it gets changed every time you type into the search input field. We want a non fluctuant variable that determines the recent submitted search term to the API and can be used to retrieve the correct result from the map of results. It is a pointer to your current result in the cache, which can be used to display the current result in the `render()` method. 851 | 852 | {title="src/App.js",lang="javascript"} 853 | ~~~~~~~~ 854 | componentDidMount() { 855 | const { searchTerm } = this.state; 856 | # leanpub-start-insert 857 | this.setState({ searchKey: searchTerm }); 858 | # leanpub-end-insert 859 | this.fetchSearchTopStories(searchTerm); 860 | } 861 | 862 | onSearchSubmit(event) { 863 | const { searchTerm } = this.state; 864 | # leanpub-start-insert 865 | this.setState({ searchKey: searchTerm }); 866 | # leanpub-end-insert 867 | this.fetchSearchTopStories(searchTerm); 868 | event.preventDefault(); 869 | } 870 | ~~~~~~~~ 871 | 872 | Now we will change where the result is stored to the local component state. It should store each result by `searchKey`. 873 | 874 | {title="src/App.js",lang="javascript"} 875 | ~~~~~~~~ 876 | class App extends Component { 877 | 878 | ... 879 | 880 | setSearchTopStories(result) { 881 | const { hits, page } = result; 882 | # leanpub-start-insert 883 | const { searchKey, results } = this.state; 884 | 885 | const oldHits = results && results[searchKey] 886 | ? results[searchKey].hits 887 | : []; 888 | # leanpub-end-insert 889 | 890 | const updatedHits = [ 891 | ...oldHits, 892 | ...hits 893 | ]; 894 | 895 | this.setState({ 896 | # leanpub-start-insert 897 | results: { 898 | ...results, 899 | [searchKey]: { hits: updatedHits, page } 900 | } 901 | # leanpub-end-insert 902 | }); 903 | } 904 | 905 | ... 906 | 907 | } 908 | ~~~~~~~~ 909 | 910 | The `searchKey` will be used as the key to save the updated hits and page in a `results` map. 911 | 912 | First, you have to retrieve the `searchKey` from the component state. Remember that the `searchKey` gets set on `componentDidMount()` and `onSearchSubmit()`. 913 | 914 | Second, the old hits have to get merged with the new hits as before. But this time the old hits get retrieved from the `results` map with the `searchKey` as key. 915 | 916 | Third, a new result can be set in the `results` map in the state. Let's examine the `results` object in `setState()`. 917 | 918 | {title="src/App.js",lang="javascript"} 919 | ~~~~~~~~ 920 | results: { 921 | ...results, 922 | [searchKey]: { hits: updatedHits, page } 923 | } 924 | ~~~~~~~~ 925 | 926 | The bottom part makes sure to store the updated result by `searchKey` in the results map. The value is an object with a hits and page property. The `searchKey` is the search term. You already learned the `[searchKey]: ...` syntax. It is an ES6 computed property name. It helps you allocate values dynamically in an object. 927 | 928 | The upper part needs to spread all other results by `searchKey` in the state using the object spread operator. Otherwise, you would lose all results that you have stored before. 929 | 930 | Now you store all results by search term. That's the first step to enable your cache. In the next step, you can retrieve the result depending on the non fluctuant `searchKey` from your map of results. That's why you had to introduce the `searchKey` in the first place as non fluctuant variable. Otherwise, the retrieval would be broken when you would use the fluctuant `searchTerm` to retrieve the current result, because this value might change when we use the Search component. 931 | 932 | {title="src/App.js",lang="javascript"} 933 | ~~~~~~~~ 934 | class App extends Component { 935 | 936 | ... 937 | 938 | render() { 939 | # leanpub-start-insert 940 | const { 941 | searchTerm, 942 | results, 943 | searchKey 944 | } = this.state; 945 | 946 | const page = ( 947 | results && 948 | results[searchKey] && 949 | results[searchKey].page 950 | ) || 0; 951 | 952 | const list = ( 953 | results && 954 | results[searchKey] && 955 | results[searchKey].hits 956 | ) || []; 957 | 958 | # leanpub-end-insert 959 | return ( 960 |
961 |
962 | ... 963 |
964 | # leanpub-start-insert 965 |
969 | # leanpub-end-insert 970 |
971 | # leanpub-start-insert 972 | 976 |
977 | 978 | ); 979 | } 980 | } 981 | ~~~~~~~~ 982 | 983 | Since you default to an empty list when there is no result by `searchKey`, you can spare the conditional rendering for the Table component. Additionally, we need to pass the `searchKey` rather than the `searchTerm` to the "More" button. Otherwise, your paginated fetch depends on the `searchTerm` value which is fluctuant. Moreover make sure to keep the fluctuant `searchTerm` property for the input field in the Search component. 984 | 985 | The search functionality should store all results from the Hacker News API now. 986 | 987 | Now, we can see the `onDismiss()` method needs improvement, as it still deals with the `result` object. We want it deal with multiple `results`: 988 | 989 | {title="src/App.js",lang="javascript"} 990 | ~~~~~~~~ 991 | onDismiss(id) { 992 | # leanpub-start-insert 993 | const { searchKey, results } = this.state; 994 | const { hits, page } = results[searchKey]; 995 | # leanpub-end-insert 996 | 997 | const isNotId = item => item.objectID !== id; 998 | # leanpub-start-insert 999 | const updatedHits = hits.filter(isNotId); 1000 | 1001 | this.setState({ 1002 | results: { 1003 | ...results, 1004 | [searchKey]: { hits: updatedHits, page } 1005 | } 1006 | }); 1007 | # leanpub-end-insert 1008 | } 1009 | ~~~~~~~~ 1010 | 1011 | Check to make sure the "Dismiss" button is working again. 1012 | 1013 | Note that nothing will stop the application from sending an API request on each search submit. Though there might be already a result, there is no check that prevents the request, so the cache functionality is not complete yet. It caches the results, but it doesn't make use of them. The last step is to prevent the API request when a result is available in the cache: 1014 | 1015 | {title="src/App.js",lang="javascript"} 1016 | ~~~~~~~~ 1017 | class App extends Component { 1018 | 1019 | constructor(props) { 1020 | 1021 | ... 1022 | 1023 | # leanpub-start-insert 1024 | this.needsToSearchTopStories = this.needsToSearchTopStories.bind(this); 1025 | # leanpub-end-insert 1026 | this.setSearchTopStories = this.setSearchTopStories.bind(this); 1027 | this.fetchSearchTopStories = this.fetchSearchTopStories.bind(this); 1028 | this.onSearchChange = this.onSearchChange.bind(this); 1029 | this.onSearchSubmit = this.onSearchSubmit.bind(this); 1030 | this.onDismiss = this.onDismiss.bind(this); 1031 | } 1032 | 1033 | # leanpub-start-insert 1034 | needsToSearchTopStories(searchTerm) { 1035 | return !this.state.results[searchTerm]; 1036 | } 1037 | # leanpub-end-insert 1038 | 1039 | ... 1040 | 1041 | onSearchSubmit(event) { 1042 | const { searchTerm } = this.state; 1043 | this.setState({ searchKey: searchTerm }); 1044 | # leanpub-start-insert 1045 | 1046 | if (this.needsToSearchTopStories(searchTerm)) { 1047 | this.fetchSearchTopStories(searchTerm); 1048 | } 1049 | 1050 | # leanpub-end-insert 1051 | event.preventDefault(); 1052 | } 1053 | 1054 | ... 1055 | 1056 | } 1057 | ~~~~~~~~ 1058 | 1059 | Now your client only makes a request to the API once, though you searched for a term twice. Even paginated data with several pages gets cached that way, because you always save the last page for each result in the `results` map. This is a powerful approach to introduce caching into an application. The Hacker News API provides you with everything you need to cache paginated data effectively. 1060 | 1061 | ## Error Handling 1062 | 1063 | Now we've taken care of interactions with the Hacker News API. We've introduced an elegant way to cache results from the API and make use of its paginated list functionality to fetch an endless list of sublists of stories from the API. But no application is complete without error handling. 1064 | 1065 | In this chapter, we introduce an efficient solution to add error handling for your application in case of an erroneous API request. We have learned the necessary building block to introduce error handling in React: local state and conditional rendering. The error is just another state, which we store in the local state and display with conditional rendering in the component. 1066 | 1067 | Now, we'll implement it in the App component, since that's where we fetch data from the Hacker News API. First, introduce the error in the local state. It is initialized as null, but will be set to the error object in case of an error. 1068 | 1069 | {title="src/App.js",lang="javascript"} 1070 | ~~~~~~~~ 1071 | class App extends Component { 1072 | constructor(props) { 1073 | super(props); 1074 | 1075 | this.state = { 1076 | results: null, 1077 | searchKey: '', 1078 | searchTerm: DEFAULT_QUERY, 1079 | # leanpub-start-insert 1080 | error: null, 1081 | # leanpub-end-insert 1082 | }; 1083 | 1084 | ... 1085 | } 1086 | 1087 | ... 1088 | 1089 | } 1090 | ~~~~~~~~ 1091 | 1092 | Second, use the catch block in your native fetch to store the error object in the local state by using `setState()`. Every time the API request isn't successful, the catch block is executed. 1093 | 1094 | {title="src/App.js",lang="javascript"} 1095 | ~~~~~~~~ 1096 | class App extends Component { 1097 | 1098 | ... 1099 | 1100 | fetchSearchTopStories(searchTerm, page = 0) { 1101 | // be careful with the "\" which shows up in the PDF/print version of the book 1102 | // it's only a line break and should not be in the actual code 1103 | // https://github.com/the-road-to-learn-react/the-road-to-learn-react/issues/43 1104 | fetch(`${PATH_BASE}${PATH_SEARCH}?${PARAM_SEARCH}${searchTerm}&${PARAM_PAGE}${page}&${PARAM_HPP}${DEFAULT_HPP}`) 1105 | .then(response => response.json()) 1106 | .then(result => this.setSearchTopStories(result)) 1107 | # leanpub-start-insert 1108 | .catch(error => this.setState({ error })); 1109 | # leanpub-end-insert 1110 | } 1111 | 1112 | ... 1113 | 1114 | } 1115 | ~~~~~~~~ 1116 | 1117 | Third, retrieve the error object from your local state in the `render()` method, and display a message in case of an error using React's conditional rendering. 1118 | 1119 | {title="src/App.js",lang="javascript"} 1120 | ~~~~~~~~ 1121 | class App extends Component { 1122 | 1123 | ... 1124 | 1125 | render() { 1126 | const { 1127 | searchTerm, 1128 | results, 1129 | searchKey, 1130 | # leanpub-start-insert 1131 | error 1132 | # leanpub-end-insert 1133 | } = this.state; 1134 | 1135 | ... 1136 | 1137 | # leanpub-start-insert 1138 | if (error) { 1139 | return

Something went wrong.

; 1140 | } 1141 | # leanpub-end-insert 1142 | 1143 | return ( 1144 |
1145 | ... 1146 |
1147 | ); 1148 | } 1149 | } 1150 | ~~~~~~~~ 1151 | 1152 | If you want to test that your error handling is working, change the API URL to something non-existent to force an error. 1153 | 1154 | {title="src/App.js",lang="javascript"} 1155 | ~~~~~~~~ 1156 | const PATH_BASE = 'https://hn.mydomain.com/api/v1'; 1157 | ~~~~~~~~ 1158 | 1159 | Now you should see an error message instead of your application. It's up to you where you want to place the conditional rendering for the error message. In this case, the app is overridden by the error message, but that wouldn't be the best user experience. The remaining application would still be visible so the user knows it's still running: 1160 | 1161 | {title="src/App.js",lang="javascript"} 1162 | ~~~~~~~~ 1163 | class App extends Component { 1164 | 1165 | ... 1166 | 1167 | render() { 1168 | const { 1169 | searchTerm, 1170 | results, 1171 | searchKey, 1172 | error 1173 | } = this.state; 1174 | 1175 | const page = ( 1176 | results && 1177 | results[searchKey] && 1178 | results[searchKey].page 1179 | ) || 0; 1180 | 1181 | const list = ( 1182 | results && 1183 | results[searchKey] && 1184 | results[searchKey].hits 1185 | ) || []; 1186 | 1187 | return ( 1188 |
1189 |
1190 | ... 1191 |
1192 | # leanpub-start-insert 1193 | { error 1194 | ?
1195 |

Something went wrong.

1196 |
1197 | :
1201 | } 1202 | # leanpub-end-insert 1203 | ... 1204 | 1205 | ); 1206 | } 1207 | } 1208 | ~~~~~~~~ 1209 | 1210 | **Remember** to revert the URL for the API to the existent one: 1211 | 1212 | {title="src/App.js",lang="javascript"} 1213 | ~~~~~~~~ 1214 | const PATH_BASE = 'https://hn.algolia.com/api/v1'; 1215 | ~~~~~~~~ 1216 | 1217 | The application now has error handling in case the API request fails. 1218 | 1219 | ### Exercises: 1220 | 1221 | * Confirm your [source code for the last section](http://bit.ly/2CYMRlf) 1222 | * Confirm the [changes from the last section](http://bit.ly/2CZYet7) 1223 | * Read about [React's Error Handling for Components](https://reactjs.org/blog/2017/07/26/error-handling-in-react-16.html) 1224 | 1225 | ## Axios instead of Fetch 1226 | 1227 | Before, we introduced the native fetch API to perform a request to the Hacker News platform. The browser allows you to use this native fetch API. However, not all browsers support this, older browsers especially. Once you start testing your application in a headless browser environment (where there is no browser, it is mocked), there can be issues with the fetch API. There are a couple of ways to make fetch work in older browsers (polyfills) and in tests ([isomorphic-fetch](https://github.com/matthew-andrews/isomorphic-fetch)), but these concepts are bit off-task for the purpose of learning React. 1228 | 1229 | One alternative is to substitute the native fetch API with a stable library such as [axios](https://github.com/axios/axios), which performs asynchronous requests to remote APIs. In this chapter, we will discover how to substitute a library, a native API of the browser in this case, with another library. 1230 | 1231 | Below, the native fetch API is substituted with axios. First, we install axios on the command line: 1232 | 1233 | {title="Command Line",lang="text"} 1234 | ~~~~~~~~ 1235 | npm install axios 1236 | ~~~~~~~~ 1237 | 1238 | Second, import axios in your App component's file: 1239 | 1240 | {title="src/App.js",lang="javascript"} 1241 | ~~~~~~~~ 1242 | import React, { Component } from 'react'; 1243 | # leanpub-start-insert 1244 | import axios from 'axios'; 1245 | # leanpub-end-insert 1246 | import './App.css'; 1247 | 1248 | ... 1249 | ~~~~~~~~ 1250 | 1251 | You can use axios instead of `fetch()`, and its usage looks almost identical to the native fetch API. It takes the URL as argument and returns a promise. You don't have to transform the returned response to JSON anymore, since axios wraps the result into a `data` object in JavaScript. Just make sure to adapt your code to the returned data structure: 1252 | 1253 | {title="src/App.js",lang="javascript"} 1254 | ~~~~~~~~ 1255 | class App extends Component { 1256 | 1257 | ... 1258 | 1259 | fetchSearchTopStories(searchTerm, page = 0) { 1260 | # leanpub-start-insert 1261 | axios(`${PATH_BASE}${PATH_SEARCH}?${PARAM_SEARCH}${searchTerm}&${PARAM_PAGE}${page}&${PARAM_HPP}${DEFAULT_HPP}`) 1262 | .then(result => this.setSearchTopStories(result.data)) 1263 | # leanpub-end-insert 1264 | .catch(error => this.setState({ error })); 1265 | } 1266 | 1267 | ... 1268 | 1269 | } 1270 | ~~~~~~~~ 1271 | 1272 | In this code, we call `axios()`, which uses an HTTP GET request by default. You can make the GET request explicit by calling `axios.get()`, or you can use another HTTP method such as HTTP POST with `axios.post()`. With these examples alone, we can see that axios is a powerful library to perform requests to remote APIs. I recommend you use it instead of the native fetch API when requests become complex, or you have to deal with promises. 1273 | 1274 | There exists another improvement for the Hacker News request in the App component. Imagine the component mounts when the page is rendered for the first time in the browser. In `componentDidMount()` the component starts to make the request, but then the user navigates away from the page with this rendered component. Then the App component unmounts, but there is still a pending request from `componentDidMount()` lifecycle method. It will attempt to use `this.setState()` eventually in the `then()` or `catch()` block of the promise. You will likely see the following warning(s) on your command line, or in your browser's developer output: 1275 | 1276 | * *Warning: Can only update a mounted or mounting component. This usually means you called setState, replaceState, or forceUpdate on an unmounted component. This is a no-op.* 1277 | 1278 | * *Warning: Can't call setState (or forceUpdate) on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.* 1279 | 1280 | If you encounter one of them in your browser's developer tools, checkout [this walkthrough on how to prevent memory leaks in React](https://www.robinwieruch.de/react-warning-cant-call-setstate-on-an-unmounted-component/). It isn't too important for starting out with React, but I have seen many React beginners being confused about this warning. 1281 | 1282 | This chapter has shown you how you can replace one library with another in React. If you run into any issues, you can use the vast library ecosystem in JavaScript to find a solution. 1283 | 1284 | ### Exercises: 1285 | 1286 | * Confirm your [source code for the last section](http://bit.ly/2Hotuos) 1287 | * Confirm the [changes from the last section](http://bit.ly/2HkpMMC) 1288 | * Read about [why frameworks matter](https://www.robinwieruch.de/why-frameworks-matter/) 1289 | 1290 | {pagebreak} 1291 | 1292 | Now you've learned to interact with an API in React! Let's recap the last chapter: 1293 | 1294 | * **React** 1295 | * ES6 class component lifecycle methods for different use cases 1296 | * `componentDidMount()` for API interactions 1297 | * Conditional renderings 1298 | * Synthetic events on forms 1299 | * Error handling 1300 | * Preventing `this.setState` in unmounted components 1301 | * **ES6 and beyond** 1302 | * Template strings to compose strings 1303 | * Spread operator for immutable data structures 1304 | * Computed property names 1305 | * Class fields 1306 | * **General** 1307 | * Hacker News API interaction 1308 | * Native fetch browser API 1309 | * Client and server-side search 1310 | * Pagination of data 1311 | * Client-side caching 1312 | * Axios as an alternative for the native fetch API 1313 | 1314 | Again, it makes sense to take a break, internalize the lessons and apply them on your own. Experiment with the parameters for the API endpoint to query different results. You can find the source code in the [official repository](http://bit.ly/2Hotuos). 1315 | -------------------------------------------------------------------------------- /manuscript/chapter4.md: -------------------------------------------------------------------------------- 1 | # Code Organization and Testing 2 | 3 | The chapter will focus on keeping code organized in a scaling application, so we will cover the best practices for structuring your folders and files. We will also cover testing, an important practice to keep source code robust, as well as useful tools for debugging a React application. This chapter will step back from the practical application for a moment, so we can cover these topics in detail before moving on. 4 | 5 | ## ES6 Modules: Import and Export 6 | 7 | In JavaScript ES6, you can import and export functionalities from modules. These can be functions, classes, components, constants, essentially anything you can assign to a variable. Modules can be single files or whole folders with one index file as entry point. 8 | 9 | After we bootstrapped our application with *create-react-app* at the beginning, we already used several `import` and `export` statements in the initial files. The `import` and `export` statements help you to share code across multiple files. Historically there were already several solutions for this in the JavaScript environment, but it was a mess because there wasn't standardized method of performing this task. JavaScript ES6 added it as a native behavior eventually. 10 | 11 | These statements embrace code splitting, where we distribute code across multiple files to keep it reusable and maintainable. The former is true because we can import a piece of code into multiple files. The latter is true because there is only one source where you maintain the piece of code. 12 | 13 | We also want to think about code encapsulation, since not every functionality needs to be exported from a file. Some of these functionalities should only be used in files where they have been defined. File exports are basically a public API to a file, where only the exported functionalities are available to be reused elsewhere. This follows the best practice of encapsulation. 14 | 15 | The following examples showcase the statements by sharing one or multiple variables across two files. In the end, the approach can scale to multiple files and could share more than simple variables. 16 | 17 | The act of exporting one or multiple variables is called a named export: 18 | 19 | {title="Code Playground: file1.js",lang="javascript"} 20 | ~~~~~~~~ 21 | const firstname = 'Robin'; 22 | const lastname = 'Wieruch'; 23 | 24 | export { firstname, lastname }; 25 | ~~~~~~~~ 26 | 27 | And import them in another file with a relative path to the first file. 28 | 29 | {title="Code Playground: file2.js",lang="javascript"} 30 | ~~~~~~~~ 31 | import { firstname, lastname } from './file1.js'; 32 | 33 | console.log(firstname); 34 | // output: Robin 35 | ~~~~~~~~ 36 | 37 | You can also import all exported variables from another file as one object. 38 | 39 | {title="Code Playground: file2.js",lang="javascript"} 40 | ~~~~~~~~ 41 | import * as person from './file1.js'; 42 | 43 | console.log(person.firstname); 44 | // output: Robin 45 | ~~~~~~~~ 46 | 47 | Imports can have aliases, which are necessary when we import functionalities from multiple files that have the same named export. 48 | 49 | {title="Code Playground: file2.js",lang="javascript"} 50 | ~~~~~~~~ 51 | import { firstname as username } from './file1.js'; 52 | 53 | console.log(username); 54 | // output: Robin 55 | ~~~~~~~~ 56 | 57 | There is also the `default` statement, which can be used for a few cases: 58 | 59 | * to export and import a single functionality 60 | * to highlight the main functionality of the exported API of a module 61 | * to have a fallback import functionality 62 | 63 | {title="Code Playground: file1.js",lang="javascript"} 64 | ~~~~~~~~ 65 | const robin = { 66 | firstname: 'Robin', 67 | lastname: 'Wieruch', 68 | }; 69 | 70 | export default robin; 71 | ~~~~~~~~ 72 | 73 | You have to leave out the curly braces to import the default export. 74 | 75 | {title="Code Playground: file2.js",lang="javascript"} 76 | ~~~~~~~~ 77 | import developer from './file1.js'; 78 | 79 | console.log(developer); 80 | // output: { firstname: 'Robin', lastname: 'Wieruch' } 81 | ~~~~~~~~ 82 | 83 | The import name can differ from the exported default name, and it can be used with the named export and import statements: 84 | 85 | {title="Code Playground: file1.js",lang="javascript"} 86 | ~~~~~~~~ 87 | const firstname = 'Robin'; 88 | const lastname = 'Wieruch'; 89 | 90 | const person = { 91 | firstname, 92 | lastname, 93 | }; 94 | 95 | export { 96 | firstname, 97 | lastname, 98 | }; 99 | 100 | export default person; 101 | ~~~~~~~~ 102 | 103 | Import the default or the named exports in another file: 104 | 105 | {title="Code Playground: file2.js",lang="javascript"} 106 | ~~~~~~~~ 107 | import developer, { firstname, lastname } from './file1.js'; 108 | 109 | console.log(developer); 110 | // output: { firstname: 'Robin', lastname: 'Wieruch' } 111 | console.log(firstname, lastname); 112 | // output: Robin Wieruch 113 | ~~~~~~~~ 114 | 115 | You can spare the extra lines, and export the variables directly for named exports. 116 | 117 | {title="Code Playground: file1.js",lang="javascript"} 118 | ~~~~~~~~ 119 | export const firstname = 'Robin'; 120 | export const lastname = 'Wieruch'; 121 | ~~~~~~~~ 122 | 123 | These are the main functionalities for ES6 modules. They help you to organize your code, to maintain it, and to design reusable module APIs. You can also export and import functionalities to test them which you will do in a later chapter. 124 | 125 | ### Exercises: 126 | 127 | * Read about [ES6 import](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import) 128 | * Read about [ES6 export](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export) 129 | 130 | ## Code Organization with ES6 Modules 131 | 132 | You might wonder why we didn't follow the best practices of putting components into different files for the *src/App.js* file. We already have multiple components in the file that can be defined in their own files/folders (modules). For the sake of learning React, it is practical to keep these items in one place. Once your React application grows, you should consider splitting these components into multiple modules so it can scale properly. 133 | 134 | In the following, I propose several module structures you can apply. You can apply them as an exercise later, but we will continue developing our app with the *src/App.js* file to keep things simple. 135 | 136 | Here is one possible module structure: 137 | 138 | {title="Folder Structure",lang="text"} 139 | ~~~~~~~~ 140 | src/ 141 | index.js 142 | index.css 143 | App.js 144 | App.test.js 145 | App.css 146 | Button.js 147 | Button.test.js 148 | Button.css 149 | Table.js 150 | Table.test.js 151 | Table.css 152 | Search.js 153 | Search.test.js 154 | Search.css 155 | ~~~~~~~~ 156 | 157 | This one separates the components into their own files, but it doesn't look too promising. You can see a lot of naming duplications, and only the file extension differs. Another module structure could be: 158 | 159 | {title="Folder Structure",lang="text"} 160 | ~~~~~~~~ 161 | src/ 162 | index.js 163 | index.css 164 | App/ 165 | index.js 166 | test.js 167 | index.css 168 | Button/ 169 | index.js 170 | test.js 171 | index.css 172 | Table/ 173 | index.js 174 | test.js 175 | index.css 176 | Search/ 177 | index.js 178 | test.js 179 | index.css 180 | ~~~~~~~~ 181 | 182 | Now it looks cleaner than before. The index naming of a file describes it as an entry point file to the folder. It is just a common naming convention, but you can use your own naming as well. In this module structure, a component is defined by its component declaration in the JavaScript file, but also by its style and tests. If you have trouble finding your components this way while searching for them in your editor/IDE, search for paths rather than files (e.g. search for "Table index"). 183 | 184 | Another step is extracting the constant variables from the App component, which were used to compose the Hacker News API URL. 185 | 186 | {title="Folder Structure",lang="text"} 187 | ~~~~~~~~ 188 | src/ 189 | index.js 190 | index.css 191 | # leanpub-start-insert 192 | constants/ 193 | index.js 194 | components/ 195 | # leanpub-end-insert 196 | App/ 197 | index.js 198 | test.js 199 | index.css 200 | Button/ 201 | index.js 202 | test.js 203 | index.css 204 | ... 205 | ~~~~~~~~ 206 | 207 | Naturally, the modules would split up into *src/constants/* and *src/components/*. The *src/constants/index.js* file could look like the following: 208 | 209 | {title="Code Playground: src/constants/index.js",lang="javascript"} 210 | ~~~~~~~~ 211 | export const DEFAULT_QUERY = 'redux'; 212 | export const DEFAULT_HPP = '100'; 213 | export const PATH_BASE = 'https://hn.algolia.com/api/v1'; 214 | export const PATH_SEARCH = '/search'; 215 | export const PARAM_SEARCH = 'query='; 216 | export const PARAM_PAGE = 'page='; 217 | export const PARAM_HPP = 'hitsPerPage='; 218 | ~~~~~~~~ 219 | 220 | The *App/index.js* file could import these variables in order to use them. 221 | 222 | {title="Code Playground: src/components/App/index.js",lang="javascript"} 223 | ~~~~~~~~ 224 | import { 225 | DEFAULT_QUERY, 226 | DEFAULT_HPP, 227 | PATH_BASE, 228 | PATH_SEARCH, 229 | PARAM_SEARCH, 230 | PARAM_PAGE, 231 | PARAM_HPP, 232 | } from '../../constants/index.js'; 233 | 234 | ... 235 | ~~~~~~~~ 236 | 237 | When you use the *index.js* naming convention, you can omit the filename from the relative path. 238 | 239 | {title="Code Playground: src/components/App/index.js",lang="javascript"} 240 | ~~~~~~~~ 241 | import { 242 | DEFAULT_QUERY, 243 | DEFAULT_HPP, 244 | PATH_BASE, 245 | PATH_SEARCH, 246 | PARAM_SEARCH, 247 | PARAM_PAGE, 248 | PARAM_HPP, 249 | # leanpub-start-insert 250 | } from '../../constants'; 251 | # leanpub-end-insert 252 | 253 | ... 254 | ~~~~~~~~ 255 | 256 | The *index.js* file naming convention was introduced in the Node.js world, where the index file is the entry point to a module. It describes the public API to the module. External modules are only allowed to use the *index.js* file to import shared code from the module. Consider the following module structure to demonstrate it: 257 | 258 | {title="Folder Structure",lang="text"} 259 | ~~~~~~~~ 260 | src/ 261 | index.js 262 | App/ 263 | index.js 264 | Buttons/ 265 | index.js 266 | SubmitButton.js 267 | SaveButton.js 268 | CancelButton.js 269 | ~~~~~~~~ 270 | 271 | The *Buttons/* folder has multiple button components defined in its distinct files. Each file can `export default` the specific component, making it available to *Buttons/index.js*. The *Buttons/index.js* file imports all different button representations and exports them as public module API. 272 | 273 | {title="Code Playground: src/Buttons/index.js",lang="javascript"} 274 | ~~~~~~~~ 275 | import SubmitButton from './SubmitButton'; 276 | import SaveButton from './SaveButton'; 277 | import CancelButton from './CancelButton'; 278 | 279 | export { 280 | SubmitButton, 281 | SaveButton, 282 | CancelButton, 283 | }; 284 | ~~~~~~~~ 285 | 286 | Now the *src/App/index.js* can import the buttons from the public module API located in the *index.js* file. 287 | 288 | {title="Code Playground: src/App/index.js",lang="javascript"} 289 | ~~~~~~~~ 290 | import { 291 | SubmitButton, 292 | SaveButton, 293 | CancelButton 294 | } from '../Buttons'; 295 | ~~~~~~~~ 296 | 297 | However, it can be seen as bad practice to reach into other files than the *index.js* in the module, as it breaks the rules of encapsulation. 298 | 299 | {title="Code Playground: src/App/index.js",lang="javascript"} 300 | ~~~~~~~~ 301 | // bad practice, don't do it 302 | import SubmitButton from '../Buttons/SubmitButton'; 303 | ~~~~~~~~ 304 | 305 | We refactored the source code in a module with the constraints of encapsulation. Again, we'll keep things simple in this book, but you should always refactor your source code to keep it clean. 306 | 307 | ### Exercises: 308 | 309 | * Refactor your *src/App.js* file into multiple component modules when you finished the book 310 | 311 | ## Snapshot Tests with Jest 312 | 313 | Testing source code is essential to programming, and should be seen as mandatory. We want to keep the quality of your code high and make sure everything works before using it in production. We will use the testing pyramid as our guide. 314 | 315 | The testing pyramid includes end-to-end tests, integration tests, and unit tests. A unit test is for an isolated and small block of code, such a single function. However, sometimes units work well in isolation but not in combination with other units, so they need to be tested as a group. That's where integration tests can help us figure out if units work well together. An end-to-end test is a simulation of a real-life scenario, such as the automated setup of a browser simulating the login flow in a web application. Where unit tests are fast and easy to write and maintain, end-to-end tests are at the opposite of the spectrum. 316 | 317 | We want to have many unit tests covering the isolated functions. After that, we can use several integration tests to make sure the most important functions work in combination as expected. Finally, we may need a few end-to-end tests to simulate critical scenarios. 318 | 319 | The foundation for testing in React are component tests, generalized partly as unit tests and partly as snapshot tests. Unit tests for our components will be covered in the next chapter with a library called Enzyme. In this section, we focus on snapshot tests, using [Jest](https://jestjs.io/). 320 | 321 | Jest is a JavaScript testing framework used by Facebook. It is used for component tests by the React community. Fortunately, *create-react-app* already comes with Jest, so you don't need to set it up. 322 | 323 | Before you can test your first components, you have to export them from your *src/App.js* file. Afterward, you can test them in a different file. 324 | 325 | {title="src/App.js",lang="javascript"} 326 | ~~~~~~~~ 327 | ... 328 | 329 | class App extends Component { 330 | ... 331 | } 332 | 333 | ... 334 | 335 | export default App; 336 | 337 | # leanpub-start-insert 338 | export { 339 | Button, 340 | Search, 341 | Table, 342 | }; 343 | # leanpub-end-insert 344 | ~~~~~~~~ 345 | 346 | In your *App.test.js* file, you will find the first test that came with *create-react-app*. It verifies the App component will render without errors. 347 | 348 | {title="src/App.test.js",lang="javascript"} 349 | ~~~~~~~~ 350 | import React from 'react'; 351 | import ReactDOM from 'react-dom'; 352 | import App from './App'; 353 | 354 | it('renders without crashing', () => { 355 | const div = document.createElement('div'); 356 | ReactDOM.render(, div); 357 | ReactDOM.unmountComponentAtNode(div); 358 | }); 359 | ~~~~~~~~ 360 | 361 | The "it"-block describes one test case. It comes with a test description, and when you test it, it can either succeed or fail. Furthermore, you could wrap it into a "describe"-block that defines your test suite. A test suite could include a bunch of the "it"-blocks for one specific component. You will see "describe"-blocks later. Both blocks are used to separate and organize your test cases. 362 | 363 | Note that the `it` function is acknowledged in the JavaScript community as the function where you run a single test. However, in Jest it is often found as an alias `test` function. 364 | 365 | You can run the test cases using the interactive *create-react-app* test script on the command line. You will get the output for all test cases on your command line interface. 366 | 367 | {title="Command Line",lang="text"} 368 | ~~~~~~~~ 369 | npm test 370 | ~~~~~~~~ 371 | 372 | Jest enables you to write snapshot tests. These tests make a snapshot of your rendered component and runs it against future snapshots. When a future snapshot changes, you will get notified in the test. You can either accept the snapshot change, because you changed the component implementation on purpose, or deny the change and investigate for the error. It complements unit tests very well, because you only test the differences of the rendered output. It doesn't add big maintenance costs since you can accept snapshots for intentional changes. 373 | 374 | Jest stores snapshots in a folder so it can validate the diff against a future snapshot. This also lets users share snapshots across teams when having version control such as git in place. 375 | 376 | Before writing your first snapshot test with Jest, you have to install its utility library: 377 | 378 | {title="Command Line",lang="text"} 379 | ~~~~~~~~ 380 | npm install --save-dev react-test-renderer 381 | ~~~~~~~~ 382 | 383 | Now you can extend the App component test with your first snapshot test. First, import the new functionality from the node package and wrap your previous "it"-block for the App component into a descriptive "describe"-block. In this case, the test suite is only for the App component. 384 | 385 | {title="src/App.test.js",lang="javascript"} 386 | ~~~~~~~~ 387 | import React from 'react'; 388 | import ReactDOM from 'react-dom'; 389 | # leanpub-start-insert 390 | import renderer from 'react-test-renderer'; 391 | # leanpub-end-insert 392 | import App from './App'; 393 | 394 | # leanpub-start-insert 395 | describe('App', () => { 396 | 397 | # leanpub-end-insert 398 | it('renders without crashing', () => { 399 | const div = document.createElement('div'); 400 | ReactDOM.render(, div); 401 | ReactDOM.unmountComponentAtNode(div); 402 | }); 403 | # leanpub-start-insert 404 | 405 | }); 406 | # leanpub-end-insert 407 | ~~~~~~~~ 408 | 409 | Now you can implement your first snapshot test by using a "test"-block: 410 | 411 | {title="src/App.test.js",lang="javascript"} 412 | ~~~~~~~~ 413 | import React from 'react'; 414 | import ReactDOM from 'react-dom'; 415 | import renderer from 'react-test-renderer'; 416 | import App from './App'; 417 | 418 | describe('App', () => { 419 | 420 | it('renders without crashing', () => { 421 | const div = document.createElement('div'); 422 | ReactDOM.render(, div); 423 | ReactDOM.unmountComponentAtNode(div); 424 | }); 425 | 426 | # leanpub-start-insert 427 | test('has a valid snapshot', () => { 428 | const component = renderer.create( 429 | 430 | ); 431 | const tree = component.toJSON(); 432 | expect(tree).toMatchSnapshot(); 433 | }); 434 | 435 | # leanpub-end-insert 436 | }); 437 | ~~~~~~~~ 438 | 439 | Run your tests again and observe how they succeed or fail. Once you change the output of the render block in your App component, the snapshot test should fail. Then you can decide to update the snapshot or investigate in your App component. 440 | 441 | The `renderer.create()` function creates a snapshot of your App component. It renders it virtually, and then stores the DOM into a snapshot. Afterward, the snapshot is expected to match the previous version from the last test. This is how we make sure the DOM stays the same and doesn't change anything by accident. 442 | 443 | Let's add more tests for our independent components. First, the Search component: 444 | 445 | {title="src/App.test.js",lang="javascript"} 446 | ~~~~~~~~ 447 | import React from 'react'; 448 | import ReactDOM from 'react-dom'; 449 | import renderer from 'react-test-renderer'; 450 | # leanpub-start-insert 451 | import App, { Search } from './App'; 452 | # leanpub-end-insert 453 | 454 | ... 455 | 456 | # leanpub-start-insert 457 | describe('Search', () => { 458 | 459 | it('renders without crashing', () => { 460 | const div = document.createElement('div'); 461 | ReactDOM.render(Search, div); 462 | ReactDOM.unmountComponentAtNode(div); 463 | }); 464 | 465 | test('has a valid snapshot', () => { 466 | const component = renderer.create( 467 | Search 468 | ); 469 | const tree = component.toJSON(); 470 | expect(tree).toMatchSnapshot(); 471 | }); 472 | 473 | }); 474 | # leanpub-end-insert 475 | ~~~~~~~~ 476 | 477 | The Search component has two tests similar to the App component. The first test simply renders the Search component to the DOM and verifies that there is no error during the rendering process. If there would be an error, the test would break even though there isn't any assertion (e.g. expect, match, equal) in the test block. The second snapshot test is used to store a snapshot of the rendered component and to run it against a previous snapshot. It fails when the snapshot has changed. 478 | 479 | Second, you can test the Button component whereas the same test rules as in the Search component apply. 480 | 481 | {title="src/App.test.js",lang="javascript"} 482 | ~~~~~~~~ 483 | ... 484 | # leanpub-start-insert 485 | import App, { Search, Button } from './App'; 486 | # leanpub-end-insert 487 | 488 | ... 489 | 490 | # leanpub-start-insert 491 | describe('Button', () => { 492 | 493 | it('renders without crashing', () => { 494 | const div = document.createElement('div'); 495 | ReactDOM.render(, div); 496 | ReactDOM.unmountComponentAtNode(div); 497 | }); 498 | 499 | test('has a valid snapshot', () => { 500 | const component = renderer.create( 501 | 502 | ); 503 | const tree = component.toJSON(); 504 | expect(tree).toMatchSnapshot(); 505 | }); 506 | 507 | }); 508 | # leanpub-end-insert 509 | ~~~~~~~~ 510 | 511 | Finally, the Table component that you can pass a bunch of initial props to render it with a sample list. 512 | 513 | {title="src/App.test.js",lang="javascript"} 514 | ~~~~~~~~ 515 | ... 516 | # leanpub-start-insert 517 | import App, { Search, Button, Table } from './App'; 518 | # leanpub-end-insert 519 | 520 | ... 521 | 522 | # leanpub-start-insert 523 | describe('Table', () => { 524 | 525 | const props = { 526 | list: [ 527 | { title: '1', author: '1', num_comments: 1, points: 2, objectID: 'y' }, 528 | { title: '2', author: '2', num_comments: 1, points: 2, objectID: 'z' }, 529 | ], 530 | }; 531 | 532 | it('renders without crashing', () => { 533 | const div = document.createElement('div'); 534 | ReactDOM.render(
, div); 535 | }); 536 | 537 | test('has a valid snapshot', () => { 538 | const component = renderer.create( 539 |
540 | ); 541 | const tree = component.toJSON(); 542 | expect(tree).toMatchSnapshot(); 543 | }); 544 | 545 | }); 546 | # leanpub-end-insert 547 | ~~~~~~~~ 548 | 549 | Snapshot tests usually stay pretty basic. You only want to make sure the component doesn't change its output. Once it does, you have to decide if you accept the changes, otherwise you have to fix the component. 550 | 551 | ### Exercises: 552 | 553 | * Confirm your [source code for the last section](http://bit.ly/2H9GkI3) 554 | * Confirm the [changes from the last section](http://bit.ly/2Ha22f6) 555 | * See how a snapshot test fails once you change your component's return value in the `render()` method 556 | * Accept or deny the snapshot change(s) 557 | * Keep your snapshots tests up to date when the implementation of components change in next chapters 558 | * Read about [Jest in React](https://jestjs.io/docs/en/tutorial-react) 559 | 560 | ## Unit Tests with Enzyme 561 | 562 | [Enzyme](https://github.com/airbnb/enzyme) is a testing utility by Airbnb to assert, manipulate, and traverse React components. It is used to conduct unit tests to complement snapshot tests in React. First we have to install it along with its extension, since it doesn't come with *create-react-app*. 563 | 564 | {title="Command Line",lang="text"} 565 | ~~~~~~~~ 566 | npm install --save-dev enzyme react-addons-test-utils enzyme-adapter-react-16 567 | ~~~~~~~~ 568 | 569 | Second, we include it in the test setup and we initialize its adapter. 570 | 571 | {title="src/App.test.js",lang="javascript"} 572 | ~~~~~~~~ 573 | import React from 'react'; 574 | import ReactDOM from 'react-dom'; 575 | import renderer from 'react-test-renderer'; 576 | # leanpub-start-insert 577 | import Enzyme from 'enzyme'; 578 | import Adapter from 'enzyme-adapter-react-16'; 579 | # leanpub-end-insert 580 | import App, { Search, Button, Table } from './App'; 581 | 582 | # leanpub-start-insert 583 | Enzyme.configure({ adapter: new Adapter() }); 584 | # leanpub-end-insert 585 | ~~~~~~~~ 586 | 587 | Now you can write your first unit test in the Table "describe"-block. You will use `shallow()` to render your component and assert that the Table was passed two items. The assertion simply checks if the element has two elements with the class `table-row`. 588 | 589 | {title="src/App.test.js",lang="javascript"} 590 | ~~~~~~~~ 591 | import React from 'react'; 592 | import ReactDOM from 'react-dom'; 593 | import renderer from 'react-test-renderer'; 594 | # leanpub-start-insert 595 | import Enzyme, { shallow } from 'enzyme'; 596 | # leanpub-end-insert 597 | import Adapter from 'enzyme-adapter-react-16'; 598 | import App, { Search, Button, Table } from './App'; 599 | 600 | ... 601 | 602 | describe('Table', () => { 603 | 604 | const props = { 605 | list: [ 606 | { title: '1', author: '1', num_comments: 1, points: 2, objectID: 'y' }, 607 | { title: '2', author: '2', num_comments: 1, points: 2, objectID: 'z' }, 608 | ], 609 | }; 610 | 611 | ... 612 | 613 | # leanpub-start-insert 614 | it('shows two items in list', () => { 615 | const element = shallow( 616 |
617 | ); 618 | 619 | expect(element.find('.table-row').length).toBe(2); 620 | }); 621 | # leanpub-end-insert 622 | 623 | }); 624 | ~~~~~~~~ 625 | 626 | Shallow renders the component without its child components, so you can dedicate the test to one component. 627 | 628 | Enzyme has three rendering mechanisms in its API. You already know `shallow()`, but there is also `mount()` and `render()`. Both instantiate instances of the parent component and all child components. `mount()` gives you access to the component lifecycle methods. These are the rules of them as to when to use each mechanism: 629 | 630 | * Always begin with a shallow test 631 | * If `componentDidMount()` or `componentDidUpdate()` should be tested, use `mount()` 632 | * If you want to test component lifecycle and children behavior, use `mount()` 633 | * If you want to test a component's children rendering with less overhead than `mount()` and you are not interested in lifecycle methods, use `render()` 634 | 635 | Continue to unit test your components, but be sure to keep the tests simple and maintainable. Otherwise you will have to refactor them once you change your components. This is one of the main reasons Facebook introduced snapshot tests with Jest. 636 | 637 | ### Exercises: 638 | 639 | * Confirm your [source code for the last section](http://bit.ly/2HaT2Gl) 640 | * Confirm the [changes from the last section](http://bit.ly/2H842oa) 641 | * Write a unit test with Enzyme for your Button component 642 | * Keep your unit tests up to date during the following chapters 643 | * Read about [Enzyme and its rendering API](https://github.com/airbnb/enzyme) 644 | * Read about [testing React applications](https://www.robinwieruch.de/react-testing-tutorial) 645 | 646 | ## Component Interface with PropTypes 647 | 648 | [TypeScript](https://www.typescriptlang.org/) and [Flow](https://flowtype.org/) are used to introduce a type system to JavaScript. A typed language is less error prone because the code gets validated based on its program text. Editors and other utilities can catch these errors before the program runs. 649 | 650 | In the book, you will not introduce Flow or TypeScript, but another useful way to check your types in components: React comes with a built-in type checker to prevent bugs. You can use PropTypes to describe your component interface. All the props passed from a parent component to a child get validated based on the PropTypes interface assigned to the child. 651 | 652 | This section will show you to make components type safe with PropTypes. I will omit the changes for the following chapters, since they add unnecessary code refactorings, but you should update them along the way to keep your components interface type safe. 653 | 654 | First, install a separate package for React: 655 | 656 | {title="Command Line",lang="text"} 657 | ~~~~~~~~ 658 | npm install prop-types 659 | ~~~~~~~~ 660 | 661 | Now, you can import the PropTypes. 662 | 663 | {title="src/App.js",lang="javascript"} 664 | ~~~~~~~~ 665 | import React, { Component } from 'react'; 666 | import axios from 'axios'; 667 | # leanpub-start-insert 668 | import PropTypes from 'prop-types'; 669 | # leanpub-end-insert 670 | ~~~~~~~~ 671 | 672 | Let's start to assign a props interface to the components: 673 | 674 | {title="src/App.js",lang="javascript"} 675 | ~~~~~~~~ 676 | const Button = ({ 677 | onClick, 678 | className = '', 679 | children, 680 | }) => 681 | 688 | 689 | # leanpub-start-insert 690 | Button.propTypes = { 691 | onClick: PropTypes.func, 692 | className: PropTypes.string, 693 | children: PropTypes.node, 694 | }; 695 | # leanpub-end-insert 696 | ~~~~~~~~ 697 | 698 | Essentially, we want to take every argument from the function signature and assign a PropType to it. The basic PropTypes for primitives and complex objects are: 699 | 700 | * PropTypes.array 701 | * PropTypes.bool 702 | * PropTypes.func 703 | * PropTypes.number 704 | * PropTypes.object 705 | * PropTypes.string 706 | 707 | There are two more PropTypes that can define a renderable fragment (node), e.g. a string, and a React element: 708 | 709 | * PropTypes.node 710 | * PropTypes.element 711 | 712 | You already used the `node` PropType for the Button component. There are more PropType definitions in the official React documentation. 713 | 714 | At the moment, all the defined PropTypes for the Button are optional. The parameters can be `null` or `undefined`. But for several props you want to enforce that they be defined. To do this, we make it a requirement that these props are passed to the component. 715 | 716 | {title="src/App.js",lang="javascript"} 717 | ~~~~~~~~ 718 | Button.propTypes = { 719 | # leanpub-start-insert 720 | onClick: PropTypes.func.isRequired, 721 | # leanpub-end-insert 722 | className: PropTypes.string, 723 | # leanpub-start-insert 724 | children: PropTypes.node.isRequired, 725 | # leanpub-end-insert 726 | }; 727 | ~~~~~~~~ 728 | 729 | The `className` is not required, because it can default to an empty string. Next you will define a PropType interface for the Table component: 730 | 731 | {title="src/App.js",lang="javascript"} 732 | ~~~~~~~~ 733 | # leanpub-start-insert 734 | Table.propTypes = { 735 | list: PropTypes.array.isRequired, 736 | onDismiss: PropTypes.func.isRequired, 737 | }; 738 | # leanpub-end-insert 739 | ~~~~~~~~ 740 | 741 | You can define the content of an array PropType more explicitly: 742 | 743 | {title="src/App.js",lang="javascript"} 744 | ~~~~~~~~ 745 | Table.propTypes = { 746 | list: PropTypes.arrayOf( 747 | PropTypes.shape({ 748 | objectID: PropTypes.string.isRequired, 749 | author: PropTypes.string, 750 | url: PropTypes.string, 751 | num_comments: PropTypes.number, 752 | points: PropTypes.number, 753 | }) 754 | ).isRequired, 755 | onDismiss: PropTypes.func.isRequired, 756 | }; 757 | ~~~~~~~~ 758 | 759 | Only the `objectID` is required, because some of the code depends on it. The other properties are only displayed, so they are not required. Moreover you cannot be sure the Hacker News API always has a defined property for each object in the array. 760 | 761 | You can also define default props in your component. The `className` property has an ES6 default parameter in the component signature: 762 | 763 | {title="src/App.js",lang="javascript"} 764 | ~~~~~~~~ 765 | const Button = ({ 766 | onClick, 767 | className = '', 768 | children 769 | }) => 770 | ... 771 | ~~~~~~~~ 772 | 773 | Replace it with the internal React default prop: 774 | 775 | {title="src/App.js",lang="javascript"} 776 | ~~~~~~~~ 777 | # leanpub-start-insert 778 | const Button = ({ 779 | onClick, 780 | className, 781 | children 782 | }) => 783 | # leanpub-end-insert 784 | 791 | 792 | # leanpub-start-insert 793 | Button.defaultProps = { 794 | className: '', 795 | }; 796 | # leanpub-end-insert 797 | ~~~~~~~~ 798 | 799 | Like the ES6 default parameter, the default prop ensures the property is set to a default value when the parent component doesn't specify it. The PropType type check happens after the default prop is evaluated. 800 | 801 | If you run your tests again, you might see PropType errors for your components on your command line. It happens when we don't define all props for components in the tests that are required in the PropType definition. The tests themselves all pass correctly though. Make sure to pass all required props to the components in your tests to avoid these errors. 802 | 803 | ### Exercises: 804 | 805 | * Confirm your [source code for the last section](http://bit.ly/2H8ILul) 806 | * Confirm the [changes from the last section](http://bit.ly/2H843se) 807 | * Note that the changes get reverted for the next section 808 | * Define the PropType interface for the Search component 809 | * Add and update the PropType interfaces when you add and update components in the next chapters 810 | * Read about [React PropTypes](https://reactjs.org/docs/typechecking-with-proptypes.html) 811 | 812 | ## Debugging with React Developer Tools 813 | 814 | [React Developer Tools](https://github.com/facebook/react-devtools) lets you inspect the React components hierarchy, props and state. It comes as a browser extension for Chrome and Firefox and as a standalone app that works with other environments. Once installed, the extension icon will light up on websites using React. On such pages, you will see a tab called "React" in your browser's developer tools. 815 | 816 | Let's try it on our Hacker News application. On most browsers, a quick way to bring the *dev tools* up is to right-click on the page and than hit "Inspect". Do it when your application is loaded, then click on the "React" tab. You should see its elements hierarchy, being `` the root element. If you expand it, you will find instances of your ``, `
` and ` 40 | 41 | # leanpub-start-insert 42 | ); 43 | } 44 | } 45 | # leanpub-end-insert 46 | ~~~~~~~~ 47 | 48 | The `this` object of an ES6 class component helps us to reference the DOM element with the `ref` attribute. 49 | 50 | {title="src/App.js",lang="javascript"} 51 | ~~~~~~~~ 52 | class Search extends Component { 53 | render() { 54 | const { 55 | value, 56 | onChange, 57 | onSubmit, 58 | children 59 | } = this.props; 60 | 61 | return ( 62 | 63 | this.input = el} 69 | # leanpub-end-insert 70 | /> 71 | 74 | 75 | ); 76 | } 77 | } 78 | ~~~~~~~~ 79 | 80 | Now you can focus the input field when the component mounted by using the `this` object, the appropriate lifecycle method, and the DOM API. 81 | 82 | {title="src/App.js",lang="javascript"} 83 | ~~~~~~~~ 84 | class Search extends Component { 85 | # leanpub-start-insert 86 | componentDidMount() { 87 | if (this.input) { 88 | this.input.focus(); 89 | } 90 | } 91 | # leanpub-end-insert 92 | 93 | render() { 94 | const { 95 | value, 96 | onChange, 97 | onSubmit, 98 | children 99 | } = this.props; 100 | 101 | return ( 102 | 103 | this.input = el} 108 | /> 109 | 112 | 113 | ); 114 | } 115 | } 116 | ~~~~~~~~ 117 | 118 | The input field should be focused when the application renders. We access to `ref` in a functional stateless component without the `this` object using the following functional stateless component: 119 | 120 | {title="src/App.js",lang="javascript"} 121 | ~~~~~~~~ 122 | const Search = ({ 123 | value, 124 | onChange, 125 | onSubmit, 126 | children 127 | }) => { 128 | # leanpub-start-insert 129 | let input; 130 | # leanpub-end-insert 131 | return ( 132 | 133 | this.input = el} 139 | # leanpub-end-insert 140 | /> 141 | 144 | 145 | ); 146 | } 147 | ~~~~~~~~ 148 | 149 | Now we can access the input DOM element. In our case it wouldn't help much since there's no lifecycle method in a functional stateless component to trigger the focus. So we are not going to make use of the input variable here. In the future, though, you may encounter cases where it makes sense to use a functional stateless component with the `ref` attribute. 150 | 151 | ### Exercises 152 | 153 | * Confirm your [source code for the last section](http://bit.ly/2H8597l) 154 | * Confirm the [changes from the last section](http://bit.ly/2H8EoQ4) 155 | * Note that the changes get reverted for the next section 156 | * Read about [the usage of the ref attribute in React](https://www.robinwieruch.de/react-ref-attribute-dom-node/) 157 | * Read about [the ref attribute in general in React](https://reactjs.org/docs/refs-and-the-dom.html) 158 | 159 | ## Loading ... 160 | 161 | Now we get back to the application, where we'll show a loading indicator when a search request submits to the Hacker News API. The request is asynchronous, so you should show your user feedback that something is happening. Let's define a reusable Loading component in your *src/App.js* file. 162 | 163 | {title="src/App.js",lang="javascript"} 164 | ~~~~~~~~ 165 | const Loading = () => 166 |
Loading ...
167 | ~~~~~~~~ 168 | 169 | Now we need a property to store the loading state. Based on the loading state, we can decide to show the Loading component later. 170 | 171 | {title="src/App.js",lang="javascript"} 172 | ~~~~~~~~ 173 | class App extends Component { 174 | constructor(props) { 175 | super(props); 176 | 177 | this.state = { 178 | results: null, 179 | searchKey: '', 180 | searchTerm: DEFAULT_QUERY, 181 | error: null, 182 | # leanpub-start-insert 183 | isLoading: false, 184 | # leanpub-end-insert 185 | }; 186 | 187 | ... 188 | } 189 | 190 | ... 191 | 192 | } 193 | ~~~~~~~~ 194 | 195 | The initial value of that `isLoading` property is false. We don't load anything before the App component is mounted. When the request is made, the loading state is set to true. The request will succeed eventually, and you can set the loading state to false. 196 | 197 | {title="src/App.js",lang="javascript"} 198 | ~~~~~~~~ 199 | class App extends Component { 200 | 201 | ... 202 | 203 | setSearchTopStories(result) { 204 | ... 205 | 206 | this.setState({ 207 | results: { 208 | ...results, 209 | [searchKey]: { hits: updatedHits, page } 210 | }, 211 | # leanpub-start-insert 212 | isLoading: false 213 | # leanpub-end-insert 214 | }); 215 | } 216 | 217 | fetchSearchTopStories(searchTerm, page = 0) { 218 | # leanpub-start-insert 219 | this.setState({ isLoading: true }); 220 | # leanpub-end-insert 221 | 222 | axios(`${PATH_BASE}${PATH_SEARCH}?${PARAM_SEARCH}${searchTerm}&${PARAM_PAGE}${page}&${PARAM_HPP}${DEFAULT_HPP}`) 223 | .then(result => this.setSearchTopStories(result.data)) 224 | .catch(error => this.setState({ error })); 225 | } 226 | 227 | ... 228 | 229 | } 230 | ~~~~~~~~ 231 | 232 | In the last step, we used the Loading component in the App component. A conditional rendering based on the loading state will decide whether to show a Loading component or the Button component. The latter is the button to fetch more data. 233 | 234 | {title="src/App.js",lang="javascript"} 235 | ~~~~~~~~ 236 | class App extends Component { 237 | 238 | ... 239 | 240 | render() { 241 | const { 242 | searchTerm, 243 | results, 244 | searchKey, 245 | error, 246 | # leanpub-start-insert 247 | isLoading 248 | # leanpub-end-insert 249 | } = this.state; 250 | 251 | ... 252 | 253 | return ( 254 |
255 | ... 256 |
257 | # leanpub-start-insert 258 | { isLoading 259 | ? 260 | : 265 | } 266 | # leanpub-end-insert 267 |
268 |
269 | ); 270 | } 271 | } 272 | ~~~~~~~~ 273 | 274 | Initially, the Loading component will show when you start your application, because you make a request on `componentDidMount()`. There is no Table component, because the list is empty. When the response returns from the Hacker News API, the result is shown, the loading state is set to false, and the Loading component disappears. Instead, the "More" button to fetch more data appears. Once you fetch more data, the button will disappear again and the Loading component will appear. 275 | 276 | ### Exercises: 277 | 278 | * Confirm your [source code for the last section](http://bit.ly/2HkQk0k) 279 | * Confirm the [changes from the last section](http://bit.ly/2Hns4uz) 280 | * Use a library such as [Font Awesome](https://fontawesome.com/) to show a loading icon instead of the "Loading ..." text 281 | 282 | ## Higher-Order Components 283 | 284 | Higher-order components (HOC) are an advanced concept in React. HOCs are an equivalent to higher-order functions. They take any input, usually a component, but also optional arguments, and return a component as output. The returned component is an enhanced version of the input component, and it can be used in your JSX. 285 | 286 | HOCs are used for different use cases. They can prepare properties, manage state, or alter the representation of a component. One case is to use a HOC as a helper for a conditional rendering. Imagine you have a List component that renders a list of items or nothing, because the list is empty or null. The HOC could shield away that the list would render nothing when there is no list. On the other hand, the plain List component doesn't need to bother anymore about a non existent list, as it only cares about rendering the list. 287 | 288 | Let's do a simple HOC that takes a component as input and returns a component. You can place it in your *src/App.js* file. 289 | 290 | {title="src/App.js",lang="javascript"} 291 | ~~~~~~~~ 292 | function withFeature(Component) { 293 | return function(props) { 294 | return ; 295 | } 296 | } 297 | ~~~~~~~~ 298 | 299 | It is a useful convention to prefix a HOC with `with`. Since you are using JavaScript ES6, you can express the HOC better with an ES6 arrow function. 300 | 301 | {title="src/App.js",lang="javascript"} 302 | ~~~~~~~~ 303 | const withEnhancement = (Component) => (props) => 304 | 305 | ~~~~~~~~ 306 | 307 | In our example, the input component stays the same as the output, so nothing happens. The output component should show the Loading component when the loading state is true, otherwise it should show the input component. A conditional rendering is a great use case for an HOC. 308 | 309 | {title="src/App.js",lang="javascript"} 310 | ~~~~~~~~ 311 | # leanpub-start-insert 312 | const withLoading = (Component) => (props) => 313 | props.isLoading 314 | ? 315 | : 316 | # leanpub-end-insert 317 | ~~~~~~~~ 318 | 319 | Based on the loading property, you can apply a conditional rendering. The function will return the Loading component or the input component. In general, it can be very efficient to spread an object like the props object in the previous example as input for a component. See the difference in the following code snippet: 320 | 321 | {title="Code Playground",lang="javascript"} 322 | ~~~~~~~~ 323 | // before you would have had to destructure the props before passing them 324 | const { firstname, lastname } = props; 325 | 326 | 327 | // but you can use the object spread operator to pass all object properties 328 | 329 | ~~~~~~~~ 330 | 331 | We passed all the props including the `isLoading` property by spreading the object into the input component. The input component may not care about the `isLoading` property. You can use the ES6 rest destructuring to avoid it: 332 | 333 | {title="src/App.js",lang="javascript"} 334 | ~~~~~~~~ 335 | # leanpub-start-insert 336 | const withLoading = (Component) => ({ isLoading, ...rest }) => 337 | isLoading 338 | ? 339 | : 340 | # leanpub-end-insert 341 | ~~~~~~~~ 342 | 343 | It takes one property out of the object, but keeps the remaining object, which also works with multiple properties. More can be read about it in Mozilla's [destructuring assignment](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment). 344 | 345 | Now you can use the HOC in JSX. Maybe you want to show either the "More" button or the Loading component. The Loading component is already encapsulated in the HOC, but an input component is missing. For showing either a Button component or a Loading component, the Button is the input component of the HOC. The enhanced output component is a ButtonWithLoading component. 346 | 347 | {title="src/App.js",lang="javascript"} 348 | ~~~~~~~~ 349 | const Button = ({ 350 | onClick, 351 | className = '', 352 | children, 353 | }) => 354 | 361 | 362 | const Loading = () => 363 |
Loading ...
364 | 365 | const withLoading = (Component) => ({ isLoading, ...rest }) => 366 | isLoading 367 | ? 368 | : 369 | 370 | # leanpub-start-insert 371 | const ButtonWithLoading = withLoading(Button); 372 | # leanpub-end-insert 373 | ~~~~~~~~ 374 | 375 | Everything is defined now. As a last step, you have to use the ButtonWithLoading component, which receives the loading state as an additional property. While the HOC consumes the loading property, all other props get passed to the Button component. 376 | 377 | {title="src/App.js",lang="javascript"} 378 | ~~~~~~~~ 379 | class App extends Component { 380 | 381 | ... 382 | 383 | render() { 384 | ... 385 | return ( 386 |
387 | ... 388 |
389 | # leanpub-start-insert 390 | this.fetchSearchTopStories(searchKey, page + 1)} 393 | > 394 | More 395 | 396 | # leanpub-end-insert 397 |
398 |
399 | ); 400 | } 401 | } 402 | ~~~~~~~~ 403 | 404 | Note that when you run your tests again, your snapshot test for the App component fails. The diff might look like the following on the command line: 405 | 406 | {title="Command Line",lang="text"} 407 | ~~~~~~~~ 408 | - 415 | +
416 | + Loading ... 417 | +
418 | ~~~~~~~~ 419 | 420 | You can either fix the component now, when you think there is something wrong about it, or can accept the new snapshot of it. Because you introduced the Loading component in this chapter, you can accept the altered snapshot test on the command line in the interactive test. 421 | 422 | Higher-order components are an advanced pattern in React. They have multiple purposes: improved reusability of components, greater abstraction, composability of components, and manipulations of props, state and view. I encourage you to read [gentle introduction to higher-order components](https://www.robinwieruch.de/gentle-introduction-higher-order-components/). It gives you another approach to learn them, shows you an elegant way to use them in a functional programming way, and solves the problem of conditional rendering with higher-order components. 423 | 424 | ### Exercises: 425 | 426 | * Confirm your [source code for the last section](http://bit.ly/2HkoP7c) 427 | * Confirm the [changes from the last section](http://bit.ly/2Ho9uT9) 428 | * Read [a gentle introduction to higher-order components](https://www.robinwieruch.de/gentle-introduction-higher-order-components/) 429 | * Experiment with the HOC you have created 430 | * Think about a use case where another HOC would make sense 431 | * Implement the HOC, if there is a use case 432 | 433 | ## Advanced Sorting 434 | 435 | We implemented a client and server-side search interaction earlier. Since you have a Table component, it makes sense to enhance it with advanced interactions. Next, we'll introduce a sort functionality for each column by using the column headers of the Table. 436 | 437 | It is possible to write your own sort function, but I prefer to use a utility library like [Lodash](https://lodash.com/) for these cases. There are other options, but we'll install Lodash for our sort function: 438 | 439 | {title="Command Line",lang="text"} 440 | ~~~~~~~~ 441 | npm install lodash 442 | ~~~~~~~~ 443 | 444 | Now we import the sort functionality of Lodash in your *src/App.js* file: 445 | 446 | {title="src/App.js",lang="javascript"} 447 | ~~~~~~~~ 448 | import React, { Component } from 'react'; 449 | import axios from 'axios'; 450 | # leanpub-start-insert 451 | import { sortBy } from 'lodash'; 452 | # leanpub-end-insert 453 | import './App.css'; 454 | ~~~~~~~~ 455 | 456 | Now we have several columns in Table: title, author, comments and points columns. You can define sort functions where each takes a list and returns a list of items sorted by a specific property. Additionally, you will need a default sort function that doesn't sort, but returns the unsorted list. This will be the initial state. 457 | 458 | {title="src/App.js",lang="javascript"} 459 | ~~~~~~~~ 460 | ... 461 | 462 | # leanpub-start-insert 463 | const SORTS = { 464 | NONE: list => list, 465 | TITLE: list => sortBy(list, 'title'), 466 | AUTHOR: list => sortBy(list, 'author'), 467 | COMMENTS: list => sortBy(list, 'num_comments').reverse(), 468 | POINTS: list => sortBy(list, 'points').reverse(), 469 | }; 470 | # leanpub-end-insert 471 | 472 | class App extends Component { 473 | ... 474 | } 475 | ... 476 | ~~~~~~~~ 477 | 478 | Two of the sort functions return a reversed list. That's to see the items with the highest comments and points, rather than the items with the lowest counts when the list is sorted for the first time. 479 | 480 | The `SORTS` object allows you to reference any sort function now. 481 | 482 | Again, the App component is responsible for storing the state of the sort. The initial state will be the default sort function, which doesn't sort at all and returns the input list as output. 483 | 484 | {title="src/App.js",lang="javascript"} 485 | ~~~~~~~~ 486 | this.state = { 487 | results: null, 488 | searchKey: '', 489 | searchTerm: DEFAULT_QUERY, 490 | error: null, 491 | isLoading: false, 492 | # leanpub-start-insert 493 | sortKey: 'NONE', 494 | # leanpub-end-insert 495 | }; 496 | ~~~~~~~~ 497 | 498 | Once we choose a different `sortKey`, like the `AUTHOR` key, we sort the list with the appropriate sort function from the `SORTS` object. 499 | 500 | Now we define a new class method in App component that sets a `sortKey` to the local component state, then `sortKey` can be used to retrieve the sorting function to apply it to the list: 501 | 502 | {title="src/App.js",lang="javascript"} 503 | ~~~~~~~~ 504 | class App extends Component { 505 | constructor(props) { 506 | 507 | ... 508 | 509 | this.needsToSearchTopStories = this.needsToSearchTopStories.bind(this); 510 | this.setSearchTopStories = this.setSearchTopStories.bind(this); 511 | this.fetchSearchTopStories = this.fetchSearchTopStories.bind(this); 512 | this.onSearchSubmit = this.onSearchSubmit.bind(this); 513 | this.onSearchChange = this.onSearchChange.bind(this); 514 | this.onDismiss = this.onDismiss.bind(this); 515 | # leanpub-start-insert 516 | this.onSort = this.onSort.bind(this); 517 | # leanpub-end-insert 518 | } 519 | 520 | ... 521 | 522 | # leanpub-start-insert 523 | onSort(sortKey) { 524 | this.setState({ sortKey }); 525 | } 526 | # leanpub-end-insert 527 | 528 | ... 529 | 530 | } 531 | ~~~~~~~~ 532 | 533 | The next step is to pass the method and `sortKey` to the Table component. 534 | 535 | {title="src/App.js",lang="javascript"} 536 | ~~~~~~~~ 537 | class App extends Component { 538 | 539 | ... 540 | 541 | render() { 542 | const { 543 | searchTerm, 544 | results, 545 | searchKey, 546 | error, 547 | isLoading, 548 | # leanpub-start-insert 549 | sortKey 550 | # leanpub-end-insert 551 | } = this.state; 552 | 553 | ... 554 | 555 | return ( 556 |
557 | ... 558 |
566 | ... 567 | 568 | ); 569 | } 570 | } 571 | ~~~~~~~~ 572 | 573 | The Table component is responsible for sorting the list. It takes one of the `SORT` functions by `sortKey` and passes the list as input, after which it keeps mapping over the sorted list. 574 | 575 | {title="src/App.js",lang="javascript"} 576 | ~~~~~~~~ 577 | # leanpub-start-insert 578 | const Table = ({ 579 | list, 580 | sortKey, 581 | onSort, 582 | onDismiss 583 | }) => 584 | # leanpub-end-insert 585 |
586 | # leanpub-start-insert 587 | {SORTS[sortKey](list).map(item => 588 | # leanpub-end-insert 589 |
590 | ... 591 |
592 | )} 593 |
594 | ~~~~~~~~ 595 | 596 | In theory, the list should get sorted by one of the functions. But the default sort is set to `NONE`, so nothing is sorted yet, as nothing executes the `onSort()` method to change the `sortKey`. We extend the Table with a row of column headers that use Sort components in columns to sort each column: 597 | 598 | {title="src/App.js",lang="javascript"} 599 | ~~~~~~~~ 600 | const Table = ({ 601 | list, 602 | sortKey, 603 | onSort, 604 | onDismiss 605 | }) => 606 |
607 | # leanpub-start-insert 608 |
609 | 610 | 614 | Title 615 | 616 | 617 | 618 | 622 | Author 623 | 624 | 625 | 626 | 630 | Comments 631 | 632 | 633 | 634 | 638 | Points 639 | 640 | 641 | 642 | Archive 643 | 644 |
645 | # leanpub-end-insert 646 | {SORTS[sortKey](list).map(item => 647 | ... 648 | )} 649 |
650 | ~~~~~~~~ 651 | 652 | Each Sort component gets a specific `sortKey` and the general `onSort()` function. Internally, it calls the method with the `sortKey` to set the specific key. 653 | 654 | {title="src/App.js",lang="javascript"} 655 | ~~~~~~~~ 656 | const Sort = ({ sortKey, onSort, children }) => 657 | 660 | ~~~~~~~~ 661 | 662 | As you can see, the Sort component reuses your common Button component. On a button click, each individual passed `sortKey` is set by the `onSort()` method, so the list is sorted when column headers are selected. 663 | 664 | Now we'll improve the look of the button in the column header. Let's give it a proper `className`: 665 | 666 | {title="src/App.js",lang="javascript"} 667 | ~~~~~~~~ 668 | const Sort = ({ sortKey, onSort, children }) => 669 | # leanpub-start-insert 670 | 677 | ~~~~~~~~ 678 | 679 | This was done to improve the UI. The next goal is to implement a reverse sort. The list should perform a reverse sort once you click a Sort component twice. First, you need to define the reverse state with a boolean. The sort can be either reversed or non-reversed. 680 | 681 | {title="src/App.js",lang="javascript"} 682 | ~~~~~~~~ 683 | this.state = { 684 | results: null, 685 | searchKey: '', 686 | searchTerm: DEFAULT_QUERY, 687 | error: null, 688 | isLoading: false, 689 | sortKey: 'NONE', 690 | # leanpub-start-insert 691 | isSortReverse: false, 692 | # leanpub-end-insert 693 | }; 694 | ~~~~~~~~ 695 | 696 | Now in your sort method, you can evaluate if the list is reverse sorted. It is reverse if the `sortKey` in the state is the same as the incoming `sortKey` and the reverse state is not already set to true. 697 | 698 | {title="src/App.js",lang="javascript"} 699 | ~~~~~~~~ 700 | onSort(sortKey) { 701 | # leanpub-start-insert 702 | const isSortReverse = this.state.sortKey === sortKey && !this.state.isSortReverse; 703 | this.setState({ sortKey, isSortReverse }); 704 | # leanpub-end-insert 705 | } 706 | ~~~~~~~~ 707 | 708 | Again, we pass the reverse prop to your Table component: 709 | 710 | {title="src/App.js",lang="javascript"} 711 | ~~~~~~~~ 712 | class App extends Component { 713 | 714 | ... 715 | 716 | render() { 717 | const { 718 | searchTerm, 719 | results, 720 | searchKey, 721 | error, 722 | isLoading, 723 | sortKey, 724 | # leanpub-start-insert 725 | isSortReverse 726 | # leanpub-end-insert 727 | } = this.state; 728 | 729 | ... 730 | 731 | return ( 732 |
733 | ... 734 |
743 | ... 744 | 745 | ); 746 | } 747 | } 748 | ~~~~~~~~ 749 | 750 | The Table has to have an arrow function block body to compute the data now: 751 | 752 | {title="src/App.js",lang="javascript"} 753 | ~~~~~~~~ 754 | # leanpub-start-insert 755 | const Table = ({ 756 | list, 757 | sortKey, 758 | isSortReverse, 759 | onSort, 760 | onDismiss 761 | }) => { 762 | const sortedList = SORTS[sortKey](list); 763 | const reverseSortedList = isSortReverse 764 | ? sortedList.reverse() 765 | : sortedList; 766 | 767 | return( 768 | # leanpub-end-insert 769 |
770 |
771 | ... 772 |
773 | # leanpub-start-insert 774 | {reverseSortedList.map(item => 775 | # leanpub-end-insert 776 | ... 777 | )} 778 |
779 | # leanpub-start-insert 780 | ); 781 | } 782 | # leanpub-end-insert 783 | ~~~~~~~~ 784 | 785 | Finally, we want to give the user visual feedback to distinguish which column is actively sorted. Each Sort component has its specific `sortKey` already, which can be used to identify the activated sort. We pass the `sortKey` from the internal component state as active sort key to your Sort component: 786 | 787 | {title="src/App.js",lang="javascript"} 788 | ~~~~~~~~ 789 | const Table = ({ 790 | list, 791 | sortKey, 792 | isSortReverse, 793 | onSort, 794 | onDismiss 795 | }) => { 796 | const sortedList = SORTS[sortKey](list); 797 | const reverseSortedList = isSortReverse 798 | ? sortedList.reverse() 799 | : sortedList; 800 | 801 | return( 802 |
803 |
804 | 805 | 812 | Title 813 | 814 | 815 | 816 | 823 | Author 824 | 825 | 826 | 827 | 834 | Comments 835 | 836 | 837 | 838 | 845 | Points 846 | 847 | 848 | 849 | Archive 850 | 851 |
852 | {reverseSortedList.map(item => 853 | ... 854 | )} 855 |
856 | ); 857 | } 858 | ~~~~~~~~ 859 | 860 | Now the user will know whether sort is active based on the `sortKey` and `activeSortKey` . Give your Sort component an extra `className` attribute, in case it is sorted, to give visual feedback: 861 | 862 | {title="src/App.js",lang="javascript"} 863 | ~~~~~~~~ 864 | # leanpub-start-insert 865 | const Sort = ({ 866 | sortKey, 867 | activeSortKey, 868 | onSort, 869 | children 870 | }) => { 871 | const sortClass = ['button-inline']; 872 | 873 | if (sortKey === activeSortKey) { 874 | sortClass.push('button-active'); 875 | } 876 | 877 | return ( 878 | 884 | ); 885 | } 886 | # leanpub-end-insert 887 | ~~~~~~~~ 888 | 889 | We can define `sortClass` more efficiently using a library called classnames, which is installed using npm: 890 | 891 | {title="Command Line",lang="text"} 892 | ~~~~~~~~ 893 | npm install classnames 894 | ~~~~~~~~ 895 | 896 | After installation, we import it on top of the *src/App.js* file. 897 | 898 | {title="src/App.js",lang="javascript"} 899 | ~~~~~~~~ 900 | import React, { Component } from 'react'; 901 | import axios from 'axios'; 902 | import { sortBy } from 'lodash'; 903 | # leanpub-start-insert 904 | import classNames from 'classnames'; 905 | # leanpub-end-insert 906 | import './App.css'; 907 | ~~~~~~~~ 908 | 909 | Now we can define `className` with conditional classes: 910 | 911 | {title="src/App.js",lang="javascript"} 912 | ~~~~~~~~ 913 | const Sort = ({ 914 | sortKey, 915 | activeSortKey, 916 | onSort, 917 | children 918 | }) => { 919 | # leanpub-start-insert 920 | const sortClass = classNames( 921 | 'button-inline', 922 | { 'button-active': sortKey === activeSortKey } 923 | ); 924 | # leanpub-end-insert 925 | 926 | return ( 927 | # leanpub-start-insert 928 | 935 | ); 936 | } 937 | ~~~~~~~~ 938 | 939 | There will be failing snapshot and unit tests for the Table component. Since we intentionally changed again our component representations, we accept the snapshot tests, but we still need to fix the unit test. In *src/App.test.js* , provide a `sortKey` and the `isSortReverse` boolean for the Table component. 940 | 941 | {title="src/App.test.js",lang="javascript"} 942 | ~~~~~~~~ 943 | ... 944 | 945 | describe('Table', () => { 946 | 947 | const props = { 948 | list: [ 949 | { title: '1', author: '1', num_comments: 1, points: 2, objectID: 'y' }, 950 | { title: '2', author: '2', num_comments: 1, points: 2, objectID: 'z' }, 951 | ], 952 | # leanpub-start-insert 953 | sortKey: 'TITLE', 954 | isSortReverse: false, 955 | # leanpub-end-insert 956 | }; 957 | 958 | ... 959 | 960 | }); 961 | ~~~~~~~~ 962 | 963 | We may need to accept the failing snapshot tests again for the Table component, because we provided extended props for it. The advanced sort interaction is finally complete. 964 | 965 | ### Exercises: 966 | 967 | * Confirm your [source code for the last section](http://bit.ly/2HteRAe) 968 | * Confirm the [changes from the last section](http://bit.ly/2HnYFAh) 969 | * Use a library like [Font Awesome](https://fontawesome.com/) to indicate the (reverse) sort. It could be an arrow up or arrow down icon next to each Sort header 970 | * Read about the [classnames library](https://github.com/JedWatson/classnames) 971 | 972 | {pagebreak} 973 | 974 | You have learned advanced component techniques in React! Let's recap the chapter: 975 | 976 | * **React** 977 | * The `ref` attribute to reference DOM elements 978 | * Higher-order components are a common way to build advanced components 979 | * Implementation of advanced interactions in React 980 | * Conditional classNames with a neat helper library 981 | * **ES6** 982 | * Rest destructuring to split up objects and arrays 983 | 984 | You can always find the source code in the [official repository](http://bit.ly/2HteRAe). 985 | -------------------------------------------------------------------------------- /manuscript/chapter6.md: -------------------------------------------------------------------------------- 1 | # State Management in React 2 | 3 | We have already covered the basics of state management in React in the previous chapters by using React's local state, so this chapter will dig a bit deeper. It will expand on the best practices, how to apply them, and why you could consider using a third-party state management library. 4 | 5 | ## Lifting State 6 | 7 | Only the App component is a stateful ES6 component in your application. It handles a lot of application state and logic in its class methods. Moreover, we pass a lot of properties to the Table component, most of which are only used in there. It's not important that the App component knows about them, so the sort functionality could be moved into the Table component. 8 | 9 | Moving substate from one component to another is known as *lifting state*. We want to move state that isn't used in the App component into the Table component, down from parent to child component. To deal with state and class methods in the Table component, it has to become an ES6 class component. The refactoring from functional stateless component to ES6 class component is straightforward. 10 | 11 | Your Table component as a functional stateless component: 12 | 13 | {title="src/App.js",lang="javascript"} 14 | ~~~~~~~~ 15 | const Table = ({ 16 | list, 17 | sortKey, 18 | isSortReverse, 19 | onSort, 20 | onDismiss 21 | }) => { 22 | const sortedList = SORTS[sortKey](list); 23 | const reverseSortedList = isSortReverse 24 | ? sortedList.reverse() 25 | : sortedList; 26 | 27 | return( 28 | ... 29 | ); 30 | } 31 | ~~~~~~~~ 32 | 33 | Your Table component as an ES6 class component: 34 | 35 | {title="src/App.js",lang="javascript"} 36 | ~~~~~~~~ 37 | # leanpub-start-insert 38 | class Table extends Component { 39 | render() { 40 | const { 41 | list, 42 | sortKey, 43 | isSortReverse, 44 | onSort, 45 | onDismiss 46 | } = this.props; 47 | 48 | const sortedList = SORTS[sortKey](list); 49 | const reverseSortedList = isSortReverse 50 | ? sortedList.reverse() 51 | : sortedList; 52 | 53 | return ( 54 | ... 55 | ); 56 | } 57 | } 58 | # leanpub-end-insert 59 | ~~~~~~~~ 60 | 61 | Since you want to deal with state and methods in your component, you have to add a constructor and initial state. 62 | 63 | {title="src/App.js",lang="javascript"} 64 | ~~~~~~~~ 65 | class Table extends Component { 66 | # leanpub-start-insert 67 | constructor(props) { 68 | super(props); 69 | 70 | this.state = {}; 71 | } 72 | # leanpub-end-insert 73 | 74 | render() { 75 | ... 76 | } 77 | } 78 | ~~~~~~~~ 79 | 80 | Now you can move state and class methods with the sort functionality from your App component down to your Table component. 81 | 82 | {title="src/App.js",lang="javascript"} 83 | ~~~~~~~~ 84 | class Table extends Component { 85 | constructor(props) { 86 | super(props); 87 | 88 | # leanpub-start-insert 89 | this.state = { 90 | sortKey: 'NONE', 91 | isSortReverse: false, 92 | }; 93 | 94 | this.onSort = this.onSort.bind(this); 95 | # leanpub-end-insert 96 | } 97 | 98 | # leanpub-start-insert 99 | onSort(sortKey) { 100 | const isSortReverse = this.state.sortKey === sortKey && 101 | !this.state.isSortReverse; 102 | 103 | this.setState({ sortKey, isSortReverse }); 104 | } 105 | # leanpub-end-insert 106 | 107 | render() { 108 | ... 109 | } 110 | } 111 | ~~~~~~~~ 112 | 113 | Remember to remove the moved state and `onSort()` class method from your App component. 114 | 115 | {title="src/App.js",lang="javascript"} 116 | ~~~~~~~~ 117 | class App extends Component { 118 | constructor(props) { 119 | super(props); 120 | 121 | this.state = { 122 | results: null, 123 | searchKey: '', 124 | searchTerm: DEFAULT_QUERY, 125 | error: null, 126 | isLoading: false, 127 | }; 128 | 129 | this.setSearchTopStories = this.setSearchTopStories.bind(this); 130 | this.fetchSearchTopStories = this.fetchSearchTopStories.bind(this); 131 | this.onDismiss = this.onDismiss.bind(this); 132 | this.onSearchSubmit = this.onSearchSubmit.bind(this); 133 | this.onSearchChange = this.onSearchChange.bind(this); 134 | this.needsToSearchTopStories = this.needsToSearchTopStories.bind(this); 135 | } 136 | 137 | ... 138 | 139 | } 140 | ~~~~~~~~ 141 | 142 | You can also make the Table component more lightweight. To do this, we move props that are passed to it from the App component, because they are handled internally in the Table component. 143 | 144 | {title="src/App.js",lang="javascript"} 145 | ~~~~~~~~ 146 | class App extends Component { 147 | 148 | ... 149 | 150 | render() { 151 | # leanpub-start-insert 152 | const { 153 | searchTerm, 154 | results, 155 | searchKey, 156 | error, 157 | isLoading 158 | } = this.state; 159 | # leanpub-end-insert 160 | 161 | ... 162 | 163 | return ( 164 |
165 | ... 166 | { error 167 | ?
168 |

Something went wrong.

169 |
170 | :
176 | } 177 | ... 178 | 179 | ); 180 | } 181 | } 182 | ~~~~~~~~ 183 | 184 | In the Table component, use the internal `onSort()` method and the internal Table state: 185 | 186 | {title="src/App.js",lang="javascript"} 187 | ~~~~~~~~ 188 | class Table extends Component { 189 | 190 | ... 191 | 192 | render() { 193 | # leanpub-start-insert 194 | const { 195 | list, 196 | onDismiss 197 | } = this.props; 198 | 199 | const { 200 | sortKey, 201 | isSortReverse, 202 | } = this.state; 203 | # leanpub-end-insert 204 | 205 | const sortedList = SORTS[sortKey](list); 206 | const reverseSortedList = isSortReverse 207 | ? sortedList.reverse() 208 | : sortedList; 209 | 210 | return( 211 |
212 |
213 | 214 | 221 | Title 222 | 223 | 224 | 225 | 232 | Author 233 | 234 | 235 | 236 | 243 | Comments 244 | 245 | 246 | 247 | 254 | Points 255 | 256 | 257 | 258 | Archive 259 | 260 |
261 | { reverseSortedList.map((item) => 262 | ... 263 | )} 264 |
265 | ); 266 | } 267 | } 268 | ~~~~~~~~ 269 | 270 | We made a crucial refactoring by moving functionality and state closer into another component, and other components got more lightweight. Again, the component API of the Table got lighter because it deals internally with the sort functionality. 271 | 272 | Lifting state can go the other way as well: from child to parent component. It is called as lifting state up. Imagine you were dealing with local state in a child component, and you want to fulfil a requirement to show the state in your parent component as well. You would have to lift up the state to your parent component. Moreover, imagine you want to show the state in a sibling component of your child component. Again, you would lift the state up to your parent component. The parent component deals with the local state, but exposes it to both child components. 273 | 274 | ### Exercises: 275 | 276 | * Confirm your [source code for the last section](http://bit.ly/2Ho9AtZ) 277 | * Confirm the [changes from the last section](http://bit.ly/2Hnuh9d) 278 | * Read about [lifting state in React](https://reactjs.org/docs/lifting-state-up.html) 279 | * Read about lifting state in [learn React before using Redux](https://www.robinwieruch.de/learn-react-before-using-redux/) 280 | 281 | ## Revisited: setState() 282 | 283 | So far, we have used React `setState()` to manage your internal component state. We can pass an object to the function where it updates partially the local state. 284 | 285 | {title="Code Playground",lang="javascript"} 286 | ~~~~~~~~ 287 | this.setState({ value: 'hello'}); 288 | ~~~~~~~~ 289 | 290 | But `setState()` doesn't take only an object. In its second version, you can pass a function to update the state. 291 | 292 | {title="Code Playground",lang="javascript"} 293 | ~~~~~~~~ 294 | this.setState((prevState, props) => { 295 | ... 296 | }); 297 | ~~~~~~~~ 298 | 299 | There is one crucial case where it makes sense to use a function over an object: when you update the state depending on the previous state or props. If you don't use a function, the local state management can cause bugs. The React `setState()` method is asynchronous. React batches `setState()` calls and executes them eventually. Sometimes, the previous state or props changes between before we can rely on it in our `setState()` call. 300 | 301 | {title="Code Playground",lang="javascript"} 302 | ~~~~~~~~ 303 | const { oneCount } = this.state; 304 | const { anotherCount } = this.props; 305 | this.setState({ count: oneCount + anotherCount }); 306 | ~~~~~~~~ 307 | 308 | Imagine that `oneCount` and `anotherCount`, thus the state or the props, change somewhere else asynchronously when you call `setState()`. In a growing application, you have more than one `setState()` call across your application. Since `setState()` executes asynchronously, you could rely in the example on stale values. 309 | 310 | With the function approach, the function in `setState()` is a callback that operates on the state and props at the time of executing the callback function. Even though `setState()` is asynchronous, with a function it takes the state and props at the time when it is executed. 311 | 312 | {title="Code Playground",lang="javascript"} 313 | ~~~~~~~~ 314 | this.setState((prevState, props) => { 315 | const { oneCount } = prevState; 316 | const { anotherCount } = props; 317 | return { count: oneCount + anotherCount }; 318 | }); 319 | ~~~~~~~~ 320 | 321 | In our code, the `setSearchTopStories()` method relies on the previous state, and this is a good example to use a function over an object in `setState()`. Right now, it looks like the following code: 322 | 323 | {title="src/App.js",lang="javascript"} 324 | ~~~~~~~~ 325 | setSearchTopStories(result) { 326 | const { hits, page } = result; 327 | const { searchKey, results } = this.state; 328 | 329 | const oldHits = results && results[searchKey] 330 | ? results[searchKey].hits 331 | : []; 332 | 333 | const updatedHits = [ 334 | ...oldHits, 335 | ...hits 336 | ]; 337 | 338 | this.setState({ 339 | results: { 340 | ...results, 341 | [searchKey]: { hits: updatedHits, page } 342 | }, 343 | isLoading: false 344 | }); 345 | } 346 | ~~~~~~~~ 347 | 348 | Here, we extracted values from the state, but updated the state depending on the previous state asynchronously. Now we'll use the functional approach to prevent bugs from a stale state: 349 | 350 | {title="src/App.js",lang="javascript"} 351 | ~~~~~~~~ 352 | setSearchTopStories(result) { 353 | const { hits, page } = result; 354 | 355 | # leanpub-start-insert 356 | this.setState(prevState => { 357 | ... 358 | }); 359 | # leanpub-end-insert 360 | } 361 | ~~~~~~~~ 362 | 363 | We can move the whole block we implemented into the function by directing it to operate on the `prevState` instead of the `this.state`. 364 | 365 | {title="src/App.js",lang="javascript"} 366 | ~~~~~~~~ 367 | setSearchTopStories(result) { 368 | const { hits, page } = result; 369 | 370 | this.setState(prevState => { 371 | # leanpub-start-insert 372 | const { searchKey, results } = prevState; 373 | 374 | const oldHits = results && results[searchKey] 375 | ? results[searchKey].hits 376 | : []; 377 | 378 | const updatedHits = [ 379 | ...oldHits, 380 | ...hits 381 | ]; 382 | 383 | return { 384 | results: { 385 | ...results, 386 | [searchKey]: { hits: updatedHits, page } 387 | }, 388 | isLoading: false 389 | }; 390 | # leanpub-end-insert 391 | }); 392 | } 393 | ~~~~~~~~ 394 | 395 | That will fix the issue with a stale state, but there is still one more improvement. Since it is a function, you can extract the function for improved readability. One more advantage to use a function over an object is that function can live outside of the component. We still have to use a higher-order function to pass the result to it since we want to update the state based on the fetched result from the API. 396 | 397 | {title="src/App.js",lang="javascript"} 398 | ~~~~~~~~ 399 | setSearchTopStories(result) { 400 | const { hits, page } = result; 401 | this.setState(updateSearchTopStoriesState(hits, page)); 402 | } 403 | ~~~~~~~~ 404 | 405 | The `updateSearchTopStoriesState()` function has to return a function. It is a higher-order function that can be defined outside the App component. Note how the function signature changes slightly now. 406 | 407 | {title="src/App.js",lang="javascript"} 408 | ~~~~~~~~ 409 | # leanpub-start-insert 410 | const updateSearchTopStoriesState = (hits, page) => (prevState) => { 411 | const { searchKey, results } = prevState; 412 | 413 | const oldHits = results && results[searchKey] 414 | ? results[searchKey].hits 415 | : []; 416 | 417 | const updatedHits = [ 418 | ...oldHits, 419 | ...hits 420 | ]; 421 | 422 | return { 423 | results: { 424 | ...results, 425 | [searchKey]: { hits: updatedHits, page } 426 | }, 427 | isLoading: false 428 | }; 429 | }; 430 | # leanpub-end-insert 431 | 432 | class App extends Component { 433 | ... 434 | } 435 | ~~~~~~~~ 436 | 437 | The function instead of object approach in `setState()` fixes potential bugs, while increasing the readability and maintainability of your code. Further, it becomes testable outside of the App component. I advise exporting and testing it as practice. 438 | 439 | ### Exercises: 440 | 441 | * Confirm your [source code for the last section](http://bit.ly/2HteTbk) 442 | * Confirm the [changes from the last section](http://bit.ly/2Hmbz1T) 443 | * Read about [React using state correctly](https://reactjs.org/docs/state-and-lifecycle.html#using-state-correctly) 444 | * Export updateSearchTopStoriesState from the file 445 | * Write a test for it which passes the a payload (hits, page) and a made up previous state and finally expect a new state 446 | * Refactor your `setState()` methods to use a function, but only when it makes sense, because it relies on props or state 447 | * Run your tests again and verify that everything is up to date 448 | 449 | ## Taming the State 450 | 451 | Previous chapters have shown you that state management can be a crucial topic in larger applications, as React and a lot of other SPA frameworks struggle with it. As applications get more complex, the big challenge in web applications is to tame and control the state. 452 | 453 | Compared to other solutions, React has already taken a big step forward. A unidirectional data flow and a simple API to manage state in components is indispensable. These concepts make it easier to reason about your state and your state changes. It also makes it easier to reason about it on a component level and on an application level to a certain degree. 454 | 455 | It is possible to introduce bugs by operating on stale state when using an object over a function in `setState()`. We lift state around to share or hide necessary state across components. Sometimes a component needs to lift up state, because its sibling component depends on it. Perhaps the component is far away in the component tree, so the stated needs to be shared across the whole component tree. Components are more involved in state management, as the main responsibility of components is representing the UI. 456 | 457 | Because of this, there are standalone solutions to take care of state management. Libraries like [Redux](https://redux.js.org/introduction) or [MobX](https://mobx.js.org/) are both feasible solutions in a React application. They come with extensions, [react-redux](https://github.com/reactjs/react-redux) and [mobx-react](https://github.com/mobxjs/mobx-react), to integrate them into the React view layer. Redux and MobX are outside of the scope of this book, but I encourage you to study the different ways to handle scaling state management as your React applications become more complex. 458 | 459 | ### Exercises: 460 | 461 | * Read about [external state management and how to learn it](https://www.robinwieruch.de/redux-mobx-confusion/) 462 | * Check out my second book about state management in React called [Taming the State in React](https://roadtoreact.com/) 463 | 464 | {pagebreak} 465 | 466 | You have learned advanced state management in React! Let's recap the last chapter: 467 | 468 | * **React** 469 | * Lift state management up and down to suitable components 470 | * `setState()` can use a function to prevent stale state bugs 471 | * Existing external solutions that help you to tame the state 472 | 473 | You can find the source code in the [official repository](http://bit.ly/2HteTbk). -------------------------------------------------------------------------------- /manuscript/contributor.md: -------------------------------------------------------------------------------- 1 | # Contributor(s) 2 | 3 | Many people have made it possible to present *The Road to learn React*, which is currently one of the most downloaded React.js books in circulation. The original author is German software engineer [Robin Wieruch](https://www.robinwieruch.de/), though translations of the book wouldn't be possible without the help of many others. This version of the book got translated by ... [tell your personal story here]. 4 | 5 | [Note: Translate and complete this paragraph for your own personal pitch. Include your social media account, GitHub account, website or anything else that is personally related to you. It's your opportunity to tell something about your own work as software engineer or translator. Afterward, include it in the Book.txt below the foreword.md that it can be used to compile the finished ebook.] 6 | 7 | {pagebreak} -------------------------------------------------------------------------------- /manuscript/deployChapter.md: -------------------------------------------------------------------------------- 1 | # Final Steps to Production 2 | 3 | The last chapters will show you how to deploy your application to production. For this, we use the free hosting service Heroku. On the way to deploying the application, we will uncover more about *create-react-app*. 4 | 5 | ## Eject 6 | 7 | The following knowledge is not necessary to deploy your application to production, but it still bears mentioning. *create-react-app* comes with one feature to keep it extendable, but also to prevent a vendor lock-in. A vendor lock-in usually happens when you buy into a technology but there is no escape hatch from using it in the future. Fortunately, in *create-react-app* you have such an escape hatch with "eject". 8 | 9 | In your *package.json* you will find the scripts to *start*, *test*, and *build* your application. The last script is *eject*. However, it is important you know that **it is a one-way operation.** That means **once you eject, you can't go back!** It is advisable to stay in the safe environment of *create-react-app* if you have just started creating applications in React. 10 | 11 | If you feel comfortable enough to run `npm run eject`, the command copies all the configuration and dependencies to your *package.json* and a new *config/* folder. It converts the whole project into a custom setup with tooling that includes Babel and Webpack, and grants full control over all these tools. 12 | 13 | Its official documentation says *create-react-app* is suitable for small to mid size projects, so you shouldn't feel obligated to use the "eject" command until you're ready. 14 | 15 | ### Exercises: 16 | 17 | * Read about [eject](https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/template/README.md#npm-run-eject) 18 | 19 | ## Deploy your App 20 | 21 | No application should stay on localhost; eventually, it has to go live to see how it will perform in real-life scenarios. Heroku is a platform as a service where you can host your application, and it offers seamless integration with React. Specifically, it's possible to deploy a *create-react-app* in minutes, with a a zero-configuration deployment which follows the philosophy of *create-react-app*. 22 | 23 | There are two requirements before an application can be deployed to Heroku: 24 | 25 | * Install the [Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli) 26 | * Create a [free Heroku account](https://www.heroku.com/) 27 | 28 | If you have installed Homebrew, you can install the Heroku CLI from command line: 29 | 30 | {title="Command Line",lang="text"} 31 | ~~~~~~~~ 32 | brew update 33 | brew install heroku-toolbelt 34 | ~~~~~~~~ 35 | 36 | Now you can use git and Heroku CLI to deploy your application: 37 | 38 | {title="Command Line",lang="text"} 39 | ~~~~~~~~ 40 | git init 41 | heroku create -b https://github.com/mars/create-react-app-buildpack.git 42 | git add . 43 | git commit -m "react-create-app on Heroku" 44 | git push heroku master 45 | heroku open 46 | ~~~~~~~~ 47 | 48 | That's all there is to it. If you run into problems, check these resources for troubleshooting options: 49 | 50 | * [Git and GitHub Essentials](https://www.robinwieruch.de/git-essential-commands/) 51 | * [Deploying React with Zero Configuration](https://blog.heroku.com/deploying-react-with-zero-configuration) 52 | * [Heroku Buildpack for create-react-app](https://github.com/mars/create-react-app-buildpack) 53 | -------------------------------------------------------------------------------- /manuscript/finalwords.md: -------------------------------------------------------------------------------- 1 | # Outline 2 | 3 | So now we've reached the end of The Road to Learn React. I hope you enjoyed reading it, and I hope it helped you to gain some traction in React. If you liked the book, share it as a way to learn React with your friends. It should be used as giveaway. Also, a review on [Amazon](https://amzn.to/2JHlP42) or [Goodreads](https://www.goodreads.com/book/show/37503118-the-road-to-learn-react) can really help improve future projects. 4 | 5 | **So, where can you go from here?** I recommend you extend the application on your own, so eventually you can start creating your very own React projects. Consider doing this before you dive into another book, course or tutorial. Do it for one week, take it to production by deploying it, and reach out to me [me](https://twitter.com/rwieruch) or others to showcase it. I am always interested in seeing what my readers built and helping them along. 6 | 7 | If you are looking for more extensions for your application, I recommend several learning paths after you've mastered the basics: 8 | 9 | * **Connecting to a Database and/or Authentication:** In a growing React application, you may want to persist data eventually. The data should be stored in a database so it can survive after a browser session, and so it can be shared across different users using your application. The simplest way to introduce a database is Firebase. In [this tutorial](https://www.robinwieruch.de/complete-firebase-authentication-react-tutorial/), you will find a step-by-step guide on how to use Firebase authentication in React. Beyond that, you can use Firebase's realtime database to store user entities. 10 | 11 | * **State Management:** You have used React `this.setState()` and `this.state` to manage and access local component state. That's a good start. In a larger application, however, you will experience the [limits of React's local component state](https://www.robinwieruch.de/learn-react-before-using-redux/). It is imperative you learn to use third-party state management libraries like [Redux or MobX](https://www.robinwieruch.de/redux-mobx-confusion/). On my [course platform](https://roadtoreact.com/), you will find the course "Taming the State in React" that teaches about advanced local state management using Redux. The course comes with an ebook as well. 12 | 13 | * **React Hooks:** You have learned about two kinds of React components: Class and Function Components. In the book it says that Function Components cannot have state management and thus they are called Functional Stateless Components. That's not true anymore, because a recent feature called Hooks in React enables us to use state and side-effects in Function Components. That's why you should learn more about [React Hooks](https://www.robinwieruch.de/react-hooks/). 14 | 15 | * **Tooling with Webpack and Babel:** We used *create-react-app* to set up the application we created for this book. At some point you may want to learn the tooling around it, which enables you to setup your own project without *create-react-app*. I recommend a minimal setup with [Webpack and Babel](https://www.robinwieruch.de/minimal-react-webpack-babel-setup/), after which you can apply additional tooling on your own. For instance, you could [use ESLint](https://www.robinwieruch.de/react-eslint-webpack-babel/) to follow a unified code style. 16 | 17 | * **Code Organization:** Recall if you will the chapter about code organization. You can apply these changes now, if you haven't already. It will help organize your components in structured files and folders (modules), and it will help you understand the principles of code splitting, reusability, maintainability, and module API design. Your applications will eventually grow and need to be structured into modules, so it's better to start now. 18 | 19 | * **Testing:** The book only scratched the surface of testing. If you are not familiar with the topic, you should dive deeper into unit testing and integration testing, especially with React applications. I would recommend to stick to Enzyme and Jest for implementations, to refine your approaches with unit and snapshot tests. 20 | 21 | * **React Component Syntax:** The best practices for implementing React components evolve over time. You will find many ways to write your React components, especially React class components, in other learning resources. A GitHub repository called [react-alternative-class-component-syntax](https://github.com/the-road-to-learn-react/react-alternative-class-component-syntax) is a great way to learn alternative ways to write React class components. By using class field declarations, you can write them even more concisely in the future. 22 | 23 | * **UI Components:** Many beginners make the mistake of introducing UI component libraries too early in their projects. It is more practical to learn how to implement and use a dropdown, checkbox, or dialog in React with standard HTML elements. Most of these components will manage their own local state. A checkbox has to know whether it is checked or unchecked, so you should implement them as controlled components. After we covered the foundational implementations, introducing a UI component library with checkboxes and dialogs as React components should be easier. 24 | 25 | * **Routing:** You can implement routing for your application with [react-router](https://github.com/ReactTraining/react-router). So far, there is only one page in your application. React Router helps manage multiple pages across multiple URLs. When you introduce routing to your application, you don't make any requests to your web server to request the next page. The router will do everything for you on the client-side. So get your hands dirty and introduce routing in your application. 26 | 27 | * **Type Checking:** Earlier, we used React PropTypes to define component interfaces. It is a good practice to prevent bugs, but the PropTypes are only checked on runtime. You can go further by introducing static type checking at compile time. [TypeScript](https://www.typescriptlang.org/) and [Flow](https://flowtype.org/) are popular type systems for React. 28 | 29 | * **React Native:** [React Native](https://facebook.github.io/react-native/) brings your application to mobile devices, such as iOS and Android applications. Once you've mastered React, the learning curve for React Native shouldn't be that steep, as they share the same principles. The only difference with mobile are the layout components. 30 | 31 | * **Other Projects:** There are plenty of tutorials out there that use only React to build exciting applications, which provide good practice to apply what you've learned in this book before you move on to intermediate projects. Here are a few of my own: 32 | * [A paginated and infinite scrolling list](https://www.robinwieruch.de/react-paginated-list/) 33 | * [Showcasing tweets on a Twitter wall](https:/www.robinwieruch.de/react-svg-patterns/) 34 | * [Connecting your React application to Stripe for charging money](https://www.robinwieruch.de/react-express-stripe-payment/). 35 | 36 | I invite you to visit my [website](https://www.robinwieruch.de) to find more interesting topics about web development and software engineering. You can also [subscribe to my Newsletter](https://www.getrevue.co/profile/rwieruch) to get updates about articles, books, and courses. Furthermore, my [course platform](https://roadtoreact.com) offers more advanced lessons about the React ecosystem. 37 | 38 | Finally, I hope to find more [Patrons](https://www.patreon.com/rwieruch) who are willing to support my content, as there are many students who cannot afford to pay for educational content. That's why I put lots of my content out there for free. Supporting me on Patreon allows to educate others for free. 39 | 40 | Once again, if you liked the book, I ask that you take a moment to think about a person who would like to learn React and share it with them. The book is intended to be given to others, and it will improve over time if more people read it and share feedback. 41 | 42 | Thank you for reading the Road to learn React. 43 | 44 | Regards, 45 | 46 | Robin Wieruch 47 | 48 | 49 | -------------------------------------------------------------------------------- /manuscript/foreword.md: -------------------------------------------------------------------------------- 1 | # Foreword 2 | 3 | The Road to learn React teaches the fundamentals of React. You will build a real-world application in plain React without complicated tooling. Everything from project setup to deployment on a server will be explained for you. The book comes with additional referenced reading material and exercises with each chapter. After reading the book, you will be able to build your own applications in React. The material is kept up to date by myself and the community. 4 | 5 | In the Road to learn React, I offer a foundation before you dive into the broader React ecosystem. The concepts will have less tooling and less external state management, but a lot of information about React. It explains general concepts, patterns, and best practices in a real world React application. 6 | 7 | Essentially, you will learn to build your own React application from scratch, with features like pagination, client-side caching, and interactions like searching and sorting. Additionally, you will transition from JavaScript ES5 to JavaScript ES6. I hope this book captures my enthusiasm for React and JavaScript, and that it helps you get started with it. 8 | 9 | {pagebreak} 10 | 11 | ## About the Author 12 | 13 | I am a German software and web engineer dedicated to learning and teaching programming in JavaScript. After obtaining my Master's Degree in computer science, I continued learning on my own. I gained experience from the startup world, where I used JavaScript intensively during both my professional life and spare time, which eventually led to a desire to teach others about these topics. 14 | 15 | For a few years, I worked closely with an exceptional team of engineers at a company called Small Improvements, developing large scale applications. The company offered a SaaS product that enables customers to give feedback to businesses. This application was developed using JavaScript on its frontend, and Java as its backend. The first iteration of Small Improvements' frontend was written in Java with the Wicket Framework and jQuery. When the first generation of SPAs became popular, the company migrated to Angular 1.x for its frontend application. After using Angular for over two years, it became clear that Angular wasn't the best solution to work with state intense applications, so they made the jump to React and Redux. This enabled it to operate on a large scale successfully. 16 | 17 | During my time in the company, I regularly wrote articles about web development on my website. I received great feedback from people learning from my articles which allowed me to improve my writing and teaching style. Article after article, I grew my ability to teach others. I felt that my first articles were packed with too much information, quite overwhelming for students, but I improved by focusing on one subject at a time. 18 | 19 | Currently, I am a self-employed software engineer and educator. I find it a fulfilling pastime to see students thrive by giving them clear objectives and short feedback loops. You can find more information about me and ways to support and work with me on my [website](https://www.robinwieruch.de/about). 20 | 21 | {pagebreak} 22 | 23 | ## Testimonials 24 | 25 | There are many [testimonials](https://roadtoreact.com/), [ratings](https://www.goodreads.com/book/show/37503118-the-road-to-learn-react) and [reviews](https://www.amazon.com/dp/B077HJFCQX) about the book that you can read to ascertain its quality. I am proud of it, and I never expected such overwhelming feedback. I would love to find your rating/review. It helps me to spread the word about the book and make improvements for future projects. The following shows a short excerpt of these voices: 26 | 27 | **[Muhammad Kashif](https://twitter.com/appsdevpk/status/848625244956901376):** "The Road to Learn React is a unique book that I recommend to any student or professional interested in learning react basics to advanced level. It is packed with insightful tips and techniques that are hard to find elsewhere, and remarkably thorough in its use of examples and references to sample problems, i have 17 years of experience in web and desktop app development, and before reading this book i was having trouble in learning react, but this book works like magic." 28 | 29 | **[Andre Vargas](https://twitter.com/andrevar66/status/853789166987038720):** "The Road to Learn React by Robin Wieruch is such an awesome book! Most of what I learned about React and even ES6 was through it!" 30 | 31 | **[Nicholas Hunt-Walker, Instructor of Python at a Seattle Coding School](https://twitter.com/nhuntwalker/status/845730837823840256):** "This is one of the most well-written & informative coding books I've ever worked through. A solid React & ES6 introduction." 32 | 33 | **[Austin Green](https://twitter.com/AustinGreen/status/845321540627521536):** "Thanks, really loved the book. Perfect blend to learn React, ES6, and higher level programming concepts." 34 | 35 | **[Nicole Ferguson](https://twitter.com/nicoleffe/status/833488391148822528):** "I'm doing Robin's Road to Learn React course this weekend & I almost feel guilty for having so much fun." 36 | 37 | **[Karan](https://twitter.com/kvss1992/status/889197346344493056):** "Just finished your Road to React. Best book for a beginner in the world of React and JS. Elegant exposure to ES. Kudos! :)" 38 | 39 | **[Eric Priou](https://twitter.com/erixtekila/status/840875459730657283):** "The Road to learn React by Robin Wieruch is a must read. Clean and concise for React and JavaScript." 40 | 41 | {pagebreak} 42 | 43 | ## Education for Children 44 | 45 | The book should enable everyone to learn React. However, not everyone has access to the required resources, because not everyone is educated in the English language. I want to use this project to support other projects that teach children English in the developing world. 46 | 47 | * April to 18. April, 2017, [Giving Back, By Learning React](https://www.robinwieruch.de/giving-back-by-learning-react/) 48 | 49 | {pagebreak} 50 | 51 | ## FAQ 52 | 53 | **How to get updates?** 54 | 55 | I have two channels where I share updates about my content. You can [subscribe to updates by email](https://www.getrevue.co/profile/rwieruch) or [follow me on Twitter](https://twitter.com/rwieruch). Regardless of the channel, my objective is to only share quality content. Once you receive notification the book has changed, you can download a new version of it. 56 | 57 | **Does it use the recent React version?** 58 | 59 | The book always receives an update when the React version is updated. Programming books are usually outdated soon after their release, but since this book is self-published, I can update it as needed. 60 | 61 | **Does it cover Redux?** 62 | 63 | It doesn't, so I have written a second book. The Road to learn React should give you a solid foundation before you dive into advanced topics. The implementation of the sample application in the book will show that you don't need Redux to build an application in React. After you have read the book, you should be able to implement a solid application without Redux. Then you can read my second book, Taming the State in React, to learn Redux. 64 | 65 | **Does it use JavaScript ES6?** 66 | 67 | Yes. Don't worry, though, you will be fine if you are familiar with JavaScript ES5. All JavaScript ES6 features, that I describe in The Journey to Learn React, will transition from ES5 to ES6. The book does not only teach React, but also all useful JavaScript ES6 features. 68 | 69 | **How to get access to the source code projects and screencasts series?** 70 | 71 | If you bought one of the extended packages that grant access to the source code projects, screencast series or any other add-on, you should find these on your [course dashboard](https://roadtoreact.com/my-courses). If you bought the course other than [the official Road to React](https://roadtoreact.com) course platform, create an account on the platform, and then go to the Admin page and contact me with one of the email templates. After that I can enroll you in the course. If you haven't bought one of the extended packages, you can reach out any time to upgrade your content to access the source code projects and screencast series. 72 | 73 | **Can I get a copy of the book if I bought it on Amazon?** 74 | 75 | If you have bought the book on Amazon, you may have seen that the book is available on my website too. Since I use Amazon as one way to monetize my often free content, I honestly thank you for your support and invite you to sign up on [Road to React](https://roadtoreact.com). There you can write me an email (Admin page) about your purchase, so that I can unlock the whole course package for you. In addition, you can always download the latest ebook version of the book on the platform. 76 | 77 | **How can I get help while reading the book?** 78 | 79 | The book has a [Slack Group](https://slack-the-road-to-learn-react.wieruch.com/) for people who are reading along. You can join the channel to get help, or to help others, as helping others may help you internalize your own understanding. If there is no one available to help you, you can always reach out to me. 80 | 81 | **Is there any troubleshoot area?** 82 | 83 | If you run into problems, please join the Slack Group. Also, check the [open issues on GitHub](https://github.com/rwieruch/the-road-to-learn-react/issues) or in the GitHub repositories of the applications you will build along the way to see if any solutions are listed for specific issue. If your problem wasn't mentioned, open a new issue where you can explain your problem, provide a screenshot, and offer more details (e.g. book page, node version). 84 | 85 | **Can I help to improve the content?** 86 | 87 | Yes, I love to hear feedback. Simply open an issue on [GitHub](https://github.com/rwieruch/the-road-to-learn-react). These can be technical improvements, or clarification on the discussed topics. You can open pull requests on the GitHub page as well. 88 | 89 | **Is there a money back guarantee?** 90 | 91 | Yes, there is 100% money back guarantee for two months if you don't think it's a good fit. Please reach out to me to get a refund. 92 | 93 | **How to support the project?** 94 | 95 | If you find my lessons useful and would like to contribute, seek my website's [About Page](https://www.robinwieruch.de/about/) for information about how to offer support. It is also very helpful for my readers spread the word about how my books helped them, so others might discover ways to improve their web development skillsets. Contributing through any of the provided channels gives me the freedom to create in-depth courses, and to continue offering free material. 96 | 97 | **What's your motivation behind the book?** 98 | 99 | I want to teach about this topic consistently. I often find materials online that don't receive update, or only applies to a small part of a topic. Sometimes people struggle to find consistent and up-to-date resources to learn from. I want to provide this consistent and up-to-date learning experience. Also, I hope I can support the less fortunate with my projects by giving them the content for free or by [having other impacts](https://www.robinwieruch.de/giving-back-by-learning-react/). Recently I've found myself fulfilled when teaching others about programming, as it's a meaningful activity I prefer over any 9 to 5 job at any company. I hope to continue this path in the future. 100 | 101 | {pagebreak} 102 | 103 | ## Change Log 104 | 105 | **10. January 2017:** 106 | 107 | * [v2 Pull Request](https://github.com/rwieruch/the-road-to-learn-react/pull/18) 108 | * even more beginner friendly 109 | * 37% more content 110 | * 30% improved content 111 | * 13 improved and new chapters 112 | * 140 pages of learning material 113 | * [+ interactive course of the book on educative.io](https://www.educative.io/collection/5740745361195008/5676830073815040) 114 | 115 | **08. March 2017:** 116 | 117 | * [v3 Pull Request](https://github.com/rwieruch/the-road-to-learn-react/pull/34) 118 | * 20% more content 119 | * 25% improved content 120 | * 9 new chapters 121 | * 170 pages of learning material 122 | 123 | **15. April 2017:** 124 | 125 | * upgrade to React 15.5 126 | 127 | **5. July 2017:** 128 | 129 | * upgrade to node 8.1.3 130 | * upgrade to npm 5.0.4 131 | * upgrade to create-react-app 1.3.3 132 | 133 | **17. October 2017:** 134 | 135 | * upgrade to node 8.3.0 136 | * upgrade to npm 5.5.1 137 | * upgrade to create-react-app 1.4.1 138 | * upgrade to React 16 139 | * [v4 Pull Request](https://github.com/rwieruch/the-road-to-learn-react/pull/72) 140 | * 15% more content 141 | * 15% improved content 142 | * 3 new chapters (Bindings, Event Handlers, Error Handling) 143 | * 190+ pages of learning material 144 | * [+9 Source Code Projects](https://roadtoreact.com) 145 | 146 | **17. February 2018:** 147 | 148 | * upgrade to node 8.9.4 149 | * upgrade to npm 5.6.0 150 | * upgrade to create-react-app 1.5.1 151 | * [v5 Pull Request](https://github.com/the-road-to-learn-react/the-road-to-learn-react/pull/105) 152 | * more learning paths 153 | * extra reading material 154 | * 1 new chapter (Axios instead of Fetch) 155 | 156 | **31. August 2018:** 157 | 158 | * professional proofreading and editing by Emmanuel Stalling 159 | * [16 Source Code Projects](https://roadtoreact.com) 160 | * [v6 Pull Request](https://github.com/the-road-to-learn-react/the-road-to-learn-react/pull/172) 161 | 162 | **3. October 2018:** 163 | 164 | * upgrade to node 10.11.0 165 | * upgrade to npm 6.4.1 166 | * upgrade to create-react-app 2.0.2 167 | 168 | {pagebreak} 169 | 170 | ## Challenge 171 | 172 | Personally I write a lot about my learnings. That's how I got where I am right now. You are teaching a topic at its best when you have just learned about it yourself. Since teaching helped me a lot in my career, I want you to experience the same effects of it. But first you have to do the cause: teaching yourself. My challenge for the book is the following: teach others what you are learning while reading the book. A couple of breakpoints on how you could achieve it: 173 | 174 | * Write a blog post about a specific topic from the book. It's not about copying and pasting the material but rather about teaching the topic your own way. Find your own words to explain something, grab a problem and solve it, and dive even more into the topic by understanding every detail about it. Then teach it to others in this one article. You will see how it fills your knowledge gaps, because you have to dig deeper into the topic, and how it opens doors for your career in the long term. 175 | 176 | * If you are active on social media, grab a couple of things you have learned while reading the book and share them with your friends. For instance, you can tweet a hot tip on Twitter about your last learning from the book which may be interesting for others too. Just take a screenshot of the passage of the book or even better: write about it in your own words. That's how you can get into teaching without investing much time. 177 | 178 | * If you feel confident recording your learning adventure, share your way through the book on Facebook Live, YouTube Live or Twitch. It helps you to stay concentrated and work your way through the book. Even though you don't have many people following your live session, you can always use the video to put it on YouTube afterward. Besides it is a great way to verbalize your problems and how you are going to solve them. 179 | 180 | I would love to see people doing especially the last breakpoint: record yourself while reading this book, implementing the application(s), and conducting the exercises, and put the final version on YouTube. If parts in between of the recording are taking longer, just cut the video or use a timelapse effect for them. If you get stuck and need to fix a bug, don't leave it out but rather include these passages in the video, because they are so valuable for your audience which may run into the same issues. I believe it is important to have these parts in your video. A couple of tips for the video: 181 | 182 | * Do it in your native language or in English if you feel comfortable with it. 183 | 184 | * Verbalize your thoughts, the things you are doing or the problems you are running into. Having a visual video is only one part of the challenge, but the other part is you narrating through the implementation. It doesn't have to be perfect. Instead it should feel natural and not polished as all the other video courses online where nobody runs into problems. 185 | 186 | * If you run into bugs, embrace the trouble. Try to fix the problem yourself and search online for help, don't give up, and speak about the problem and how you attempt to solve it. This helps others to follow your thought process. As I said before, it has no value to follow polished video courses online where the instructor never runs into problems. It's the most valuable part to see someone else fixing a bug in the source code. 187 | 188 | * Some words about the technical side of the recording: Check your audio before you record a longer video. It should have the correct volume and the quality should be alright too. Regarding your editor/IDE/terminal, make sure to increase the font size. Maybe it is possible to place the code and the browser side by side. If not, make them fullscreen and switch between (e.g. MacOS CMD + Tab). 189 | 190 | * Edit the video yourself before you put it on YouTube. It doesn't have to be a high quality, but you should try to keep it concise for your audience (e.g. leaving out the reading passages and rather summarize the steps in your own words). 191 | 192 | In the end, you can reach out to me for promoting anything you have released. For instance, if the video turns out well, I would love to include it in this book as officially supplementary material. Just reach out to me once you finished it. After all, I hope you accept this challenge to enhance your learning experience while reading the book which also may help others. I wish you all the best for it. 193 | 194 | {pagebreak} 195 | --------------------------------------------------------------------------------