React-技巧积累

React 中文文档

使用 await fetch

在 React 中, 若想使用 await 关键词, 则需要将包含其的函数设置为 async, 如:

1
2
3
4
5
6
7
export default async function Test() {
let response = await fetch("http://localhost:8112/homepage/music/list")
.then(response => {
return response.json()
})
console.log(response)
}

状态变量与 input 输入绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
import { useState } from 'react';

export default function App() {
const [value, setValue] = useState('');
return (
<div>
<input
value={value}
onChange={(e) => setValue(e.target.value)}
type="text" />
</div>
);
}

组件的两种标签写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Button() {
return (
<button>click me</button>
);
}

function App() {
return (
<div className='App'>
{ /* First type */ }
<Button />
{ /* Second type */ }
<Button></Button>
</div>
);
}

基础事件绑定

语法为:

1
on+EventName={handler}

整体上为驼峰命名法. 如

1
2
3
4
5
6
7
8
function App() {
const clickHandler = () => {
console.log('button pressed');
}
return (
<button onClick={clickHandler}></button>
);
}

使用事件对象

1
2
3
4
5
6
7
8
function App() {
const clickHandler = (e) => {
console.log('button pressed', e);
}
return (
<button onClick={clickHandler}></button>
);
}

onClick={clickHandler} 会自动给 clickHandler 传递一个参数, 就是事件对象.

传递参数

1
2
3
4
5
6
7
8
function App() {
const clickHandler = (name) => {
console.log('button pressed ', name);
}
return (
<button onClick={() => clickHandler('Jie')}></button>
);
}

同时使用自定义参数和事件对象

1
2
3
4
5
6
7
8
function App() {
const clickHandler = (name, e) => {
console.log('button pressed ', name, e);
}
return (
<button onClick={(e) => clickHandler('Jie', e)}></button>
);
}

这里 onClick 同样给 () => {} 这一匿名函数传递了一个事件对象.

JSX 常见使用场景

JSX 使用 JavaScript 对象作为内联样式

1
<div style={{ color: 'red' }}>This is red text</div>

注意 JSX 中只能是表达式, 而不能是语句与 if, switch.

列表渲染

1
2
3
4
5
6
7
8
9
const list = [
{ id: 1001, name: 'hello' },
{ id: 1002, name: 'world' },
{ id: 1003, name: 'lalal' },
]

<ul>
{list.map(item => <li key={item.id}>{item.name}</li>)}
</ul>

条件渲染

可以通过逻辑与运算符 && (似乎不能用逻辑或), 也可以用三元表达式 ?:.

如:

1
2
{flag && <span></span>}
{loading ? <span>loading...</span> : <span>this is true span</span>}

复杂条件渲染

用单独的函数处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
const type = 1

function getTypeRendered() {
if (type === 1) {
return <div>TYPE 1</div>
} else if (type === 2) {
return <div>TYPE 2</div>
} else {
return <div>TYPE 3</div>
}
}

<div>{getTypeRendered()}</div>

React 哲学

构建页面时, 将其拆分为一个个组件, 只需要把组件连接在一起, 使数据流经它们.

常用步骤:

  • 在原型图的每个组件和子组件周围绘制盒子并命名
  • 在原型中辨别好组件之后, 将其转化为层级结构
  • 之后使用 React 构建一个静态版本, 此时不考虑交互和 state (构建一个静态版本需要写大量的代码, 并不需要什么思考; 但添加交互需要大量的思考, 却不需要大量的代码)
  • 找出需要用到 state 的最小集合
    • 随时间保持不变的不是 state
    • 通过 props 从父组件传递的不是 state
    • 可以由其他 state 组件计算得出的不是 state
  • 验证 state 应该放置在哪里, 一般是共同的父组件或父组件上层的组件

React 渲染 UI 的流程

  1. 触发渲染
  2. 渲染组件
  3. 提交到 DOM

也就是说, 组件是先分别渲染好了, 才显示到页面上. (在渲染完成并且 React 更新 DOM 之后, 浏览器才会重新绘制屏幕)

React 中的渲染必须是一次 “纯计算”, 即:

  • 输入相同, 输出相同. 给定相同的输入, 组件应始终返回相同的 JSX
  • 只做它自己的事情. 它不应更改任何存在于渲染之前的对象或变量

