Introduction
This course is unique as compared to other React introductions because this course attempts to teach you not only React but the ecosystem around React. When I was learning React myself, I found myself frustrated that it seemed like every tutorial started on step 14 and left out the steps 1-13 of how to set up a React project. React is nearly never used by itself so it’s useful to know the tools you’re using. I believe you as a developer should know how your tools works and what purpose they’re serving. Many times have I taught courses similar to this one to hear people using tools and complaining about them because they don’t actually know why they’re using them, just that they’re necessary. As such, in this course we show you how to build projects without any using tools at all and introduce the various tools, one at a time so you understand the actual problem being solved by the tool. Hopefully given the knowledge of the problem solved by the tool you’ll embrace the tools despite their complexities due to the ease and power they offer you.
github
course note
sourcetree
Git In-depth
Front End Happy Hour
Project Setup Emmet cheatsheet
A Note on the Course Font Dank Mono
Fira Code
Pure React Getting Started with Pure React
1 2 3 4 5 6 7 8 9 10 11 12 const App = ( ) => { return React .createElement ( 'div' , {}, React .createElement ('h1' , {}, 'Adopt Me!' ) ) } ReactDOM .render ( React .createElement (App ), document .getElementById ('root' ) )
createElement Arguments Three parameters that React.createElement
get:
what kind of tag is it (h1
or div
or a composite component)
All the attribute that you want to give the component ({ id: ?? })
Children
Reusable Components 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const Pet = ( ) => { return React .createElement ('div' , {}, [ React .createElement ('h1' , {}, "Luna" ), React .createElement ('h2' , {}, "Dog" ), React .createElement ('h2' , {}, "Havanese" ) ]) } const App = ( ) => { return React .createElement ( 'div' , {}, [ React .createElement ('h1' , { id : 'something' }, 'Adopt Me!' ), React .createElement (Pet ), React .createElement (Pet ), React .createElement (Pet ) ] ) }
Passing in Component Props 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 const Pet = (props ) => { return React .createElement ('div' , {}, [ React .createElement ('h1' , {}, props.name ), React .createElement ('h2' , {}, props.animal ), React .createElement ('h2' , {}, props.breed ) ]) } const App = ( ) => { return React .createElement ( 'div' , {}, [ React .createElement ('h1' , { id : 'something' }, 'Adopt Me!' ), React .createElement (Pet , { name : 'Luna' , animal : 'Dog' , breed : 'Havanese' }), React .createElement (Pet , { name : 'Pepper' , animal : 'Bird' , breed : 'Cockatiel' }), React .createElement (Pet , { name : 'Doink' , animal : 'Cat' , breed : 'Stray' }) ] ) }
Destructuring Props 可以使用es6的对象结构赋值重新设置参数
1 2 3 4 5 6 7 const Pet = ({ name, animal, breed } ) => { return React .createElement ('div' , {}, [ React .createElement ('h1' , {}, name), React .createElement ('h2' , {}, animal), React .createElement ('h2' , {}, breed) ]) }
destructuring
npm & Generating a package.json
File npm
Prettier
npm Scripts
package.json
1 2 3 4 "scripts" : { "format" : "prettier \"src/**/*.{js,html}\" --write" , "test" : "echo \"Error: no test specified\" && exit 1" } ,
Prettier Setup
.prettierrc
1 2 3 4 { "semi" : false , "singleQuote" : true }
https://prettier.io/
ESLint Setup 1 npm i -D eslint eslint-config-prettier
ESLint Confirguration
.eslintrc.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 { "extends" : [ "eslint:recommended" , "prettier" , "prettier/react" ] , "plugins" : [ ] , "parserOptions" : { "ecmaVersion" : 2019 , "sourceType" : "module" , "ecmaFeatures" : { "jsx" : true } } , "env" : { "es6" : true , "browser" : true , "node" : true } }
package.json
1 "lint" : "eslint \"src/**/*.{js,jsx}\" --quiet"
gitignore
.gitignore设置
node_modules
.cache/
dist/
.env
.DS_Store
coverage/
.vscode/
Parcel Webpack 4 Fundamentals
Complete Intro to React, v3 (feat. Redux, Router & Flow)
Parcel 极速零配置Web应用打包工具
1 npm install -D parcel-bundler
package.json
1 "dev" : "parcel src/index.html" ,
Installing React & ReactDOM
app.js
1 2 import React from 'react' import { render } from 'react-dom'
Separate App into Modules 鼠标选中组件高亮显示,点击小灯泡(code action),选择move to a new file
,会将组件自动生成新文件并在当前文件进行导入
Pet.js
1 2 3 4 5 6 7 8 import React from 'react' export default function Pet ({ name, animal, breed } ) { return React .createElement ('div' , {}, [ React .createElement ('h1' , {}, name), React .createElement ('h2' , {}, animal), React .createElement ('h2' , {}, breed) ]) }
JSX Converting to JSX Babel 是一个 JavaScript 编译器
Pet.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import React from 'react' export default function Pet ({ name, animal, breed } ) { return ( <div > <h1 > {name}</h1 > <h2 > {animal}</h2 > <h2 > {breed}</h2 > </div > ) }
Configuring ESLint for React 1 npm install -D babel-eslint eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react
.eslintrc.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 { "extends" : [ "eslint:recommended" , "plugin:import/errors" , "plugin:react/recommended" , "plugin:jsx-a11y/recommended" , "prettier" , "prettier/react" ] , "plugins" : [ "react" , "import" , "jsx-a11y" ] , "rules" : { "react/prop-types" : 0 , "no-console" : 1 } , "parserOptions" : { "ecmaVersion" : 2019 , "sourceType" : "module" , "ecmaFeatures" : { "jsx" : true } } , "env" : { "es6" : true , "browser" : true , "node" : true } , "settings" : { "react" : { "version" : "detect" } } }
JSX Composite Components & Expressions 使用JSX语法重构App.js
App.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import React from 'react' import { render } from 'react-dom' import Pet from './Pet' const App = ( ) => { return ( <div > <h1 > Adopt Me!</h1 > <Pet name ="Luna" animal ="Dog" breed ="Havanese" /> <Pet name ="Pepper" animal ="Bird" breed ="Cocktiel" /> <Pet name ="Doink" animal ="Cat" breed ="Mixed" /> </div > ) } render ( <App /> , document .getElementById ('root' ))
Hooks Creating a Search Component notes
Hooks in Depth
1 touch ./src/SearchParams.js
SearchParams.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import React from 'react' const SearchParams = ( ) => { const location = 'Seattle, WA' return ( <div className ="search-params" > <form > <label htmlFor ="location" > Location <input id ="location" value ={location} placeholder ="location" /> </label > <button > Submint</button > </form > </div > ) } export default SearchParams
App.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import React from 'react' import { render } from 'react-dom' import SearchParams from './SearchParams' const App = ( ) => { return ( <div > <h1 > Adopt Me!</h1 > <SearchParams /> </div > ) } render (<App /> , document .getElementById ('root' ))
Setting State with Hooks 在更改input
标签内容时,可以发现并没有改变,这是因为react在设计时没有选择双向数据绑定,需要用户自己操作数据的状态,而不是由框架进行处理。
此时我们需要引入hooks
进行数据双向绑定(所有hook
均以use
开头)
SearchParams.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import React , { useState } from 'react' const SearchParams = ( ) => { const [location, setLocation] = useState ('Seattle, WA' ) return ( <div className ="search-params" > <form > <label htmlFor ="location" > Location <input id ="location" value ={location} placeholder ="location" onChange ={e => setLocation(e.target.value)} /> </label > <button > Submint</button > </form > </div > ) } export default SearchParams
Best Practice for Hooks Hooks never go inside of if statements, and they never go inside for loops or anything like that.
Configuring ESLint for Hooks 1 npm i -D eslint-plugin-react-hooks
eslint-plugin-react-hooks
.eslintrc.json
1 2 3 4 5 6 7 "plugins" : [ "react" , "import" , "jsx-a11y" , "react-hooks" ] , "rules" : { "react/prop-types" : 0 , "no-console" : 1 , "react-hooks/rules-of-hooks" : 2 , "react-hooks/exhaustive-deps" : 1 } ,
开启之后将hook
放入if语句中会看到如下警告:
React Hook “useState” is called conditionally. React Hooks must be called in the exact same order in every component render.eslint(react-hooks/rules-of-hooks)
Calling the Pet API parcel
可以自动安装引用的npm包而不必手动安装
所以即可使用以下命令
1 npm i @frontendmasters/pet
也可只在js文件头部添加
1 import { ANIMALS } from '@frontendmasters/pet'
此时修改SearchParams.js如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 import React , { useState } from 'react' import { ANIMALS } from '@frontendmasters/pet' const SearchParams = ( ) => { const [location, setLocation] = useState ('Seattle, WA' ) const [animal, setAnimal] = useState ('dog' ) return ( <div className ="search-params" > <form > <label htmlFor ="location" > Location <input id ="location" value ={location} placeholder ="location" onChange ={e => setLocation(e.target.value)} /> </label > <label htmlFor ="animal" > Animal <select id ="animal" value ={animal} onChange ={e => setAnimal(e.target.value)} onBlur={e => setAnimal(e.target.value)} > <option > All</option > {ANIMALS.map(animal => ( <option value ={animal} > {animal}</option > ))} </select > </label > <button > Submit</button > </form > </div > ) } export default SearchParams
Unique List Item Keys 可以看到,此时lint提示我们要添加`属性,这是为了在diff算法进行时提升性能的措施。
此时修改SearchParams.js,给``添加`key`属性如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 import React , { useState } from 'react' import { ANIMALS } from '@frontendmasters/pet' const SearchParams = ( ) => { const [location, setLocation] = useState ('Seattle, WA' ) const [animal, setAnimal] = useState ('dog' ) return ( <div className ="search-params" > <form > <label htmlFor ="location" > Location <input id ="location" value ={location} placeholder ="location" onChange ={e => setLocation(e.target.value)} /> </label > <label htmlFor ="animal" > Animal <select id ="animal" value ={animal} onChange ={e => setAnimal(e.target.value)} onBlur={e => setAnimal(e.target.value)} > <option > All</option > {ANIMALS.map(animal => ( <option value ={animal} /*这里添加了key属性 */key ={animal} > {animal} </option > ))} </select > </label > <button > Submit</button > </form > </div > ) } export default SearchParams
Breed Dropdown 在form中添加breed选择标签
searchParams.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 import React , { useState } from 'react' import { ANIMALS } from '@frontendmasters/pet' const SearchParams = ( ) => { const [location, setLocation] = useState ('Seattle, WA' ) const [animal, setAnimal] = useState ('dog' ) const [breed, setBreed] = useState ('' ) const [breeds, setBreeds] = useState ([]) return ( <div className ="search-params" > <form > <label htmlFor ="location" > Location <input id ="location" value ={location} placeholder ="location" onChange ={e => setLocation(e.target.value)} onBlur={e => setLocation(e.target.value)} /> </label > <label htmlFor ="animal" > Animal <select id ="animal" value ={animal} onChange ={e => setAnimal(e.target.value)} onBlur={e => setAnimal(e.target.value)} > <option > All</option > {ANIMALS.map(animal => ( <option value ={animal} key ={animal} > {animal} </option > ))} </select > </label > <label htmlFor ="breed" > breed <select id ="breed" value ={breed} onChange ={e => setBreed(e.target.value)} onBlur={e => setBreed(e.target.value)} disabled={breeds.length === 0} > <option > All</option > {breeds.map(breedString => ( <option key ={breedString} value ={breedString} > {breedString} </option > ))} </select > </label > <button > Submit</button > </form > </div > ) } export default SearchParams
Custom Hooks 把animal标签和breed标签通过custom hooks抽离成公共方法
新建useDropdown.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import React , { useState } from 'react' const useDropdown = (label, defaultState, options ) => { const [state, setState] = useState (defaultState) const id = `use-dropdown-${label.replace('' , '' ).toLowerCase()} ` const Dropdown = ( ) => ( <label htmlFor ={id} > {label} <select id ={id} value ={state} onChange ={e => setState(e.target.value)} onBlur={e => setState(e.target.value)} disabled={options.length === 0} > <option > All</option > {options.map(item => ( <option key ={item} value ={item} > {item} </option > ))} </select > </label > ) return [state, Dropdown , setState] } export default useDropdown
修改SearchParams.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 import React , { useState } from 'react' import { ANIMALS } from '@frontendmasters/pet' import useDropdown from './useDropdown' const SearchParams = ( ) => { const [location, setLocation] = useState ('Seattle, WA' ) const [breeds, setBreeds] = useState ([]) const [animal, AnimalDropdown ] = useDropdown ('Animal' , 'dog' , ANIMALS ) const [breed, BreedDropdown ] = useDropdown ('Breed' , '' , breeds) return ( <div className ="search-params" > <form > <label htmlFor ="location" > Location <input id ="location" value ={location} placeholder ="location" onChange ={e => setLocation(e.target.value)} onBlur={e => setLocation(e.target.value)} /> </label > {/* <label htmlFor ="animal" > Animal <select id ="animal" value ={animal} onChange ={e => setAnimal(e.target.value)} onBlur={e => setAnimal(e.target.value)} > <option > All</option > {ANIMALS.map(animal => ( <option value ={animal} key ={animal} > {animal} </option > ))} </select > </label > <label htmlFor ="breed" > breed <select id ="breed" value ={breed} onChange ={e => setBreed(e.target.value)} onBlur={e => setBreed(e.target.value)} disabled={breeds.length === 0} > <option > All</option > {breeds.map(breedString => ( <option key ={breedString} value ={breedString} > {breedString} </option > ))} </select > </label > */} <AnimalDropdown /> <BreedDropdown /> <button > Submit</button > </form > </div > ) } export default SearchParams
Effects useEffect petfinder
SearchParams.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 import React , { useState, useEffect } from 'react' import pet, { ANIMALS } from '@frontendmasters/pet' import useDropdown from './useDropdown' const SearchParams = ( ) => { const [location, setLocation] = useState ('Seattle, WA' ) const [breeds, setBreeds] = useState ([]) const [animal, AnimalDropdown ] = useDropdown ('Animal' , 'dog' , ANIMALS ) const [breed, BreedDropdown ] = useDropdown ('Breed' , '' , breeds) useEffect (() => { setBreeds ([]) setBreed ('' ) pet.breeds (animal).then (({ breeds } ) => { const breedStrings = breeds.map (({ name } ) => name) setBreeds (breedStrings) }, console .error ) }) return ( <div className ="search-params" > <form > <label htmlFor ="location" > Location <input id ="location" value ={location} placeholder ="location" onChange ={e => setLocation(e.target.value)} onBlur={e => setLocation(e.target.value)} /> </label > <AnimalDropdown /> <BreedDropdown /> <button > Submit</button > </form > </div > ) } export default SearchParams
Declaring Effect Dependancies 上述代码中,页面每次重新渲染都会从api重新获取数据,根据需求这明显没有必要,此时需要给useEffect
添加参数,声明依赖,通过给useEffect
传入一个数组作为第二个参数,数组的每一项为状态改变后需要从api重新获取数据的变量。
SearchParams.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 import React , { useState, useEffect } from 'react' import pet, { ANIMALS } from '@frontendmasters/pet' import useDropdown from './useDropdown' const SearchParams = ( ) => { const [location, setLocation] = useState ('Seattle, WA' ) const [breeds, setBreeds] = useState ([]) const [animal, AnimalDropdown ] = useDropdown ('Animal' , 'dog' , ANIMALS ) const [breed, BreedDropdown , setBreed] = useDropdown ('Breed' , '' , breeds) useEffect (() => { setBreeds ([]) setBreed ('' ) pet.breeds (animal).then (({ breeds } ) => { const breedStrings = breeds.map (({ name } ) => name) setBreeds (breedStrings) }, console .error ) }, [animal, setBreed, setBreeds]) return ( <div className ="search-params" > <form > <label htmlFor ="location" > Location <input id ="location" value ={location} placeholder ="location" onChange ={e => setLocation(e.target.value)} onBlur={e => setLocation(e.target.value)} /> </label > <AnimalDropdown /> <BreedDropdown /> <button > Submit</button > </form > </div > ) } export default SearchParams
Effect Lifecycle Walkthrough 在SeacrhParmas.js
中,useEffect
是一个异步操作,在首次渲染中并不会执行,以提高首次渲染的速度。
在页面首次渲染完成后,会调用useEffect
的第一个参数,也就是更新breed
和breeds
的函数。在执行完毕后,会监听animal
,setBreed
和setBreeds
,也就是useEffect
第二个参数的数组中的每一项的数据状态有没有改变,只有当参数数组中的数据状态改变时,才会重新调用第一个参数函数更新breed
和breeds
的数据。
Run Only Once 如果只想在首次加载时执行useEffect
并只执行一次,可以将useEffect
第二个参数设置为一个空数组。如果想在每次重渲染后执行,只设置一个参数即可(此时会一直调用api获取数据,不要这样做)。
Environment Variables & Strict Mode NODE_ENV=development
React already has a lot of developer conveniences built into it out of the box. What’s better is that they automatically strip it out when you compile your code for production. So how do you get the debugging conveniences then? Well, if you’re using Parcel.js, it will compile your development server with an environment variable of NODE_ENV=development
and then when you run parcel build <entry point>
it will automatically change that to NODE_ENV=production
which is how all the extra weight gets stripped out. Why is it important that we strip the debug stuff out? The dev bundle of React is quite a bit bigger and quite a bit slower than the production build. Make sure you’re compiling with the correct environmental variables or your users will suffer.
Strict Mode
React has a new strict mode. If you wrap your app in <React.Strict></React.Strict>
it will give you additional warnings about things you shouldn’t be doing. I’m not teaching you anything that would trip warnings from React.Strict
but it’s good to keep your team in line and not using legacy features or things that will be sooned be deprecated.
React has wonderful dev tools that the core team maintains. They’re available for both Chromium-based browsers and Firefox. They let you do several things like explore your React app like a DOM tree, modify state and props on the fly to test things out, tease out performance problems, and programtically manipulate components. Definitely worth downloading now.
Firefox React Dev Tools
Chrome React Dev Tools
Async & Routing Asynchronous API Requests notes
course async await
SearchParams.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 import React , { useState, useEffect } from 'react' import pet, { ANIMALS } from '@frontendmasters/pet' import useDropdown from './useDropdown' const SearchParams = ( ) => { const [location, setLocation] = useState ('Seattle, WA' ) const [breeds, setBreeds] = useState ([]) const [animal, AnimalDropdown ] = useDropdown ('Animal' , 'dog' , ANIMALS ) const [breed, BreedDropdown , setBreed] = useDropdown ('Breed' , '' , breeds) const [pets, setPets] = useState ([]) async function requestPets ( ) { const { animals } = await pet.animals ({ location, breed, type : animal }) setPets (animals || []) } useEffect (() => { setBreeds ([]) setBreed ('' ) pet.breeds (animal).then (({ breeds } ) => { const breedStrings = breeds.map (({ name } ) => name) setBreeds (breedStrings) }, console .error ) }, [animal, setBreed, setBreeds]) return ( <div className ="search-params" > <form onSubmit ={e => { //add this e.preventDefault() requestPets() }} > <label htmlFor ="location" > Location <input id ="location" value ={location} placeholder ="location" onChange ={e => setLocation(e.target.value)} onBlur={e => setLocation(e.target.value)} /> </label > <AnimalDropdown /> <BreedDropdown /> <button > Submit</button > </form > </div > ) } export default SearchParams
在package.json添加浏览器支持
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 { "name" : "adopt-me" , "version" : "1.0.0" , "description" : "" , "main" : "index.js" , "scripts" : { "dev" : "parcel src/index.html" , "format" : "prettier \"src/**/*.{js,html}\" --write" , "lint" : "eslint \"src/**/*.{js,jsx}\" --quiet" , "test" : "echo \"Error: no test specified\" && exit 1" } , "keywords" : [ ] , "author" : "" , "license" : "ISC" , "devDependencies" : { "babel-eslint" : "^10.0.1" , "eslint" : "^5.16.0" , "eslint-config-prettier" : "^4.3.0" , "eslint-plugin-import" : "^2.17.3" , "eslint-plugin-jsx-a11y" : "^6.2.1" , "eslint-plugin-react" : "^7.13.0" , "eslint-plugin-react-hooks" : "^1.6.0" , "parcel-bundler" : "^1.12.3" , "prettier" : "^1.17.1" } , "dependencies" : { "@frontendmasters/pet" : "^1.0.3" , "react" : "^16.8.6" , "react-dom" : "^16.8.6" } , "browserslist" : [ "last 2 Chrome versions" , "last 2 ChromeAndroid versions" , "last 2 Firefox versions" , "last 2 FirefoxAndroid versions" , "last 2 Safari versions" , "last 2 iOS versions" , "last 2 Edge versions" , "last 2 Opera versions" , "last 2 OperaMobile versions" ] }
Use the Fallback Mock API 之前的数据都是通过互联网请求获取的,下面安装mock API,从本地获取数据
首先安装npm包
给package.json添加如下指令
1 "dev:mock" : "cross-env PET_MOCK=mock npm run dev"
One-Way Data Flow 新建Results
组件,更改SearchParams.js
如下
1 2 3 4 5 import Results from "./Results" ;<Results pets ={pets} /> ;
React的数据会从父组件传递到子组件(One-Way Data Flow),反之并不成立。
新建`Results.js`
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import React from 'react' import Pet from './Pet' const Results = ({ pets } ) => { return ( <div className ="search" > {pets.length === 0 ? ( <h1 > No Pets Found</h1 > ) : ( pets.map(pet => ( <Pet animal ={pet.type} key ={pet.id} name ={pet.name} breed ={pet.breeds.primary} media ={pet.photos} location ={ `${pet.contact.address.city },${ pet.contact.address.state }`} id ={pet.id} /> )) )} </div > ) } export default Results
Pet.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import React from 'react' export default function Pet ({ name, animal, breed, media, location, id } ) { let hero = 'http://placecorgi.com/300/300' if (media.length ) { hero = media[0 ].small } return ( <a href ={ `/details /${id }`} className ="pet" > <div className ="image-container" > <img src ={hero} alt ={name} /> </div > <div className ="info" > <h1 > {name}</h1 > <h2 > {`${animal} - ${breed} - ${location}`}</h2 > </div > </a > ) }
Reach Router Navi-Declarative, asynchronous routing for React.
react-router
reach/router
Details.js
1 2 3 4 5 6 7 8 import React from 'react' const Details = ( ) => { return <h1 > Details</h1 > } export default Details
App.js
1 2 3 4 5 6 7 8 9 import { Router } from "@reach/router" ;import Details from "./Details" ;<Router > <SearchParams path ="/" /> <Details path ="/details/:id" /> </Router > ;
Debugging & Reach Router Link 可以在Details.js
中,使用<pre>
和<code>
标签包裹Json.stringify()
在页面显示变量对象,也可以在DevTools中查看
1 2 3 4 5 6 7 const Details = props => { return ( <pre > <code > {JSON.stringify(props, null, 2)}</code > </pre > ) }
更改app.js
,添加<link>
1 2 3 4 5 6 7 import { Router , Link } from "@reach/router" ;<header > <Link to ="/" > Adopt Me!</Link > </header > ;
Class Components React Class Components
Details.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import React from 'react' import pet from '@frontendmasters/pet' class Details extends React.Component { constructor (props ) { super (props) this .state = { loading : true } } componentDidMount ( ) { pet.animal (this .props .id ).then (({ animal } ) => { this .setState ({ name : animal.name , animal : animal.type , location : `${animal.contact.address.city} , ${ animal.contact.address.state } ` , description : animal.description , media : animal.photos , breed : animal.breeds .primary , loading : false }) }) } render ( ) { return } } export default Details
Rendering the Component
Details.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 import React from 'react' import pet from '@frontendmasters/pet' class Details extends React.Component { constructor (props ) { super (props) this .state = { loading : true } } componentDidMount ( ) { pet .animal (this .props .id ) .then (({ animal } ) => { this .setState ({ name : animal.name , animal : animal.type , location : `${animal.contact.address.city} , ${ animal.contact.address.state } ` , description : animal.description , media : animal.photos , breed : animal.breeds .primary , loading : false }) }) .catch (err => this .setState ({ error : err })) } render ( ) { if (this .state .loading ) { return <h1 > loading … </h1 > } const { animal, breed, location, description, name } = this .state return ( <div className ="details" > <div > <h1 > {name}</h1 > <h2 > {`${animal} — ${breed} — ${location}`}</h2 > <button > Adopt {name}</button > <p > {description}</p > </div > </div > ) } } export default Details
Configuring Babel for Parcel 更具js的class语法,在class component组件中要加入如下代码:
1 2 3 4 5 6 7 8 9 10 class Details extends React.component { constructor (props ) { super (props) this .state = { loading : true } } }
毫无疑问,这会加重我们的心智负担,我们希望将上述代码直接改写如下:
1 2 3 4 5 class Details extends React.component { state = { loading : true } }
直接运行会报错
1 Support for the experimental syntax 'classProperties' isn't currently enabled (5:9)
所以要启用babel和parcel的一些设置。
1 npm install -D babel-eslint @babel/core @babel/preset-env @babel/plugin-proposal-class-properties @babel/preset-react
.babelrc
1 2 3 4 { "presets" : [ "@babel/preset-react" , "@babel/preset-env" ] , "plugins" : [ "@babel/plugin-proposal-class-properties" ] }
在.eslintrc.json
中添加"parser": "babel-eslint",
上述步骤完成后,就可以启用这个新语法了。
Creating an Image Carousel
Carousel.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 import React from 'react' class Carousel extends React.Component { state = { photos : [], active : 0 } static getDerivedStateFromProps ({ media } ) { let photos = ['http://placecorgi.com/600/600' ] if (media.length ) { photos = media.map (({ large } ) => large) } return { photos } } render ( ) { const { photos, active } = this .state return ( <div className ="carousel" > <img src ={photos[active]} alt ="an animal" /> <div className ="carousel-smaller" > {photos.map((photo, index) => ( // eslint-disable-next-line <img key ={photo} onClick ={this.handelIndexClick} data-index ={index} src ={photo} className ={index === active ? 'active ' : ''} alt ="animal thumbnail" /> ))} </div > </div > ) } } export default Carousel
Context Problem 在Carousel.js
中,如果我们直接将handelIndexClick
方法写为如下代码:
1 2 3 4 5 6 7 8 9 10 11 class Carousel extends React.Component { state = { photos : [], active : 0 } handelIndexClick (event ) { this .setState ({ active : event.target .dataset .index }) } }
会有如上两个问题。
所以可以将如上代码改写为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Carousel extends React.Component { constructor (props) { super (props) this .state = { photos : [], active : 0 } this .handelIndexClick = this .handelIndexClick .bind (this ) } handelIndexClick (event ) { this .setState ({ active : +event.target .dataset .index }) } }
我们可以用更为简洁的写法
1 2 3 4 5 6 7 8 9 10 11 class Carousel extends React.Component { state = { photos : [], active : 0 } handelIndexClick = event => { this .setState ({ active : +event.target .dataset .index }) } }
因为箭头函数并不会创造新的执行上下文,而是会继承上级作用域函数的执行上下文,所以这里的this指向了Carousel
所以当传入函数进入子函数或者进行事件监听时,使用箭头函数,来避免this指向为其他对象 。
在React中,componentDidMount
和render
方法的this
指向的执行上下文已经为当前组件。
Carousel Implementation 将Carousel组件添加到Detail页面:
1 2 3 4 5 import Carousel from "./Carousel" ;<Carousel media ={media} /> ;
Error Boundaries 错误边界(Error Boundaries)
1 touch ./src/ErrorBoundary.js
ErrorBoundary.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import React from 'react' import { Link } from '@reach/router' class ErrorBoundary extends React.Component { state = { hasError : false } static getDerivedStateFromError ( ) { return { hasError : true } } componentDidCatch (error, info ) { console .error ('ErrorBoundary caught an error' , error, info) } render ( ) { if (this .state .hasError ) { return ( <h1 > There was an error with this listing. <Link to ="/" > Click here</Link > {' '} to back to the home page or wait five seconds. </h1 > ) } return this .props .children } } export default ErrorBoundary
Error Boundary Middleware Details.js:
1 2 3 4 5 6 7 8 9 10 11 export default function DetailsWithErrorBoundary (props ) { return ( <ErrorBoundary > {/* 使用扩展运算符传递props对象参数 */} <Details {...props } /> </ErrorBoundary > ); }
404 Page Redirect 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import { Link , Redirect } from "@reach/router" ;this .state = { hasError : false , redirect : false };componentDidUpdate ( ) { if (this .state .hasError ) { setTimeout (() => this .setState ({ redirect : true }), 5000 ); } } if (this .state .redirect ) { return <Redirect to ="/" /> ; }
Context React Context Notes
What is context? Context is like state, but instead of being confined to a component, it’s global to your application. It’s application-level state. This is dangerous. Avoid using context until you have to use it. One of React’s primary benefit is it makes the flow of data obvious by being explicit. This can make it cumbersome at times but it’s worth it because your code stays legible and understandable. Things like context obscure it. Context (mostly) replaces Redux. Well, typically. It fills the same need as Redux. I really can’t see why you would need to use both. Use one or the other.
redux
Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。
1 touch ./src/ThemeContext.js
ThemeContext.js
1 2 3 4 5 6 import { createContext } from 'react' const ThemeContext = createContext (['green' , () => {}])export default ThemeContext
App.js
1 2 3 4 5 6 7 8 9 10 11 import React , { useState } from "react" ;import ThemeContext from "./ThemeContext" ;const theme = useState ("darkblue" );<ThemeContext.Provider value ={theme} > […] </ThemeContext >
Context with Hooks
SearchParams.js
1 2 3 4 5 6 7 8 import ThemeContext from "./ThemeContext" ;const [theme] = useContext (ThemeContext );<button style ={{ backgroundColor: theme }}> Submit</button > ;
Context with Classes
Details.js
1 2 3 4 5 6 7 8 9 10 11 import ThemeContext from "./ThemeContext" ;<ThemeContext.Consumer > {([theme]) => ( <button style ={{ backgroundColor: theme }} onClick ={this.toggleModal} > Adopt {name} </button > )} </ThemeContext.Consumer > ;
Persisting State with Context Hooks
SearchParams.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const [theme, setTheme] = useContext (ThemeContext );<label htmlFor ="location" > Theme <select value ={theme} onChange ={e => setTheme(e.target.value)} onBlur={e => setTheme(e.target.value)} > <option value ="peru" > Peru</option > <option value ="darkblue" > Dark Blue</option > <option value ="chartreuse" > Chartreuse</option > <option value ="mediumorchid" > Medium Orchid</option > </select > </label > ;
同时要修改Pet.js
,把<a>
链接更改为<link>
,不然会重新打开网页,无法保存状态。
Portals Modal Dialog with Portals 在index.html中添加一个单独的挂载点
创建一个Modal.js
Modal.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import React , { useEffect, useRef } from 'react' import { createPortal } from 'react-dom' const Modal = ({ children } ) => { const elRef = useRef (null ) if (!elRef.current ) { elRef.current = document .createElement ('div' ) } useEffect (() => { const modalRoot = document .getElementById ('modal' ) modalRoot.appendChild (elRef.current ) return () => modalRoot.removeChild (elRef.current ) }, []) return createPortal (<div > {children}</div > , elRef.current ) } export default Modal
Displaying the Modal
修改Details.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import { navigate } from "@reach/router" ;import Modal from "./Modal" ;state = { loading : true , showModal : false }; url : animal.url , (toggleModal = () => this .setState ({ showModal : !this .state .showModal })); adopt = () => navigate (this .state .url ); const { media, animal, breed, location, description, name, url, showModal } = this .state ; { showModal ? ( <Modal > <div > <h1 > Would you like to adopt {name}?</h1 > <div className ="buttons" > <button onClick ={this.adopt} > Yes</button > <button onClick ={this.toggleModal} > No</button > </div > </div > </Modal > ) : null ; }