React笔记
基础
语法基础与规范
JSX
注意事项。JSX
必须有一个根节点,如果没有,可以用幽灵节点<></>
代替。- 所有标签必须闭合,成对闭合或者自闭和都可以。
JSX
采用小驼峰命名法,对于class
要转化成className
,for
需要转化成htmlFor
,以防止js
关键字冲突。JSX
支持换行,如果需要换行需要加上()
,防止bug
出现。- 注释在模板中的书写方式:
{/* 苏苏苏 */}
。1
2
3
4
5
6
7
8
9
10
11import "./app.css"; const showTitle = true; function App() { return ( <div className="App"> {/* 苏苏苏 */} <div className={showTitle ? "title" : ""}>this is a div</div> </div> ); } export default App;
jsx
的列表渲染1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// 来个列表 const songs = [ { id: 1, name: "痴心绝对" }, { id: 2, name: "像我这样的人" }, { id: 3, name: "南山南" }, ]; function App() { return ( <div className="App"> <ul> {songs.map((item, index) => ( <li key={index}>{item.name}</li> ))} </ul> </div> ); } export default App;
React
函数组件- 函数组件首字母必须大写。
- 函数组件必须有返回值,若不需要渲染内容则返回
null
。 - 使用函数组件可以自闭和也可以成对闭合。
注意在
React
中的样式绑定只能使用驼峰规则,这点和Vue
不一样,vue
可以使用kebab-cased
命名方式。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// 这种写法才是正确的 export function TodoList() { return ( <ul style={{ backgroundColor: "black", color: "pink", }} > <li>Improve the videophone</li> <li>Prepare aeronautics lectures</li> <li>Work on the alcohol-fuelled engine</li> </ul> ); }
1
2
3
4
5
6
7
8
9
10
11
12<script setup> import { ref, reactive } from "vue"; // react 不能使用这样的方式,这样的方式会发出警告 const styleObject = reactive({ color: "red", "font-size": "20px", }); </script> <template> <input v-model="msg" :style="styleObject" /> </template>
React
的纯函数概念。- 一个组件必须是纯粹的,就意味着:
- 只负责自己的任务。 它不会更改在该函数调用前就已存在的对象或变量。
- 输入相同,则输出相同。 给定相同的输入,组件应该总是返回相同的
JSX
。
- 渲染随时可能发生,因此组件不应依赖于彼此的渲染顺序。
- 你不应该改变任何用于组件渲染的输入。这包括
props
、state
和context
。通过 “设置”state
来更新界面,而不要改变预先存在的对象。 - 努力在你返回的
JSX
中表达你的组件逻辑。当你需要“改变事物”时,你通常希望在事件处理程序中进行。作为最后的手段,你可以使用useEffect
。 - 编写纯函数需要一些练习,但它充分释放了
React
范式的能力。
- 一个组件必须是纯粹的,就意味着:
状态(state)
useState
特征- 每个渲染(以及其中的函数)始终“看到”的是
React
提供给这个 渲染的state
快照。 - 过去创建的事件处理函数拥有的是创建它们的那次渲染中的
state
值。 setState
推入队列后遍历1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18export default function Counter() { const [number, setNumber] = useState(0); return ( <> <h1>{number}</h1> <button onClick={() => { setNumber((n) => n + 1); setNumber((n) => n + 1); setNumber((n) => n + 1); }} > +3 </button> </> ); }
当你在下次渲染期间调用
useState
时,React
会遍历队列。之前的number state
的值是0
,所以这就是React
作为参数n
传递给第一个更新函数的值。然后React
会获取你上一个更新函数的返回值,并将其作为n
传递给下一个更新函数,以此类推…注意永远不要直接修改
state
!!!
- 每个渲染(以及其中的函数)始终“看到”的是
Immer
简化react
对象的修改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
65import { useImmer } from "use-immer"; export default function Form() { const [person, updatePerson] = useImmer({ name: "Niki de Saint Phalle", artwork: { title: "Blue Nana", city: "Hamburg", image: "https://i.imgur.com/Sd1AgUOm.jpg", }, }); function handleNameChange(e) { updatePerson((draft) => { draft.name = e.target.value; }); } function handleTitleChange(e) { updatePerson((draft) => { draft.artwork.title = e.target.value; }); } function handleCityChange(e) { updatePerson((draft) => { draft.artwork.city = e.target.value; }); } function handleImageChange(e) { updatePerson((draft) => { draft.artwork.image = e.target.value; }); } return ( <> <label> Name: <input value={person.name} onChange={handleNameChange} /> </label> <label> Title: <input value={person.artwork.title} onChange={handleTitleChange} /> </label> <label> City: <input value={person.artwork.city} onChange={handleCityChange} /> </label> <label> Image: <input value={person.artwork.image} onChange={handleImageChange} /> </label> <p> <i>{person.artwork.title}</i> {" by "} {person.name} <br /> (located in {person.artwork.city}) </p> <img src={person.artwork.image} alt={person.artwork.title} /> </> ); }
使用
Immer
编写数组更简单的逻辑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
59import { useState } from "react"; import { useImmer } from "use-immer"; let nextId = 3; const initialList = [ { id: 0, title: "Big Bellies", seen: false }, { id: 1, title: "Lunar Landscape", seen: false }, { id: 2, title: "Terracotta Army", seen: true }, ]; export default function BucketList() { const [myList, updateMyList] = useImmer(initialList); const [yourList, updateYourList] = useImmer(initialList); function handleToggleMyList(id, nextSeen) { updateMyList((draft) => { const artwork = draft.find((a) => a.id === id); artwork.seen = nextSeen; }); } function handleToggleYourList(artworkId, nextSeen) { updateYourList((draft) => { const artwork = draft.find((a) => a.id === artworkId); artwork.seen = nextSeen; }); } return ( <> <h1>艺术愿望清单</h1> <h2>我想看的艺术清单:</h2> <ItemList artworks={myList} onToggle={handleToggleMyList} /> <h2>你想看的艺术清单:</h2> <ItemList artworks={yourList} onToggle={handleToggleYourList} /> </> ); } function ItemList({ artworks, onToggle }) { return ( <ul> {artworks.map((artwork) => ( <li key={artwork.id}> <label> <input type="checkbox" checked={artwork.seen} onChange={(e) => { onToggle(artwork.id, e.target.checked); }} /> {artwork.title} </label> </li> ))} </ul> ); }
State
不依赖于特定的函数调用或在代码中的位置,它的作用域“只限于”屏幕上的某块特定区域。你渲染了两个<Gallery />
组件,所以它们的state
是分别存储的。
还要注意Page
组件“不知道”关于Gallery state
的任何信息,甚至不知道它是否有任何state
。与props
不同,state
完全私有于声明它的组件。父组件无法更改它。这使你可以向任何组件添加或删除state
,而不会影响其他组件。1
2
3
4
5
6
7
8
9
10import Gallery from "./Gallery.js"; export default function Page() { return ( <div className="Page"> <Gallery /> <Gallery /> </div> ); }
不要在
state
中镜像props
由于state
只会在第一次渲染期间初始化,后续props
发生更新后,这样的代码不会发生更新。1
2
3function Message({ messageColor }) { const [color, setColor] = useState(messageColor); }
只有当你 想要 忽略特定
props
属性的所有更新时,将props
“镜像”到state
才有意义。按照惯例,prop
名称以initial
或default
开头,以阐明该prop
的新值将被忽略:1
2
3
4
5function Message({ initialColor }) { // 这个 `color` state 变量用于保存 `initialColor` 的 **初始值**。 // 对于 `initialColor` 属性的进一步更改将被忽略。 const [color, setColor] = useState(initialColor); }
React
中的Hook
只能在组件或者自己的Hook
的顶层调用,而不能嵌套调用。原因为Hooks
的执行需要严格按照其调用顺序,Hooks
的调用顺序是一致的,从而避免了状态的混乱或者副作用的错误。- 注意
React
中的setState
方法中的更改只会在下一次渲染时才会生效。 useState
内部原理的简单实现。index.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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188let componentHooks = []; let currentHookIndex = 0; // useState 在 React 中是如何工作的(简化版) function useState(initialState) { let pair = componentHooks[currentHookIndex]; if (pair) { // 这不是第一次渲染 // 所以 state pair 已经存在 // 将其返回并为下一次 hook 的调用做准备 currentHookIndex++; return pair; } // 这是我们第一次进行渲染 // 所以新建一个 state pair 然后存储它 pair = [initialState, setState]; function setState(nextState) { // 当用户发起 state 的变更, // 把新的值放入 pair 中 pair[0] = nextState; updateDOM(); } // 存储这个 pair 用于将来的渲染 // 并且为下一次 hook 的调用做准备 componentHooks[currentHookIndex] = pair; currentHookIndex++; return pair; } function Gallery() { // 每次调用 useState() 都会得到新的 pair const [index, setIndex] = useState(0); const [showMore, setShowMore] = useState(false); function handleNextClick() { setIndex(index + 1); } function handleMoreClick() { setShowMore(!showMore); } let sculpture = sculptureList[index]; // 这个例子没有使用 React,所以 // 返回一个对象而不是 JSX return { onNextClick: handleNextClick, onMoreClick: handleMoreClick, header: `${sculpture.name} by ${sculpture.artist}`, counter: `${index + 1} of ${sculptureList.length}`, more: `${showMore ? "Hide" : "Show"} details`, description: showMore ? sculpture.description : null, imageSrc: sculpture.url, imageAlt: sculpture.alt, }; } function updateDOM() { // 在渲染组件之前 // 重置当前 Hook 的下标 currentHookIndex = 0; let output = Gallery(); // 更新 DOM 以匹配输出结果 // 这部分工作由 React 为你完成 nextButton.onclick = output.onNextClick; header.textContent = output.header; moreButton.onclick = output.onMoreClick; moreButton.textContent = output.more; image.src = output.imageSrc; image.alt = output.imageAlt; if (output.description !== null) { description.textContent = output.description; description.style.display = ""; } else { description.style.display = "none"; } } let nextButton = document.getElementById("nextButton"); let header = document.getElementById("header"); let moreButton = document.getElementById("moreButton"); let description = document.getElementById("description"); let image = document.getElementById("image"); let sculptureList = [ { name: "Homenaje a la Neurocirugía", artist: "Marta Colvin Andrade", description: "Although Colvin is predominantly known for abstract themes that allude to pre-Hispanic symbols, this gigantic sculpture, an homage to neurosurgery, is one of her most recognizable public art pieces.", url: "https://i.imgur.com/Mx7dA2Y.jpg", alt: "A bronze statue of two crossed hands delicately holding a human brain in their fingertips.", }, { name: "Floralis Genérica", artist: "Eduardo Catalano", description: "This enormous (75 ft. or 23m) silver flower is located in Buenos Aires. It is designed to move, closing its petals in the evening or when strong winds blow and opening them in the morning.", url: "https://i.imgur.com/ZF6s192m.jpg", alt: "A gigantic metallic flower sculpture with reflective mirror-like petals and strong stamens.", }, { name: "Eternal Presence", artist: "John Woodrow Wilson", description: 'Wilson was known for his preoccupation with equality, social justice, as well as the essential and spiritual qualities of humankind. This massive (7ft. or 2,13m) bronze represents what he described as "a symbolic Black presence infused with a sense of universal humanity."', url: "https://i.imgur.com/aTtVpES.jpg", alt: "The sculpture depicting a human head seems ever-present and solemn. It radiates calm and serenity.", }, { name: "Moai", artist: "Unknown Artist", description: "Located on the Easter Island, there are 1,000 moai, or extant monumental statues, created by the early Rapa Nui people, which some believe represented deified ancestors.", url: "https://i.imgur.com/RCwLEoQm.jpg", alt: "Three monumental stone busts with the heads that are disproportionately large with somber faces.", }, { name: "Blue Nana", artist: "Niki de Saint Phalle", description: "The Nanas are triumphant creatures, symbols of femininity and maternity. Initially, Saint Phalle used fabric and found objects for the Nanas, and later on introduced polyester to achieve a more vibrant effect.", url: "https://i.imgur.com/Sd1AgUOm.jpg", alt: "A large mosaic sculpture of a whimsical dancing female figure in a colorful costume emanating joy.", }, { name: "Ultimate Form", artist: "Barbara Hepworth", description: "This abstract bronze sculpture is a part of The Family of Man series located at Yorkshire Sculpture Park. Hepworth chose not to create literal representations of the world but developed abstract forms inspired by people and landscapes.", url: "https://i.imgur.com/2heNQDcm.jpg", alt: "A tall sculpture made of three elements stacked on each other reminding of a human figure.", }, { name: "Cavaliere", artist: "Lamidi Olonade Fakeye", description: "Descended from four generations of woodcarvers, Fakeye's work blended traditional and contemporary Yoruba themes.", url: "https://i.imgur.com/wIdGuZwm.png", alt: "An intricate wood sculpture of a warrior with a focused face on a horse adorned with patterns.", }, { name: "Big Bellies", artist: "Alina Szapocznikow", description: "Szapocznikow is known for her sculptures of the fragmented body as a metaphor for the fragility and impermanence of youth and beauty. This sculpture depicts two very realistic large bellies stacked on top of each other, each around five feet (1,5m) tall.", url: "https://i.imgur.com/AlHTAdDm.jpg", alt: "The sculpture reminds a cascade of folds, quite different from bellies in classical sculptures.", }, { name: "Terracotta Army", artist: "Unknown Artist", description: "The Terracotta Army is a collection of terracotta sculptures depicting the armies of Qin Shi Huang, the first Emperor of China. The army consisted of more than 8,000 soldiers, 130 chariots with 520 horses, and 150 cavalry horses.", url: "https://i.imgur.com/HMFmH6m.jpg", alt: "12 terracotta sculptures of solemn warriors, each with a unique facial expression and armor.", }, { name: "Lunar Landscape", artist: "Louise Nevelson", description: "Nevelson was known for scavenging objects from New York City debris, which she would later assemble into monumental constructions. In this one, she used disparate parts like a bedpost, juggling pin, and seat fragment, nailing and gluing them into boxes that reflect the influence of Cubism’s geometric abstraction of space and form.", url: "https://i.imgur.com/rN7hY6om.jpg", alt: "A black matte sculpture where the individual elements are initially indistinguishable.", }, { name: "Aureole", artist: "Ranjani Shettar", description: 'Shettar merges the traditional and the modern, the natural and the industrial. Her art focuses on the relationship between man and nature. Her work was described as compelling both abstractly and figuratively, gravity defying, and a "fine synthesis of unlikely materials."', url: "https://i.imgur.com/okTpbHhm.jpg", alt: "A pale wire-like sculpture mounted on concrete wall and descending on the floor. It appears light.", }, { name: "Hippos", artist: "Taipei Zoo", description: "The Taipei Zoo commissioned a Hippo Square featuring submerged hippos at play.", url: "https://i.imgur.com/6o5Vuyu.jpg", alt: "A group of bronze hippo sculptures emerging from the sett sidewalk as if they were swimming.", }, ]; // 使 UI 匹配当前 state updateDOM();
index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20<button id="nextButton">Next</button> <h3 id="header"></h3> <button id="moreButton"></button> <p id="description"></p> <img id="image" /> <style> * { box-sizing: border-box; } body { font-family: sans-serif; margin: 20px; padding: 0; } button { display: block; margin-bottom: 10px; } </style>
相同位置的相同组件会使得 state 被保留下来:记住 对
React
来说重要的是组件在UI
树中的位置,而不是在JSX
中的位置!
重置state
有两种方法:- 在不同的位置渲染组件
- 添加唯一的
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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61import { useState } from "react"; export default function App() { const [isFancy, setIsFancy] = useState(false); if (isFancy) { return ( <div> <Counter isFancy={true} /> <label> <input type="checkbox" checked={isFancy} onChange={(e) => { setIsFancy(e.target.checked); }} /> 使用好看的样式 </label> </div> ); } return ( <div> <Counter isFancy={false} /> <label> <input type="checkbox" checked={isFancy} onChange={(e) => { setIsFancy(e.target.checked); }} /> 使用好看的样式 </label> </div> ); } function Counter({ isFancy }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = "counter"; if (hover) { className += " hover"; } if (isFancy) { className += " fancy"; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}>加一</button> </div> ); }
Props
在
React
中使用props
传递参数对于
React
中的函数组件,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
33
34
35
36
37
38
39function Avatar({ person, size = 20 }) { return ( <img className="avatar" src={getImageUrl(person)} alt={person.name} width={size} height={size} /> ); } export default function Profile() { return ( <div> <Avatar size={100} person={{ name: "Katsuko Saruhashi", imageId: "YfeOqp2", }} /> <Avatar size={80} person={{ name: "Aklilu Lemma", imageId: "OKS67lh", }} /> <Avatar size={50} person={{ name: "Lin Lanying", imageId: "1bX5QH6", }} /> </div> ); }
React
中的默认参数与js
中函数的默认参数一致,但位置随意。在
React
中通过扩展运算符传递参数1
2
3
4
5
6
7function Profile(props) { return ( <div className="card"> <Avatar {...props} /> </div> ); }
你可以使用
<Avatar {...props} />
JSX
展开语法转发所有props
,但不要过度使用它!React
中的列表渲染1
const listItems = people.map((person, key) => <li key={key}>{person}</li>);
对于
React
和Vue
中的组件中的key
,key
会作为组件中的一个特殊的属性而不会作为props
传入。React
中props
的children
属性如果有多个同级的JSX
元素传递过来,则children
是数组的形式。
注意事项
React
使用Fragment
标签来充当幽灵标签,用来添加key
值,这时候不能使用<></>
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22import { Fragment } from "react"; const poem = { lines: [ "I write, erase, rewrite", "Erase again, and then", "A poppy blooms.", ], }; export default function Poem() { return ( <article> {poem.lines.map((line, i) => ( <Fragment key={i}> {i > 0 && <hr />} <p>{line}</p> </Fragment> ))} </article> ); }
React
只能渲染字符串,数字,布尔值,null
,undefined
,数组或者React
元素在
React
中绑定一个事件的捕获模式,即在事件名称末尾添加Capture
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16{ /* 每个事件分三个阶段传播: 1. 它向下传播,调用所有的 onClickCapture 处理函数。 2. 它执行被点击元素的 onClick 处理函数。 3. 它向上传播,调用所有的 onClick 处理函数。 捕获事件对于路由或数据分析之类的代码很有用,但你可能不会在应用程序代码中使用它们。 */ } <div onClickCapture={() => { /* 这会首先执行 */ }} > <button onClick={(e) => e.stopPropagation()} /> <button onClick={(e) => e.stopPropagation()} /> </div>;
React
如果需要触发组件的re-render
,除了自身的setState
方法,如果父组件的变量作为props
传递给子组件,那么如果该变量发生变化也会引起子组件的re-render
。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
42import { useState } from "react"; import { foods, filterItems } from "./data.js"; export default function FilterableList() { const [query, setQuery] = useState(""); const results = filterItems(foods, query); function handleChange(e) { setQuery(e.target.value); } return ( <> <SearchBar query={query} onChange={handleChange} /> <hr /> <List items={results} /> </> ); } function SearchBar({ query, onChange }) { return ( <label> 搜索: <input value={query} onChange={onChange} /> </label> ); } function List({ items }) { return ( <table> <tbody> {items.map((food) => ( <tr key={food.id}> <td>{food.name}</td> <td>{food.description}</td> </tr> ))} </tbody> </table> ); }
井字棋游戏示例代码
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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123import { useState } from "react"; function Square({ value, onSquareClick }) { return ( <button className="square" onClick={onSquareClick}> {value} </button> ); } function Board({ xIsNext, squares, onPlay }) { function handleClick(i) { if (calculateWinner(squares) || squares[i]) { return; } const nextSquares = squares.slice(); if (xIsNext) { nextSquares[i] = "X"; } else { nextSquares[i] = "O"; } onPlay(nextSquares); } const winner = calculateWinner(squares); let status; if (winner) { status = "Winner: " + winner; } else { status = "Next player: " + (xIsNext ? "X" : "O"); } return ( <> <div className="status">{status}</div> <div className="board-row"> <Square value={squares[0]} onSquareClick={() => handleClick(0)} /> <Square value={squares[1]} onSquareClick={() => handleClick(1)} /> <Square value={squares[2]} onSquareClick={() => handleClick(2)} /> </div> <div className="board-row"> <Square value={squares[3]} onSquareClick={() => handleClick(3)} /> <Square value={squares[4]} onSquareClick={() => handleClick(4)} /> <Square value={squares[5]} onSquareClick={() => handleClick(5)} /> </div> <div className="board-row"> <Square value={squares[6]} onSquareClick={() => handleClick(6)} /> <Square value={squares[7]} onSquareClick={() => handleClick(7)} /> <Square value={squares[8]} onSquareClick={() => handleClick(8)} /> </div> </> ); } export default function Game() { const [history, setHistory] = useState([Array(9).fill(null)]); const [currentMove, setCurrentMove] = useState(0); const xIsNext = currentMove % 2 === 0; const currentSquares = history[currentMove]; function handlePlay(nextSquares) { const nextHistory = [...history.slice(0, currentMove + 1), nextSquares]; setHistory(nextHistory); setCurrentMove(nextHistory.length - 1); } function jumpTo(nextMove) { setCurrentMove(nextMove); } const moves = history.map((squares, move) => { let description; if (move > 0) { description = "Go to move #" + move; } else { description = "Go to game start"; } return ( <li key={move}> <button onClick={() => jumpTo(move)}>{description}</button> </li> ); }); return ( <div className="game"> <div className="game-board"> <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} /> </div> <div className="game-info"> <ol>{moves}</ol> </div> </div> ); } function calculateWinner(squares) { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6], ]; for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i]; if ( squares[a] && squares[a] === squares[b] && squares[a] === squares[c] ) { return squares[a]; } } return null; }
React Router
创建
React Router
的基本过程1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17import * as React from "react"; import * as ReactDOM from "react-dom/client"; import { createBrowserRouter, RouterProvider } from "react-router-dom"; import "./index.css"; const router = createBrowserRouter([ { path: "/", element: <div>Hello world!</div>, }, ]); ReactDOM.createRoot(document.getElementById("root")).render( <React.StrictMode> <RouterProvider router={router} /> </React.StrictMode> );
React
中子路由的创建,使用<Outlet />
渲染路由相当于vue
中的<RouterView />
。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
26const router = createBrowserRouter([ { path: "/", element: <Root />, errorElement: <ErrorPage />, children: [ { path: "contacts/:contactId", element: <Contact />, }, ], }, ]); import { Outlet } from "react-router-dom"; export default function Root() { return ( <> {/* all the other elements */} <div id="detail"> <Outlet /> </div> </> ); }
React Router
中通过向路由配置的loader
配置项传递一个
具有返回值的函数,然后通过useLoaderData
函数进行数据的获取。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20/* other imports */ import Root, { loader as rootLoader } from "./routes/root"; const router = createBrowserRouter([ { path: "/", element: <Root />, errorElement: <ErrorPage />, loader: rootLoader, children: [ { path: "contacts/:contactId", element: <Contact />, }, ], }, ]); // other file import { useLoaderData } from "react-router-dom"; const { contacts } = useLoaderData();
React Router
会截取页面中表单提交的行为,并将其转发到目前路由的action
当中。edit.tsx
1
2
3
4
5
6
7
8
9
10
11import { Form, useLoaderData, redirect } from "react-router-dom"; import { updateContact } from "../contacts"; export async function action({ request, params }) { const formData = await request.formData(); const updates = Object.fromEntries(formData); await updateContact(params.contactId, updates); return redirect(`/contacts/${params.contactId}`); } /* existing code */
main.tsx
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/* existing code */ import EditContact, { action as editAction } from "./routes/edit"; const router = createBrowserRouter([ { path: "/", element: <Root />, errorElement: <ErrorPage />, loader: rootLoader, action: rootAction, children: [ { path: "contacts/:contactId", element: <Contact />, loader: contactLoader, }, { path: "contacts/:contactId/edit", element: <EditContact />, loader: contactLoader, action: editAction, }, ], }, ]); /* existing code */
使用
React Router
中的JSX Routes
。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
34import { createRoutesFromElements, createBrowserRouter, Route, } from "react-router-dom"; const router = createBrowserRouter( createRoutesFromElements( <Route path="/" element={<Root />} loader={rootLoader} action={rootAction} errorElement={<ErrorPage />} > <Route errorElement={<ErrorPage />}> <Route index element={<Index />} /> <Route path="contacts/:contactId" element={<Contact />} loader={contactLoader} action={contactAction} /> <Route path="contacts/:contactId/edit" element={<EditContact />} loader={contactLoader} action={editAction} /> <Route path="contacts/:contactId/destroy" action={destroyAction} /> </Route> </Route> ) );
React Router
集中式路由配置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
48import { BrowserRouter, Routes, Route, useRoutes } from "react-router-dom"; import Layout from "./pages/Layout"; import Board from "./pages/Board"; import Article from "./pages/Article"; import NotFound from "./pages/NotFound"; // 1. 准备一个路由数组 数组中定义所有的路由对应关系 const routesList = [ { path: "/", element: <Layout />, children: [ { element: <Board />, index: true, // index设置为true 变成默认的二级路由 }, { path: "article", element: <Article />, }, ], }, // 增加n个路由对应关系 { path: "*", element: <NotFound />, }, ]; // 2. 使用useRoutes方法传入routesList生成Routes组件 function WrapperRoutes() { let element = useRoutes(routesList); return element; } function App() { return ( <div className="App"> <BrowserRouter> {/* 3. 替换之前的Routes组件 */} <WrapperRoutes /> </BrowserRouter> </div> ); } export default App;
react router
中jsx
方式的嵌套路由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 { BrowserRouter, Routes, Route, Link } from "react-router-dom"; // 准备俩个路由组件 const Home = () => <div>this is home</div>; const About = () => <div>this is about</div>; function App() { return ( <div className="App"> {/* 按照规则配置路由 */} <BrowserRouter> <Link to="/">首页</Link> <Link to="/about">关于</Link> <Routes> <Route path="/" element={<Home />}></Route> <Route path="/about" element={<About />}></Route> </Routes> </BrowserRouter> </div> ); } export default App;
React
路由懒加载以及回调显示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
52import { Routes, Route } from "react-router-dom"; import { HistoryRouter, history } from "./utils/history"; import { AuthRoute } from "./components/AuthRoute"; // 导入必要组件 import { lazy, Suspense } from "react"; // 按需导入路由组件 const Login = lazy(() => import("./pages/Login")); const Layout = lazy(() => import("./pages/Layout")); const Home = lazy(() => import("./pages/Home")); const Article = lazy(() => import("./pages/Article")); const Publish = lazy(() => import("./pages/Publish")); function App() { return ( <HistoryRouter history={history}> <Suspense fallback={ <div style={{ textAlign: "center", marginTop: 200, }} > loading... </div> } > <Routes> {/* 需要鉴权的路由 */} <Route path="/" element={ <AuthRoute> <Layout /> </AuthRoute> } > {/* 二级路由默认页面 */} <Route index element={<Home />} /> <Route path="article" element={<Article />} /> <Route path="publish" element={<Publish />} /> </Route> {/* 不需要鉴权的路由 */} <Route path="/login" element={<Login />} /> </Routes> </Suspense> </HistoryRouter> ); } export default App;
vite
配置路径别名的方案。- 安装
@types/node
库,便于后续文件的引用。 打开
vite.config.ts
进行路径别名的配置,使vite
能够识别@
。1
2
3
4
5
6
7
8
9
10
11
12
13
14import { fileURLToPath, URL } from "node:url"; import { defineConfig } from "vite"; import react from "@vitejs/plugin-react-swc"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], resolve: { alias: { "@": fileURLToPath(new URL("./src", import.meta.url)), }, }, });
在
tsconfig.json
中进行配置,使得vscode
能使用@
进行智能提示。1
2
3
4
5
6
7
8
9{ "compilerOptions": { "composite": true, "baseUrl": ".", "paths": { "@/*": ["./src/*"] } } }
- 安装
Mobx
mobx
模块化的基本用法。定义单独的
store
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// login.ts import { makeAutoObservable } from "mobx"; import request from "@/utils/request"; import { clearToken, getToken, setToken } from "@/utils/token"; class LoginStore { token = getToken() || ""; constructor() { makeAutoObservable(this); } login = async (data: { mobile: string; code: string }) => { const res = await request.post<any, LoginResData>( "/authorizations", data ); this.token = res.data.token; setToken(res.data.token); }; logout = async () => { this.token = ""; clearToken(); }; } export default LoginStore; // user.ts import { makeAutoObservable } from "mobx"; import request from "@/utils/request"; class UserStore { userInfo = {}; constructor() { makeAutoObservable(this); } getUserInfo = async () => { const res = await request.get("/user/profile"); this.userInfo = res.data; }; } export default UserStore;
在
index.ts
中统一进行注入1
2
3
4
5
6
7
8
9
10
11
12
13
14import React from "react"; import LoginStore from "./modules/login"; import UserStore from "./modules/user"; class RootStore { loginStore: LoginStore; userStore: UserStore; constructor() { this.loginStore = new LoginStore(); this.userStore = new UserStore(); } } const StoresContext = React.createContext(new RootStore()); export const useStore = () => React.useContext(StoresContext);
使用
store
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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85import { Layout as AntLayout, Menu, Popconfirm, message } from "antd"; import { HomeOutlined, DiffOutlined, EditOutlined, LogoutOutlined, } from "@ant-design/icons"; import { Link, Outlet, useLocation, useNavigate } from "react-router-dom"; import { observer } from "mobx-react-lite"; import "./index.scss"; import { useEffect } from "react"; import { useStore } from "@/stores"; const { Header, Sider } = AntLayout; const Layout = observer(() => { const location = useLocation(); const selectedKey = location.pathname; // load userinfo const { userStore, loginStore } = useStore(); useEffect(() => { try { userStore.getUserInfo(); } catch (error: any) { message.error(error.response?.data?.message || "获取用户信息失败"); } }, [userStore]); // logout function const navigate = useNavigate(); const handleLogout = () => { loginStore.logout(); navigate("/login"); message.success("退出登录成功"); }; return ( <AntLayout> <Header className="header"> <div className="logo" /> <div className="user-info"> <span className="user-name">user.name</span> <span className="user-logout"> <Popconfirm title="是否确认退出?" okText="退出" cancelText="取消" onConfirm={handleLogout} > <LogoutOutlined /> 退出 </Popconfirm> </span> </div> </Header> <AntLayout> <Sider width={200} className="site-layout-background"> <Menu mode="inline" theme="dark" selectedKeys={[selectedKey]} style={{ height: "100%", borderRight: 0 }} items={[ { icon: <HomeOutlined />, key: "/", label: <Link to="/">数据概览</Link>, }, { icon: <DiffOutlined />, key: "/article", label: <Link to="/article">内容管理</Link>, }, { icon: <EditOutlined />, key: "/publish", label: <Link to="/publish">发布文章</Link>, }, ]} ></Menu> </Sider> <AntLayout className="layout-content" style={{ padding: 20 }}> <Outlet /> </AntLayout> </AntLayout> </AntLayout> ); }); export default Layout;
组件通信
父传子
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
41import React from "react"; // 函数式子组件 function FSon(props) { console.log(props); return ( <div> 子组件1 {props.msg} </div> ); } // 类子组件 class CSon extends React.Component { render() { return ( <div> 子组件2 {this.props.msg} </div> ); } } // 父组件 class App extends React.Component { state = { message: "this is message", }; render() { return ( <div> <div>父组件</div> <FSon msg={this.state.message} /> <CSon msg={this.state.message} /> </div> ); } } export default App;
子传父
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
42import React from "react"; // 子组件 function Son(props) { function handleClick() { // 调用父组件传递过来的回调函数 并注入参数 props.changeMsg("this is newMessage"); } return ( <div> {props.msg} <button onClick={handleClick}>change</button> </div> ); } class App extends React.Component { state = { message: "this is message", }; // 提供回调函数 changeMessage = (newMsg) => { console.log("子组件传过来的数据:", newMsg); this.setState({ message: newMsg, }); }; render() { return ( <div> <div>父组件</div> <Son msg={this.state.message} // 传递给子组件 changeMsg={this.changeMessage} /> </div> ); } } export default App;
兄弟组件通信,核心思路为变量提升。
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
42import React from "react"; // 子组件 function Son(props) { function handleClick() { // 调用父组件传递过来的回调函数 并注入参数 props.changeMsg("this is newMessage"); } return ( <div> {props.msg} <button onClick={handleClick}>change</button> </div> ); } class App extends React.Component { state = { message: "this is message", }; // 提供回调函数 changeMessage = (newMsg) => { console.log("子组件传过来的数据:", newMsg); this.setState({ message: newMsg, }); }; render() { return ( <div> <div>父组件</div> <Son msg={this.state.message} // 传递给子组件 changeMsg={this.changeMessage} /> </div> ); } } export default App;
跨组件间通信
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
31import React, { createContext } from "react"; // 1. 创建Context对象 const { Provider, Consumer } = createContext(); // 3. 消费数据 function ComC() { return <Consumer>{(value) => <div>{value}</div>}</Consumer>; } function ComA() { return <ComC />; } // 2. 提供数据 class App extends React.Component { state = { message: "this is message", }; render() { return ( <Provider value={this.state.message}> <div className="app"> <ComA /> </div> </Provider> ); } } export default App;
小案例
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
58import React from "react"; // 子组件 function ListItem(props) { const { name, price, info, id, delHandler } = props; return ( <div> <h3>{name}</h3> <p>{price}</p> <p>{info}</p> <button onClick={() => delHandler(id)}>删除</button> </div> ); } // 父组件 class App extends React.Component { state = { list: [ { id: 1, name: "超级好吃的棒棒糖", price: 18.8, info: "开业大酬宾,全场8折", }, { id: 2, name: "超级好吃的大鸡腿", price: 34.2, info: "开业大酬宾,全场8折", }, { id: 3, name: "超级无敌的冰激凌", price: 14.2, info: "开业大酬宾,全场8折", }, ], }; delHandler = (id) => { this.setState({ list: this.state.list.filter((item) => item.id !== id), }); }; render() { return ( <> {this.state.list.map((item) => ( <ListItem key={item.id} {...item} delHandler={this.delHandler} /> ))} </> ); } } export default App;
Hooks
useState
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { useState } from "react";
function App() {
// 参数:状态初始值比如,传入 0 表示该状态的初始值为 0
// 返回值:数组,包含两个值:1 状态值(state) 2 修改该状态的函数(setState)
const [count, setCount] = useState(0);
return (
<button
onClick={() => {
setCount(count + 1);
}}
>
{count}
</button>
);
}
export default App;
- 如果就是初始化一个普通的数据 直接使用
useState(普通数据)
即可 - 如果要初始化的数据无法直接得到需要通过计算才能获取到,使用
useState(()=>{})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { useState } from "react";
function Counter(props) {
const [count, setCount] = useState(() => {
return props.count;
});
return (
<div>
<button onClick={() => setCount(count + 1)}>{count}</button>
</div>
);
}
function App() {
return (
<>
<Counter count={10} />
<Counter count={20} />
</>
);
}
export default App;
useEffect
不添加依赖项
组件首次渲染执行一次,以及不管是哪个状态更改引起组件更新时都会重新执行
- 组件初始渲染
- 组件更新 (不管是哪个状态引起的更新)
1
2
3useEffect(() => { console.log("副作用执行了"); });
添加空数组
组件只在首次渲染时执行一次
1
2
3useEffect(() => { console.log("副作用执行了"); }, []);
添加特定依赖项
副作用函数在首次渲染时执行,在依赖项发生变化时重新执行
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
27function App() { const [count, setCount] = useState(0); const [name, setName] = useState("zs"); useEffect(() => { console.log("副作用执行了"); }, [count]); return ( <> <button onClick={() => { setCount(count + 1); }} > {count} </button> <button onClick={() => { setName("cp"); }} > {name} </button> </> ); }
useEffect
回调函数中用到的数据(比如,count
)就是依赖数据,就应该出现在依赖项数组中,如果不添加依赖项就会报错。如果想要清理副作用 可以在副作用函数中的末尾
return
一个新的函数,在新的函数中编写清理副作用的逻辑- 组件卸载时自动执行
组件更新时,下一个
useEffect
副作用函数执行之前自动执行1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17import { useEffect, useState } from "react"; const App = () => { const [count, setCount] = useState(0); useEffect(() => { const timerId = setInterval(() => { setCount(count + 1); }, 1000); return () => { // 用来清理副作用的事情 clearInterval(timerId); }; }, [count]); return <div>{count}</div>; }; export default App;
异步的正确写法
1
2
3
4
5
6useEffect(() => { async function fetchData() { const res = await axios.get("http://geek.itheima.net/v1_0/channels"); console.log(res); } }, []);
useRef
获取一个在重新渲染中保存,但改变其值不会触发页面重新渲染的对象。
1
2
3
4
5
6
7
8
9
10
11
12import { useRef } from "react"; export default function Counter() { let ref = useRef(0); function handleClick() { ref.current = ref.current + 1; alert("你点击了 " + ref.current + " 次!"); } return <button onClick={handleClick}>点击我!</button>; }
获取
DOM
实例。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
66import { useRef } from "react"; export default function CatFriends() { const firstCatRef = useRef(null); const secondCatRef = useRef(null); const thirdCatRef = useRef(null); function handleScrollToFirstCat() { firstCatRef.current.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center", }); } function handleScrollToSecondCat() { secondCatRef.current.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center", }); } function handleScrollToThirdCat() { thirdCatRef.current.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center", }); } return ( <> <nav> <button onClick={handleScrollToFirstCat}>Tom</button> <button onClick={handleScrollToSecondCat}>Maru</button> <button onClick={handleScrollToThirdCat}>Jellylorum</button> </nav> <div> <ul> <li> <img src="https://placekitten.com/g/200/200" alt="Tom" ref={firstCatRef} /> </li> <li> <img src="https://placekitten.com/g/300/200" alt="Maru" ref={secondCatRef} /> </li> <li> <img src="https://placekitten.com/g/250/200" alt="Jellylorum" ref={thirdCatRef} /> </li> </ul> </div> </> ); }
通过
Ref
函数参数的形式管理Ref
列表: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
60import { useRef } from "react"; export default function CatFriends() { const itemsRef = useRef(null); function scrollToId(itemId) { const map = getMap(); const node = map.get(itemId); node.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center", }); } function getMap() { if (!itemsRef.current) { // 首次运行时初始化 Map。 itemsRef.current = new Map(); } return itemsRef.current; } return ( <> <nav> <button onClick={() => scrollToId(0)}>Tom</button> <button onClick={() => scrollToId(5)}>Maru</button> <button onClick={() => scrollToId(9)}>Jellylorum</button> </nav> <div> <ul> {catList.map((cat) => ( <li key={cat.id} ref={(node) => { const map = getMap(); if (node) { map.set(cat.id, node); } else { map.delete(cat.id); } }} > <img src={cat.imageUrl} alt={"Cat #" + cat.id} /> </li> ))} </ul> </div> </> ); } const catList = []; for (let i = 0; i < 10; i++) { catList.push({ id: i, imageUrl: "https://placekitten.com/250/200?image=" + i, }); }
子组件通过
forwardRef
选择是否暴露DOM
元素
它是这样工作的:<MyInput ref={inputRef} />
告诉React
将对应的DOM
节点放入inputRef.current
中。但是,这取决于MyInput
组件是否允许这种行为, 默认情况下是不允许的。MyInput
组件是使用forwardRef
声明的。 这让从上面接收的inputRef
作为第二个参数ref
传入组件,第一个参数是props
。MyInput
组件将自己接收到的ref
传递给它内部的<input>
。
现在,单击按钮聚焦输入框起作用了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20import { forwardRef, useRef } from "react"; const MyInput = forwardRef((props, ref) => { return <input {...props} ref={ref} />; }); export default function Form() { const inputRef = useRef(null); function handleClick() { inputRef.current.focus(); } return ( <> <MyInput ref={inputRef} /> <button onClick={handleClick}>聚焦输入框</button> </> ); }
通过命令句柄(
useImperativeHandle
)暴露一部分的 API1
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// 通过命令句柄暴露一部分 API import { forwardRef, useRef, useImperativeHandle } from "react"; const MyInput = forwardRef((props, ref) => { const realInputRef = useRef(null); useImperativeHandle(ref, () => ({ // 只暴露 focus,没有别的 focus() { realInputRef.current.focus(); }, })); return <input {...props} ref={realInputRef} />; }); export default function Form() { const inputRef = useRef(null); function handleClick() { inputRef.current.focus(); } return ( <> <MyInput ref={inputRef} /> <button onClick={handleClick}>聚焦输入框</button> </> ); }
flushSync
同步更新 DOM:与Vue
中的nextTick
的用途类似,都是用来在 DOM 更改后进行某些操作,但Vue
是将该操作放在nextTick
的回调队列里面执行,包裹的是目的执行操作,而React
是让setState
同步更新 DOM。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
41import { useState, useRef } from "react"; import { flushSync } from "react-dom"; export default function TodoList() { const listRef = useRef(null); const [text, setText] = useState(""); const [todos, setTodos] = useState(initialTodos); function handleAdd() { const newTodo = { id: nextId++, text: text }; flushSync(() => { setText(""); setTodos([...todos, newTodo]); }); listRef.current.lastChild.scrollIntoView({ behavior: "smooth", block: "nearest", }); } return ( <> <button onClick={handleAdd}>添加</button> <input value={text} onChange={(e) => setText(e.target.value)} /> <ul ref={listRef}> {todos.map((todo) => ( <li key={todo.id}>{todo.text}</li> ))} </ul> </> ); } let nextId = 0; let initialTodos = []; for (let i = 0; i < 20; i++) { initialTodos.push({ id: nextId++, text: "待办 #" + (i + 1), }); }
函数组件由于没有实例,不能使用
ref
获取,如果想获取组件实例,必须是类组件
useReducer
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49import { useReducer } from "react"; import AddTask from "./AddTask.js"; import TaskList from "./TaskList.js"; import tasksReducer from "./tasksReducer.js"; export default function TaskApp() { const [tasks, dispatch] = useReducer(tasksReducer, initialTasks); function handleAddTask(text) { dispatch({ type: "added", id: nextId++, text: text, }); } function handleChangeTask(task) { dispatch({ type: "changed", task: task, }); } function handleDeleteTask(taskId) { dispatch({ type: "deleted", id: taskId, }); } return ( <> <h1>布拉格的行程安排</h1> <AddTask onAddTask={handleAddTask} /> <TaskList tasks={tasks} onChangeTask={handleChangeTask} onDeleteTask={handleDeleteTask} /> </> ); } let nextId = 3; const initialTasks = [ { id: 0, text: "参观卡夫卡博物馆", done: true }, { id: 1, text: "看木偶戏", done: false }, { id: 2, text: "打卡列侬墙", done: false }, ];
tasksReducer.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
29export default function tasksReducer(tasks, action) { switch (action.type) { case "added": { return [ ...tasks, { id: action.id, text: action.text, done: false, }, ]; } case "changed": { return tasks.map((t) => { if (t.id === action.task.id) { return action.task; } else { return t; } }); } case "deleted": { return tasks.filter((t) => t.id !== action.id); } default: { throw Error("未知 action:" + action.type); } } }
useContext
常规使用
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
30import { createContext, useContext } from "react"; // 创建Context对象,可以添加默认值 const Context = createContext(); function Foo() { return ( <div> Foo <Bar /> </div> ); } function Bar() { // 底层组件通过useContext函数获取数据 const name = useContext(Context); return <div>Bar {name}</div>; } function App() { return ( // 顶层组件通过Provider 提供数据 <Context.Provider value={"this is name"}> <div> <Foo /> </div> </Context.Provider> ); } export default App;
在相同的组件中提供并使用 context:
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
69
70
71
72
73
74// App.js import Heading from "./Heading.js"; import Section from "./Section.js"; export default function Page() { return ( <Section> <Heading>主标题</Heading> <Section> <Heading>副标题</Heading> <Heading>副标题</Heading> <Heading>副标题</Heading> <Section> <Heading>子标题</Heading> <Heading>子标题</Heading> <Heading>子标题</Heading> <Section> <Heading>子子标题</Heading> <Heading>子子标题</Heading> <Heading>子子标题</Heading> </Section> </Section> </Section> </Section> ); } // Section.js import { useContext } from 'react'; import { LevelContext } from './LevelContext.js'; export default function Section({ children }) { const level = useContext(LevelContext); return ( <section className="section"> <LevelContext.Provider value={level + 1}> {children} </LevelContext.Provider> </section> ); } // Heading.js import { useContext } from 'react'; import { LevelContext } from './LevelContext.js'; export default function Heading({ children }) { const level = useContext(LevelContext); switch (level) { case 0: throw Error('Heading 必须在 Section 内部!'); case 1: return <h1>{children}</h1>; case 2: return <h2>{children}</h2>; case 3: return <h3>{children}</h3>; case 4: return <h4>{children}</h4>; case 5: return <h5>{children}</h5>; case 6: return <h6>{children}</h6>; default: throw Error('未知的 level:' + level); } } // LevelContext.js import { createContext } from 'react'; // 默认值 export const LevelContext = createContext(1);
使用
Context
和Reducer
封装一个组件1
2
3
4
5
6
7
8
9
10
11
12
13import AddTask from "./AddTask.js"; import TaskList from "./TaskList.js"; import { TasksProvider } from "./TasksContext.js"; export default function TaskApp() { return ( <TasksProvider> <h1>Day off in Kyoto</h1> <AddTask /> <TaskList /> </TasksProvider> ); }
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
61import { createContext, useContext, useReducer } from "react"; const TasksContext = createContext(null); const TasksDispatchContext = createContext(null); export function TasksProvider({ children }) { const [tasks, dispatch] = useReducer(tasksReducer, initialTasks); return ( <TasksContext.Provider value={tasks}> <TasksDispatchContext.Provider value={dispatch}> {children} </TasksDispatchContext.Provider> </TasksContext.Provider> ); } export function useTasks() { return useContext(TasksContext); } export function useTasksDispatch() { return useContext(TasksDispatchContext); } function tasksReducer(tasks, action) { switch (action.type) { case "added": { return [ ...tasks, { id: action.id, text: action.text, done: false, }, ]; } case "changed": { return tasks.map((t) => { if (t.id === action.task.id) { return action.task; } else { return t; } }); } case "deleted": { return tasks.filter((t) => t.id !== action.id); } default: { throw Error("Unknown action: " + action.type); } } } const initialTasks = [ { id: 0, text: "Philosopher’s Path", done: true }, { id: 1, text: "Visit the temple", done: false }, { id: 2, text: "Drink matcha", done: false }, ];
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
30import { useState } from "react"; import { useTasksDispatch } from "./TasksContext.js"; export default function AddTask() { const [text, setText] = useState(""); const dispatch = useTasksDispatch(); return ( <> <input placeholder="Add task" value={text} onChange={(e) => setText(e.target.value)} /> <button onClick={() => { setText(""); dispatch({ type: "added", id: nextId++, text: text, }); }} > Add </button> </> ); } let nextId = 3;
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
69
70
71
72
73
74
75import { useState } from "react"; import { useTasks, useTasksDispatch } from "./TasksContext.js"; export default function TaskList() { const tasks = useTasks(); return ( <ul> {tasks.map((task) => ( <li key={task.id}> <Task task={task} /> </li> ))} </ul> ); } function Task({ task }) { const [isEditing, setIsEditing] = useState(false); const dispatch = useTasksDispatch(); let taskContent; if (isEditing) { taskContent = ( <> <input value={task.text} onChange={(e) => { dispatch({ type: "changed", task: { ...task, text: e.target.value, }, }); }} /> <button onClick={() => setIsEditing(false)}>Save</button> </> ); } else { taskContent = ( <> {task.text} <button onClick={() => setIsEditing(true)}>Edit</button> </> ); } return ( <label> <input type="checkbox" checked={task.done} onChange={(e) => { dispatch({ type: "changed", task: { ...task, done: e.target.checked, }, }); }} /> {taskContent} <button onClick={() => { dispatch({ type: "deleted", id: task.id, }); }} > Delete </button> </label> ); }
useCallback
React
中的 useCallback
是一个用于缓存函数定义的 Hook
。它的作用是避免在每次重新渲染时都创建新的函数,从而提高性能和避免不必要的渲染。
useCallback
的使用方法如下:
useCallback
接收两个参数:回调函数和依赖项数组。回调函数是需要缓存的函数,依赖项数组用于确定何时需要重新创建函数实例。- 当依赖项数组中的任何一个值发生变化时,
useCallback
将返回一个新的函数实例,否则它将返回之前缓存的函数实例。 useCallback
返回的函数不会被React
调用,而是由你自己决定何时以及是否调用它。useCallback
必须在组件的顶层或自定义Hook
中调用,不能在循环或条件语句中调用。
useCallback
的使用场景有以下几种:
当你需要将一个函数作为
prop
传递给子组件时,你可以使用useCallback
来避免子组件因为父组件的重新渲染而不必要地重新渲染。例如,下面的代码展示了一个使用useCallback
的计数器组件,它将一个函数作为prop
传递给子组件Button
,用来增加计数器的值。由于使用了useCallback
,Button
组件只会在count
发生变化时才重新渲染,而不会因为父组件的其他state
变化而重新渲染。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
28import React, { useState, useCallback } from "react"; function Button({ onClick, children }) { console.log("Button 组件重新渲染"); return <button onClick={onClick}>{children}</button>; } export default function Counter() { const [count, setCount] = useState(0); const [name, setName] = useState(""); const handleIncrement = useCallback(() => { setCount((prevCount) => prevCount + 1); }, []); const handleNameChange = (event) => { setName(event.target.value); }; return ( <div> <p>计数器的值:{count}</p> <Button onClick={handleIncrement}>增加</Button> <p>姓名:{name}</p> <input type="text" value={name} onChange={handleNameChange} /> </div> ); }
当你需要在一个
memoized
的回调函数中更新state
时,你可以使用useCallback
来保证回调函数的引用不变,从而避免触发无限循环。例如,下面的代码展示了一个使用useCallback
的自定义Hook
,用来获取和设置localStorage
中的数据。由于使用了useCallback
,setStoredValue
函数的引用不会在每次重新渲染时改变,从而避免在useEffect
中触发无限循环。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
31import { useState, useEffect, useCallback } from "react"; function useLocalStorage(key, initialValue) { // 获取 localStorage 中的数据 const [storedValue, setStoredValue] = useState(() => { try { const item = window.localStorage.getItem(key); return item ? JSON.parse(item) : initialValue; } catch (error) { console.error(error); return initialValue; } }); // 设置 localStorage 中的数据 const setValue = useCallback( (value) => { try { const valueToStore = value instanceof Function ? value(storedValue) : value; setStoredValue(valueToStore); window.localStorage.setItem(key, JSON.stringify(valueToStore)); } catch (error) { console.error(error); } }, [key, storedValue] ); return [storedValue, setValue]; }
当你需要在一个
useEffect
中使用一个函数时,你可以使用useCallback
来避免因为函数的重新创建而导致useEffect
的频繁执行。例如,下面的代码展示了一个使用useCallback
的组件,用来在组件挂载时获取用户的位置信息,并在用户位置发生变化时更新state
。由于使用了useCallback
,getLocation
函数的引用不会在每次重新渲染时改变,从而避免在useEffect
中触发不必要的执行。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
33import React, { useState, useEffect, useCallback } from "react"; export default function Location() { const [location, setLocation] = useState({}); // 获取用户的位置信息 const getLocation = useCallback(() => { navigator.geolocation.getCurrentPosition( (position) => { setLocation({ latitude: position.coords.latitude, longitude: position.coords.longitude, }); }, (error) => { console.error(error); } ); }, []); // 在组件挂载时获取用户的位置信息 useEffect(() => { getLocation(); }, [getLocation]); return ( <div> <p>你的位置是:</p> <p>纬度:{location.latitude}</p> <p>经度:{location.longitude}</p> </div> ); }
useMemo
useMemo
是一个 React
钩子,它允许你记住函数调用的结果,并在依赖关系发生变化时重新计算它。它可以通过防止不必要的重渲染来优化 React
应用程序的性能。
useMemo
的基本语法是:
1
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
其中,第一个参数是一个函数,它返回需要被记住的值。第二个参数是一个数组,它指定了哪些变量会影响这个值的计算。如果这个数组为空,那么 useMemo
只会在组件第一次渲染时执行一次。如果这个数组包含了一些变量,那么 useMemo
只会在这些变量发生变化时重新执行。如果这个数组包含了所有的状态和属性,那么 useMemo
的行为就和普通的函数一样,每次渲染都会执行一次。
useMemo
的使用场景有以下几种:
- 当组件中有一些复杂的计算逻辑,而这些计算逻辑的结果又不会频繁地变化时,可以使用
useMemo
来缓存这些结果,避免每次渲染都重新计算,提高性能。 - 当组件中有一些子组件,而这些子组件的属性是由父组件的状态或属性计算而来时,可以使用
useMemo
来缓存这些属性,避免父组件的状态或属性发生变化时,导致子组件不必要的重渲染。 - 当组件中有一些引用类型的状态或属性,如对象或数组时,可以使用
useMemo
来缓存这些状态或属性,避免每次渲染都创建新的引用,导致组件的浅比较失效,引起不必要的重渲染。
下面是一个使用 useMemo
的例子,它展示了如何使用 useMemo
来缓存一个复杂的计算结果,并将其作为子组件的属性传递:
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
import React, { useState, useMemo } from "react";
// 一个子组件,接收一个数字作为属性,并显示它
function Child(props) {
console.log("Child component rendered");
return <div>{props.number}</div>;
}
// 一个父组件,有一个数字状态,一个按钮,和一个子组件
function Parent() {
// 定义一个数字状态,初始值为0
const [number, setNumber] = useState(0);
// 定义一个函数,用来计算一个复杂的值,这里简单地模拟为一个循环
function computeExpensiveValue(num) {
console.log("computeExpensiveValue called");
let i = 0;
while (i < 1000000000) i++;
return num * 2;
}
// 使用useMemo来缓存计算结果,只有当number发生变化时,才重新计算
const memoizedValue = useMemo(() => computeExpensiveValue(number), [number]);
// 定义一个函数,用来更新数字状态,每次加1
function handleClick() {
setNumber(number + 1);
}
// 返回一个JSX元素,包含一个按钮,一个子组件,和一个显示useMemo返回值的div
return (
<div>
<button onClick={handleClick}>Click</button>
<Child number={number} />
<div>{memoizedValue}</div>
</div>
);
}
export default Parent;
在这个例子中,当点击按钮时,数字状态会加 1
,父组件会重新渲染,但是由于使用了 useMemo
,只有当数字状态发生变化时,才会重新调用computeExpensiveValue
函数,否则会直接返回缓存的值。这样就避免了每次渲染都执行一个复杂的计算,提高了性能。同时,由于子组件的属性没有变化,所以子组件也不会重新渲染,这也提高了性能。