为什么需要 state

  1. 局部变量无法在多次渲染中持久保存
  2. 更改局部变量不会触发渲染

state 更新的时机

1
2
3
4
const [count, setCount] = useState(0);
console.log(count); // 0
setCount(count + 1); // 请求用 1 重新渲染
console.log(count); // 仍然是 0!

也就是说, 当次渲染中, count 的值不会立即改变, 而是在重新渲染时更新值.

总结来说:

  • 设置 state 只会为下一次渲染变更 state 的值
  • 一个 state 变量的值永远不会在一次渲染的内部发生变化

state 的批处理更新

React 会等到事件处理函数中的 所有代码都运行完毕再处理 state 更新

这可以更新多个 state 变量, 甚至来自多个组件的 state 变量, 而不会触发太多的重新渲染. 但这也意味着只有在事件处理函数及其中任何代码执行完成之后, UI 才会更新. 这种特性也就是批处理.

示例:

1
2
3
4
5
6
7
8
9
function handleClick() {
console.log(count);
setCount(count+1);
console.log(count);
setCount(count+1);
console.log(count);
setCount(count+1);
console.log(count);
}

对当次渲染的影响是, count 值不变, 对下一次渲染的影响是 count 值加一.

这里实际上是先执行了:

1
2
3
4
console.log(count);
console.log(count);
console.log(count);
console.log(count);

然后再执行:

1
2
3
setCount(count+1);
setCount(count+1);
setCount(count+1);

即批处理. 在原来的执行顺序中, 每到 setCount(), 就将其加入到更新队列中, 在下一次渲染期间, React 会遍历队列并给你更新之后的最终 state.

组件位置对 state 的影响

对 React 来说重要的是组件在 UI 树中的位置, 而不是在 JSX 中的位置, 如:

  • 相同位置的相同组件会使得 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
    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 { 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>
    );
    }
  • 相同位置的不同组件会使 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
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    import { useState } from 'react';

    export default function App() {
    const [isPaused, setIsPaused] = useState(false);
    return (
    <div>
    {isPaused ? (
    <p>待会见!</p>
    ) : (
    <Counter />
    )}
    <label>
    <input
    type="checkbox"
    checked={isPaused}
    onChange={e => {
    setIsPaused(e.target.checked)
    }}
    />
    休息一下
    </label>
    </div>
    );
    }

    function Counter() {
    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>{score}</h1>
    <button onClick={() => setScore(score + 1)}>
    加一
    </button>
    </div>
    );
    }

也就是说, 如果想保留 state, 就需要确保渲染的树形结构就应该相互 “匹配”, 结构不同就会导致 state 的销毁, 因为 React 会在将一个组件从树中移除时销毁它的 state.

在在相同位置重置 state

可以使用 key 来让 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
import { useState } from 'react';

