Complete-intro-to-React-v5

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:

  1. what kind of tag is it (h1 or div or a composite component)
  2. All the attribute that you want to give the component ({ id: ?? })
  3. 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
// 创建一个Pet组件
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

Tools

npm & Generating a package.json File

npm

1
npm init -y

Prettier

1
npm i -D 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

1
touch .prettierrc
.prettierrc
1
2
3
4
{
"semi": false,
"singleQuote": true
}

https://prettier.io/

ESLint Setup

1
npm i -D eslint eslint-config-prettier
1
touch .eslintrc.json

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

1
touch .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",
1
npm run dev

Installing React & ReactDOM

1
npm i react react-dom
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 React.createElement('div', {}, [
// React.createElement('h1', {}, name),
// React.createElement('h2', {}, animal),
// React.createElement('h2', {}, breed)
// ])

// 下面的代码会被babel编译为上面的代码

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 React.createElement('div', {}, [
// React.createElement('h1', {}, '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: 'Mix' })
// ])

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(/* React.createElement(App) */ <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,给`
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, setAnimal] = useState('dog')
// const [breed, setBreed] = 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的第一个参数,也就是更新breedbreeds的函数。在执行完毕后,会监听animal,setBreedsetBreeds,也就是useEffect第二个参数的数组中的每一项的数据状态有没有改变,只有当参数数组中的数据状态改变时,才会重新调用第一个参数函数更新breedbreeds的数据。

Run Only Once

如果只想在首次加载时执行useEffect并只执行一次,可以将useEffect第二个参数设置为一个空数组。如果想在每次重渲染后执行,只设置一个参数即可(此时会一直调用api获取数据,不要这样做)。

Dev Tools

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 Browser Dev Tools

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([]) // add this

// add this
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包

1
npm i -D cross-env

给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
// at top
import Results from "./Results";

// under </form>, still inside the div
<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

Reformatting the Pet Component

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

1
touch ./src/Details.js
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
// at top
import { Router } from "@reach/router";
import Details from "./Details";

// replace <Results />
<Router>
<SearchParams path="/" />
<Details path="/details/:id" />
</Router>;

可以在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 Link too
import { Router, Link } from "@reach/router";

// replace h1
<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
}
}
// 不能在class中使用hooks
componentDidMount() {
pet.animal(this.props.id).then(({ animal }) => { // 为了保持this的指向,这里最好使用箭头函数
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() {
//每个class component都必须有一个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
}
}
// 不能在class中使用hooks
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() {
//每个class component都必须有一个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
1
touch ./.babelrc
.babelrc
1
2
3
4
{
"presets": ["@babel/preset-react", "@babel/preset-env"],
"plugins": ["@babel/plugin-proposal-class-properties"]
}

.eslintrc.json中添加"parser": "babel-eslint",

上述步骤完成后,就可以启用这个新语法了。

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({ // 这里的this指向的不是Carousel组件,而是其他的什么东西
active: event.target.dataset.index // 这里Dom的dataset返回的属性会是字符串,需要转换为数字
})
}
}

会有如上两个问题。

所以可以将如上代码改写为

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) // 显式绑定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中,componentDidMountrender方法的this指向的执行上下文已经为当前组件。

将Carousel组件添加到Detail页面:

1
2
3
4
5
// import at top
import Carousel from "./Carousel";

// first component inside div.details
<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
// mostly code from https://zh-hans.reactjs.org/docs/error-boundaries.html

import React from 'react'
import { Link } from '@reach/router'

class ErrorBoundary extends React.Component {
state = {
hasError: false
}

static getDerivedStateFromError() {
// Update state so the next render will show the fallback UI.
return { hasError: true }
}

componentDidCatch(error, info) {
// You can also log the error to an error reporting service
console.error('ErrorBoundary caught an error', error, info)
}

render() {
if (this.state.hasError) {
// You can render any custom fallback UI
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
// add import

// replace export
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
// top
import { Link, Redirect } from "@reach/router";

// add redirect
this.state = { hasError: false, redirect: false };

// under componentDidCatch
componentDidUpdate() {
if (this.state.hasError) {
setTimeout(() => this.setState({ redirect: true }), 5000);
}
}

// first thing inside render
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 useState and ThemeContext
import React, { useState } from "react";
import ThemeContext from "./ThemeContext";

// top of App function body
const theme = useState("darkblue");

// wrap the rest of the app
<ThemeContext.Provider value={theme}>
[…]
</ThemeContext>

Context with Hooks

SearchParams.js
1
2
3
4
5
6
7
8
// import at top
import ThemeContext from "./ThemeContext";

// top of SearchParams function body
const [theme] = useContext(ThemeContext);

// replace button
<button style={{ backgroundColor: theme }}>Submit</button>;

Context with Classes

Details.js
1
2
3
4
5
6
7
8
9
10
11
// import
import ThemeContext from "./ThemeContext";

// replace button
<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
// also grab setTheme
const [theme, setTheme] = useContext(ThemeContext);

// below BreedDropdown
<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

在index.html中添加一个单独的挂载点

1
2
<!-- above #root -->
<div id="modal"></div>

创建一个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
// at the top
import { navigate } from "@reach/router";
import Modal from "./Modal";

// add showModal
state = { loading: true, showModal: false };

// add setState inside componentDidMount
url: animal.url,
// above render
(toggleModal = () => this.setState({ showModal: !this.state.showModal }));
adopt = () => navigate(this.state.url);

// add showModal
const {
media,
animal,
breed,
location,
description,
name,
url,
showModal
} = this.state;

// below description
{
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;
}