553 lines
15 KiB
Markdown
553 lines
15 KiB
Markdown
# 快速入门
|
||
## 创建和嵌套组件
|
||
React应用由组件组成,每个组件都是UI的一部分,组件拥有自己的外观和逻辑。
|
||
### 组件是返回标签的js函数
|
||
```js
|
||
function MyButton() {
|
||
return (
|
||
<button>I'm a button</button>
|
||
);
|
||
}
|
||
```
|
||
上述已经声明了MyButton组件,可以将该组件嵌套到另一个组件中
|
||
```js
|
||
export default function MyApp() {
|
||
return (
|
||
<div>
|
||
<h1>Welcome to my app</h1>
|
||
<MyButton />
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
> react组件必须以大写字母开头,而html组件则是必须以小写字母开头,可以通过组件开头字母的大小写来区分html组件和react组件
|
||
|
||
## jsx编写标签
|
||
上述返回标签的语法被称为jsx,大多数react项目都支持jsx。
|
||
jsx比html更加严格:
|
||
1. 所有的标签都要有闭合标签(例如`<br/>`)
|
||
2. 组件不能返回多个标签,只能返回一个标签,如果存在多个,必须将其包含到一个公共的父级`<div>`或`<>`中
|
||
```js
|
||
function AboutPage() {
|
||
return (
|
||
<>
|
||
<h1>About</h1>
|
||
<p>Hello there.<br />How do you do?</p>
|
||
</>
|
||
);
|
||
}
|
||
```
|
||
|
||
## 添加样式
|
||
React中,可以在标签中添加`className`属性来添加样式,其和html中的`class`工作方式相同
|
||
```html
|
||
<img className="avatar" />
|
||
```
|
||
```css
|
||
/* In your CSS */
|
||
.avatar {
|
||
border-radius: 50%;
|
||
}
|
||
```
|
||
## 显示数据
|
||
在jsx中,标签位于js中,而可以在标签内通过`{}`来计算js表达式并将其填充到标签中
|
||
```js
|
||
return (
|
||
<h1>
|
||
{user.name}
|
||
</h1>
|
||
);
|
||
```
|
||
jsx还可以将表达式的值传递给标签属性,通过`{}`传递表达式的值
|
||
```js
|
||
return (
|
||
<img
|
||
className="avatar"
|
||
src={user.imageUrl}
|
||
/>
|
||
);
|
||
```
|
||
同时,可以通过js表达式来复制css
|
||
```js
|
||
const user = {
|
||
name: 'Hedy Lamarr',
|
||
imageUrl: 'https://i.imgur.com/yXOvdOSs.jpg',
|
||
imageSize: 90,
|
||
};
|
||
|
||
export default function Profile() {
|
||
return (
|
||
<>
|
||
<h1>{user.name}</h1>
|
||
<img
|
||
className="avatar"
|
||
src={user.imageUrl}
|
||
alt={'Photo of ' + user.name}
|
||
style={{
|
||
width: user.imageSize,
|
||
height: user.imageSize
|
||
}}
|
||
/>
|
||
</>
|
||
);
|
||
}
|
||
```
|
||
## 条件渲染
|
||
在React中,没有特殊语法编写条件,故而可以在js中通过if条件引入jsx:
|
||
```js
|
||
let content;
|
||
if (isLoggedIn) {
|
||
content = <AdminPanel />;
|
||
} else {
|
||
content = <LoginForm />;
|
||
}
|
||
return (
|
||
<div>
|
||
{content}
|
||
</div>
|
||
);
|
||
```
|
||
也可以使用如下方式:
|
||
```js
|
||
<div>
|
||
{isLoggedIn ? (
|
||
<AdminPanel />
|
||
) : (
|
||
<LoginForm />
|
||
)}
|
||
</div>
|
||
```
|
||
## 渲染列表
|
||
可以通过如下方式将js数组渲染为列表
|
||
```js
|
||
const products = [
|
||
{ title: 'Cabbage', id: 1 },
|
||
{ title: 'Garlic', id: 2 },
|
||
{ title: 'Apple', id: 3 },
|
||
];
|
||
const listItems = products.map(product =>
|
||
<li key={product.id}>
|
||
{product.title}
|
||
</li>
|
||
);
|
||
|
||
return (
|
||
<ul>{listItems}</ul>
|
||
);
|
||
```
|
||
上述示例中,`<li>`有一个key属性,对于列表中的每个属性,都应该传递给其一个字符串或数字的key,用于在兄弟节点之间唯一标识该元素。
|
||
|
||
## 响应事件
|
||
可以在组件中声明事件处理函数来响应事件
|
||
```js
|
||
function MyButton() {
|
||
function handleClick() {
|
||
alert('You clicked me!');
|
||
}
|
||
|
||
return (
|
||
<button onClick={handleClick}>
|
||
Click me
|
||
</button>
|
||
);
|
||
}
|
||
```
|
||
## 更新界面
|
||
如果希望组件维护状态信息,可以通过导入useState来完成
|
||
```js
|
||
import { useState } from 'react';
|
||
|
||
function MyButton() {
|
||
const [count, setCount] = useState(0);
|
||
```
|
||
其中,count记录当前的状态,而setCount则是用于改变状态的函数,可以为数组中变量取任何名称。
|
||
```js
|
||
import { useState } from 'react';
|
||
|
||
export default function MyApp() {
|
||
return (
|
||
<div>
|
||
<h1>Counters that update separately</h1>
|
||
<MyButton />
|
||
<MyButton />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function MyButton() {
|
||
const [count, setCount] = useState(0);
|
||
|
||
function handleClick() {
|
||
setCount(count + 1);
|
||
}
|
||
|
||
return (
|
||
<button onClick={handleClick}>
|
||
Clicked {count} times
|
||
</button>
|
||
);
|
||
}
|
||
```
|
||
## 使用hook
|
||
以`use`开头的函数被称为hook,useState是react提供的一个内置hook。
|
||
hook比普通函数更为严格,只能在组件顶层或其他hook的顶层调用hook。如果想要在条件或循环中使用hook,请新建一个组件并在组件内部使用。
|
||
## 组件之间共享数据
|
||
如果想要将状态在多个组件之间共享,需要将状态提升存储到最近的公共父组件中
|
||
```js
|
||
export default function MyApp() {
|
||
const [count, setCount] = useState(0);
|
||
|
||
function handleClick() {
|
||
setCount(count + 1);
|
||
}
|
||
|
||
return (
|
||
<div>
|
||
<h1>Counters that update together</h1>
|
||
<MyButton count={count} onClick={handleClick} />
|
||
<MyButton count={count} onClick={handleClick} />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function MyButton({ count, onClick }) {
|
||
return (
|
||
<button onClick={onClick}>
|
||
Clicked {count} times
|
||
</button>
|
||
);
|
||
}
|
||
```
|
||
此时,由MyApp传递给MyButton的值称之为prop
|
||
## jsx展开传递props
|
||
如果父组件想要将接收到的props全部传递给子组件,无需在子组件上列出props中全部属性,可以使用`...props`来进行展开
|
||
```js
|
||
function Profile(props) {
|
||
return (
|
||
<div className="card">
|
||
<Avatar {...props} />
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
## 将jsx作为子组件传递
|
||
当采用如下形式时:
|
||
```js
|
||
<Card>
|
||
<Avatar />
|
||
</Card>
|
||
```
|
||
其中,父组件接收到的prop中,children将代表接受到的子组件内容
|
||
```js
|
||
import Avatar from './Avatar.js';
|
||
|
||
function Card({ children }) {
|
||
return (
|
||
<div className="card">
|
||
{children}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default function Profile() {
|
||
return (
|
||
<Card>
|
||
<Avatar
|
||
size={100}
|
||
person={{
|
||
name: 'Katsuko Saruhashi',
|
||
imageId: 'YfeOqp2'
|
||
}}
|
||
/>
|
||
</Card>
|
||
);
|
||
}
|
||
```
|
||
此时,想要嵌套在父组件内部的子组件可以通过props.children来访问。
|
||
|
||
## 条件渲染
|
||
如果在某些条件下,组件不想显示任何内容,可以返回null
|
||
```js
|
||
function Item({ name, isPacked }) {
|
||
if (isPacked) {
|
||
return null;
|
||
}
|
||
return <li className="item">{name}</li>;
|
||
}
|
||
```
|
||
## 组件的渲染和提交
|
||
### 组件渲染的原因
|
||
- 组件的初次渲染
|
||
- 组件(或先祖组件)的状态发生了变化
|
||
#### 初次渲染
|
||
当引用启动时,会触发初次渲染
|
||
#### 状态更新时重新渲染
|
||
当初次渲染完成后,可以通过set函数来更新state并触发渲染。更新组件状态会将重新渲染加入到队列。
|
||
|
||
对于初次渲染,React会调用根部组件的方法来进行渲染,而对于状态更新触发的渲染,react只会调用状态更新对应组件的函数
|
||
> 对于组件的渲染,其过程是递归的。如果待渲染的组件中包含了子组件,那么react会对子组件进行渲染,对子组件中包含的孙组件也同样。渲染会对组件子树中所有的组件进行渲染。
|
||
|
||
#### 渲染差异
|
||
React只会在渲染之间存在差异时才会更改React节点,如一个节点,如果渲染之间从父组件接受到了不同的props,此时该组件才会发生重新渲染。
|
||
|
||
## state快照
|
||
在渲染函数中,state的快照是不可变的,即使在调用setState函数将更新state后,旧组件中获取的state值仍然没有变化。
|
||
```js
|
||
// 即使调用setTitle,此快照中的title值在setTitle之前和之后仍然没有变化
|
||
// ,而新渲染的组件title初始值则为改变之后的值
|
||
function Title() {
|
||
const [title,setTitle]=useState('Morning News');
|
||
|
||
return (
|
||
<div>
|
||
<h1>{title}</h1>
|
||
<button onClick={
|
||
()=>{
|
||
alert(title);
|
||
setTitle(title==='Morning News'?'Evening Newng News':'Morning News');
|
||
alert(title);
|
||
}
|
||
}>Switch</button>
|
||
</div>
|
||
)
|
||
}
|
||
```
|
||
## 向setState中传入更新函数
|
||
react支持向setState中传入更新函数而不是更新后的值,例如
|
||
```js
|
||
setNumber(n => n + 1);
|
||
setNumber(n => n + 1);
|
||
setNumber(n => n + 1);
|
||
```
|
||
此时react会将上述三个函数以此加入到队列中,并在下次渲染时遍历执行队列中的函数。
|
||
|
||
## 更新state对象
|
||
如果向useState中传入object,那么在调用setState函数时,则是应该传入一个新的对象,而不是在原有state对象上进行更新。
|
||
**应该将state对象看作是不可变的,调用setState时,应该创建一个state对象的副本,并且修改副本对象后用副本对象的值去更新setState方法。**
|
||
```js
|
||
// 如果想要修改对象中的某个字段,可以使用如下方法
|
||
let oldObj = {
|
||
name : 'kazusa',
|
||
isMale: false,
|
||
};
|
||
let newObj = {
|
||
// 展开对象中的属性
|
||
...odlObj,
|
||
// 新设置某个属性,用于覆盖旧的值
|
||
name:'mashiro',
|
||
}
|
||
```
|
||
### 修改嵌套对象中的属性
|
||
```js
|
||
setPerson({
|
||
...person, // Copy other fields
|
||
artwork: { // but replace the artwork
|
||
...person.artwork, // with the same one
|
||
city: 'New Delhi' // but in New Delhi!
|
||
}
|
||
});
|
||
```
|
||
## Immer
|
||
如果要修改深层嵌套的state值,可以引入Immer库来完成对象的拷贝工作。通过Immer库,可以无需关心对象不可变,而是直接以可变的语法来修改对象属性,而Immer会实际帮忙处理对象不可便的工作。
|
||
```js
|
||
// 通过该import代替react中useState
|
||
import { useImmer } from 'use-immer';
|
||
|
||
// 然后可以直接修改对象中的值
|
||
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;
|
||
});
|
||
}
|
||
```
|
||
|
||
## 更新state数组
|
||
和object一样,在react中数组应该也要是不可变的,在更新state中的值时,也应该再创建一个新的数组,并用新的数组值设置
|
||
|
||
## 重新渲染时组件状态的保存
|
||
只要一个组件还在**UI树中相同的位置**,那么react就会保存其state;如果组件被移除,或另有一个类型的组件被渲染在相同的位置,那么react将会丢弃旧组件的state。
|
||
如果不同同类型组件被渲染在相同位置,state仍然会被保存。
|
||
|
||
### 在UI树结构的同一位置渲染不同组件
|
||
如果在两次渲染之间,在UI树的同一位置渲染了相同类型的组件,那么默认情况下两次ui组件的state相同的。
|
||
如果要显式指定两次渲染的组件是不同的,各自有独立的state,可以为两次渲染的组件指定不同的key:
|
||
```js
|
||
import { useState } from 'react';
|
||
|
||
export default function Scoreboard() {
|
||
const [isPlayerA, setIsPlayerA] = useState(true);
|
||
return (
|
||
<div>
|
||
{isPlayerA ? (
|
||
<Counter person="Taylor" />
|
||
) : (
|
||
<Counter person="Sarah" />
|
||
)}
|
||
<button onClick={() => {
|
||
setIsPlayerA(!isPlayerA);
|
||
}}>
|
||
下一位玩家!
|
||
</button>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function Counter({ person }) {
|
||
const [score, setScore] = useState(0);
|
||
const [hover, setHover] = useState(false);
|
||
|
||
let className = 'counter';
|
||
if (hover) {
|
||
className += ' hover';
|
||
}
|
||
|
||
return (
|
||
<div
|
||
className={className}
|
||
onPointerEnter={() => setHover(true)}
|
||
onPointerLeave={() => setHover(false)}
|
||
>
|
||
<h1>{person} 的分数:{score}</h1>
|
||
<button onClick={() => setScore(score + 1)}>
|
||
加一
|
||
</button>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
```
|
||
> 注意,key并不需要全局唯一,key只用于指定父组件内部的顺序
|
||
|
||
### 前后两次组件渲染位于不同UI树位置
|
||
如果前后两次组件渲染时位于不同的ui树位置,如果要保持前后两次渲染组件state相同,可以为前后两次渲染指定相同的key,那么在前后两次渲染时,即使ui树位置不同,state仍然会延续相同。
|
||
|
||
state和树中位置相关,例如
|
||
```js
|
||
// 第一次渲染
|
||
return (
|
||
<ul>
|
||
<li key={id1}><Item key="a" /></li>
|
||
<li key={id2}><Item key="b"/></li>
|
||
</ul>
|
||
)
|
||
// 第二次渲染
|
||
return (
|
||
<ul>
|
||
<li key={id2}><Item key="b"/></li>
|
||
<li key={id1}><Item key="a"/></li>
|
||
</ul>
|
||
)
|
||
```
|
||
在上述两次渲染中,`item a`和`item b`父节点`<li>`的顺序虽然发生了改变,但是能够通过`<li>`元素的key属性来区分,故而`item a`和`item b`的state仍然会保持原样。显示效果为列表第一行元素和第二行元素发生了对调。
|
||
|
||
## Reducer
|
||
reducer是一个函数,接受两个参数:
|
||
- 当前state
|
||
- action,代表改变状态的参动作,数据结构可以自定义
|
||
|
||
reducer方法的返回值是一个新的state,react会将旧的状态设置为新的状态。
|
||
|
||
### Reducer使用
|
||
```js
|
||
import { useReducer } from 'react';
|
||
|
||
// 用于替换useState
|
||
// useReducer接受两个参数,一个reducer函数,一个state的初始值
|
||
// 其会返回有状态的值和一个dispatch函数,dispatch函数用于分发action给reducer
|
||
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
|
||
|
||
// 之后可以向dispatch函数提交action
|
||
function handleAddTask(text) {
|
||
dispatch({
|
||
type: 'added',
|
||
id: nextId++,
|
||
text: text,
|
||
});
|
||
}
|
||
|
||
function handleChangeTask(task) {
|
||
dispatch({
|
||
type: 'changed',
|
||
task: task,
|
||
});
|
||
}
|
||
|
||
// reducer函数会处理action
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
## Context
|
||
如果需要在先祖节点和子孙节点间隔多级时,从先祖节点向子孙节点传递数据,可以使用Context.
|
||
Context使用如下:
|
||
### 创建Context
|
||
```js
|
||
import { createContext } from 'react';
|
||
|
||
export const LevelContext = createContext(1);
|
||
```
|
||
### 子孙组件从Context中获取值
|
||
```js
|
||
import { useContext } from 'react';
|
||
import { LevelContext } from './LevelContext.js';
|
||
export default function Heading({ children }) {
|
||
const level = useContext(LevelContext);
|
||
// ...
|
||
}
|
||
```
|
||
|
||
### 先祖节点提供Context
|
||
```js
|
||
import { LevelContext } from './LevelContext.js';
|
||
|
||
export default function Section({ level, children }) {
|
||
return (
|
||
<section className="section">
|
||
<LevelContext.Provider value={level}>
|
||
{children}
|
||
</LevelContext.Provider>
|
||
</section>
|
||
);
|
||
}
|
||
```
|
||
|
||
|
||
|
||
|
||
|