export default function Scoreboard() {
const [isPlayerA, setIsPlayerA] = useState(true);
return (
<div>
{isPlayerA ? (
<Counter key="Taylor" person="Taylor" />
) : (
<Counter key="Sarah" 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 不是全局唯一的, 它们只能指定父组件内部的顺序)

在下次渲染前多次更新同一个 state

setNumber(n => n + 1) 这种, 传入一个根据队列中的前一个 state 计算下一个 state 的 函数, 而不是像 setNumber(number + 1) 这样传入 下一个 state 值.

这里的 n => n + 1 就被称为 “更新函数”.

可以理解为 setCount 函数中的匿名函数, 会自动传入一个变量表示 state.

这里有个命名规范: 通常可以通过相应 state 变量的第一个字母来命名更新函数的参数.

1
2
3
setEnabled(e => !e);
setLastName(ln => ln.reverse());
setFriendCount(fc => fc * 2);

传递事件函数的误区

如:

1
<button onClick={handleClick()}>

其会在 React 渲染时就触发.

正确为:

1
<button onClick={handleClick}>

其会在点击时才触发.

事件的传播以及处理方法

事件处理函数还将捕获任何来自子组件的事件. 通常, 说事件会沿着树向上 “冒泡” 或 “传播”.

在 React 中所有事件都会传播, 除了 onScroll.

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export default function Toolbar() {
return (
<div className="Toolbar" onClick={() => {
alert('你点击了 toolbar !');
}}>
<button onClick={() => alert('正在播放!')}>
播放电影
</button>
<button onClick={() => alert('正在上传!')}>
上传图片
</button>
</div>
);
}

如果点击任一按钮, 它自身的 onClick 将首先执行, 然后父级 <div>onClick 会接着执行.

阻止传播

事件处理函数都会默认接收一个 “事件对象” 作为唯一参数 (e, event), 可以通过这个参数来获取该事件的一些相关信息, 也可用于阻止传播, 如:

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
function Button({ onClick, children }) {
return (
<button onClick={e => {
e.stopPropagation();
onClick();
}}>
{children}
</button>
);
}

export default function Toolbar() {
return (
<div className="Toolbar" onClick={() => {
alert('你点击了 toolbar !');
}}>
<Button onClick={() => alert('正在播放!')}>
播放电影
</Button>
<Button onClick={() => alert('正在上传!')}>
上传图片
</Button>
</div>
);
}

阻止默认行为

某些事件具有一些默认行为, 如点击 <form> 表单内部的按钮会触发表单提交事件, 默认情况下将重新加载整个页面:

1
2
3
4
5
6
7
8
export default function Signup() {
return (
<form onSubmit={() => alert('提交表单!')}>
<input />
<button>发送</button>
</form>
);
}

可以通过调用事件对象中的 e.preventDefault() 来阻止:

1
2
3
4
5
6
7
8
9
10
11
export default function Signup() {
return (
<form onSubmit={e => {
e.preventDefault();
alert('提交表单!');
}}>
<input />
<button>发送</button>
</form>
);
}

返回 null

return null 可以不做任何渲染.

三目运算符

将:

1
2
3
4
if (isPacked) {
return <li className="item">{name} ✔</li>;
}
return <li className="item">{name}</li>;

写为:

1
2
3
4
5
return (
<li className="item">
{isPacked ? name + ' ✔' : name}
</li>
);

与运算符 (&&) 的经典示例

1
2
3
4
5
return (
<li className="item">
{name} {isPacked && '✔'}
</li>
);

用 JSX 展开语法传递 props

1
2
3
4
5
6
7
function Profile(props) {
return (
<div className="card">
<Avatar {...props} />
</div>
);
}

其会将所有 Profile 接收到的 props 转发到 Avatar, 而不需要一个个列出名字.

将 JSX 作为子组件传递

如:

1
2
3
<Card>
<Avatar />
</Card>

将内容嵌套在 JSX 标签中时, 父组件将在名为 children 的 prop 中接收到该内容. 如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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>
);
}

不要嵌套定义组件

在组件中定义另一个组件会导致代码运行慢, 且容易导致 bug. 应该在顶层定义每一个组件, 如:

1
2
3
4
5
6
7
8
export default function Gallery() {
// ...
}

// ✅ 在顶层声明组件
function Profile() {
// ...
}

列表 key

在创建列表时, 每一个列表项需要有一个唯一的 key 来将其与其他列表项区分开:

1
2
3
<li key={user.id}>
{user.name}: {user.taskCount} tasks left
</li>

key 便于 React 在渲染时更新组件的状态, 如果组件的 key 发生变化, 组件将被销毁, 新 state 将重新创建.

key 是 React 中一个特殊的保留属性. 创建元素时, React 提取 key 属性并将 key 直接存储在返回的元素上.

key 不需要是全局唯一的, 它们只需要在组件及其同级组件之间是唯一的. (一般不用数组的 index 作为 key, 因为可能会改变)

可以用 uuid 库来生产 key.

() => 语法

1
<Square value={squares[0]} onSquareClick={handleClick(0)} />

会导致无限调用. handleClick() 函数会调用 setSquares() 更新 state, 导致再次渲染, 即再次调用 handleClick().

而:

1
<Square value={squares[0]} onSquareClick={() => handleClick(0)} />

能解决这个问题. 其传递一个函数而非运行一个函数.

指定 Class

同 HTML 有所不同, JSX 使用 className 属性来指定, 如:

1
2
3
export default function Square() {
return <button className="square">X</button>;
}

Hook

use 开头的函数被称为 Hook. 如 useState 就是 React 提供的一个内置 Hook. 注意 Hook 只有在 React 渲染时有效.

Hook 是 React 的特性, 因此与普通 JavaScript 函数有区别:

  1. Hook 只能在 React 函数组件或自定义 Hook 中调用
  2. 只能在顶层调用 Hook, 即不要在循环, 条件判断或嵌套函数中调用 Hooks
  3. 自定义 Hook 必须以 use 开头

state 变量

在 React 中,state 是一个用于管理组件内部状态的数据结构. state 允许组件动态地响应用户输入, 网络请求, 时间变化等, 进而更新 UI.

需要先从 React 中导入 useState:

1
import { useState } from 'react';

使用如:

1
2
3
4
function MyButton() {
const [count, setCount] = useState(0);
// ...
}

useState 返回两个值:

  • 当前的 state, 这里是 count
  • 更新 state 的函数, 这里是 setCount

useState(0), 这里的 0 是传递给 state 的初始值.

若想改变 state, 则用 setCount() 传递新的值. 如:

1
2
3
4
5
6
7
8
9
10
11
12
13
function MyButton() {
const [count, setCount] = useState(0);

function handleClick() {
setCount(count + 1);
}

return (
<button onClick={handleClick}>
Clicked {count} times
</button>
);
}

嵌入 JavaScript 变量

使用大括号:

1
2
3
4
5
6
7
8
9
export default function MyBotton() {
let hello = "OHHHH";
return (
<>
<h1>{hello}</h1>
<button>I'm a button</button>
</>
);
}

JSX 规则

  1. 必须使用闭合标签
  2. 一个组件不能返回多个 JSX 标签 (只能返回一个根元素)
  3. 使用驼峰式命名法给大部分属性命名

如:

1
2
3
4
5
6
export default function MyBotton() {
return (
<h1>hello</h1>
<button>I'm a button</button>
);
}

就会报错. 若想同时返回这两个标签, 则用一个空标签 <>...</> 包裹:

1
2
3
4
5
6
7
8
export default function MyBotton() {
return (
<>
<h1>hello</h1>
<button>I'm a button</button>
</>
);
}

JSX 中嵌入 JavaScript 的规则

使用大括号 {} 嵌入, 但只有两种场景能使用:

  1. 用作 JSX 标签内的文本:<h1>{name}'s To Do List</h1> 是有效的, 但是 <{tag}>Gregorio Y. Zara's To Do List</{tag}> 无效
  2. 用作紧跟在 = 符号后的 属性: src={avatar} 会读取 avatar 变量, 但是 src="{avatar}" 只会传一个字符串 {avatar}

组件命名约定

React 组件必须以大写字母开头, 而 HTML 标签则必须是小写字母.

组件的嵌套使用

一个组件 MyButton:

1
2
3
4
5
function MyButton() {
return (
<button>I'm a button</button>
);
}

另一组件 MyApp:

1
2
3
4
5
6
7
function MyApp() {
return (
<div>
<h1>Welcome to my app</h1>
</div>
);
}

若将 MyButton 嵌入到 MyApp 中, 则为:

1
2
3
4
5
6
7
8
function MyApp() {
return (
<div>
<h1>Welcome to my app</h1>
<MyButton />
</div>
);
}

组件返回值用 () 包裹

若不使用括号, 如:

1
2
3
4
export function MyButton() {
return
<button>I'm a button</button>;
}

此时 JavaScript 会在 return 语句后自动插入一个 ;, 变为:

1
2
3
4
export function MyButton() {
return ;
<button>I'm a button</button>;
}

因此返回值变成了 undefined

添加括号可以避免这种问题:

1
2
3
4
5
export function MyButton() {
return (
<button>I'm a button</button>
);
}

使用 uuid 库生成唯一 key

1
2
3
4
5
import { v4 as uuidv4 } from 'uuid';

{items.map((item) => (
<li key={uuidv4()}>{item.name}</li>
))}

React-技巧积累
http://example.com/2024/07/13/React-技巧积累/
作者
Jie
发布于
2024年7月13日
许可协议