React/Solid学习笔记

React/Solid学习笔记

小叶子

封面作者:CoCoLo

请先阅读JavaScript学习笔记

React19相关笔记修改

  • useActionState Hook 用于方便处理异步操作的状态, 可以用来禁用按钮等
  • useOptimistic Hook 用于处理乐观更新 (可以大幅优化 CounselorLeaf 的对话加载中代码)
  • use API 用于在渲染中等待异步操作完成 (类似于 await 语法, 但是在同步的渲染函数中使用)
  • ref 可以直接作为 props 传递给组件, 无需使用 forwardRef
  • <Content.Provider value={value}> 改为 <Content value={value}>
  • 组件 ref 函数支持返回一个清理函数, 用于在组件卸载时清理资源
  • 支持在组件中定义 <link>, <meta>, <title> 等标签, 而无需通过 useEffect 等方法
  • 新增一系列预加载资源的方法
  • 弃用 PropTypes, 使用 TypeScriptJSDoc 代替

⭐React

React 是一个用于构建用户界面的开源 JavaScript 库, 由 Facebook 开发和维护; 使用 pnpm create vite 创建项目时, 可以选择 React 模板

React 应用程序由一个个组件构成, 一个组件就是一个返回 JSX 元素的 JavaScript 函数 (为了与 HTML 标签区分, 必须用大写字母开头的函数名)

JSX 是一种 JavaScript 语法扩展, 可以在 JavaScript 中编写类似 HTML 的代码, 用于描述用户界面; 实际上, JSXReact.createElement 函数的语法糖

1
2
3
4
5
6
7
8
9
10
11
12
13
// App.jsx
export default function App() {
return ( // 多行 JSX 必须用括号包裹
<div>
<h1>Hello, React!</h1>
<Button />
</div>
)
}

function Button() {
return <button>Click me</button>
}
  • 不可以在组件内部定义组件 (会很慢且有 bug)
  • 由于一个组件就是一个函数, 所以按照一般 ES Module 的规范进行模块化即可
  • 组件函数应是纯函数: 只负责自己的任务 (不修改函数作用域外对象), 输入相同则输出相同 (类似于数学公式)
    副作用: 与渲染过程无关的操作, 如网络请求、定时器等
    副作用通常属于事件处理函数, 因此事件处理函数不必是纯函数
    如果一定要在渲染函数中执行副作用, 可以使用 useEffect 方法 (告诉 React 组件需要在渲染后执行某些操作)

JSX

  • JSXHTML 更严格:
    1. 单标签必须闭合 (如 <br />)
    2. 一个组件只能返回一个标签 (可以用 <div></div><></> 包裹, 其中 <></> 不会在 DOM 生成额外节点)
    3. 使用小驼峰命名法命名属性 (如 classNameonClick, 但 data-* 例外)
    4. 通过 {} 插入 JavaScript 表达式 (只能在标签内文本或属性的 ={xxx} 中使用)
    5. 为避免与 JavaScript 关键字冲突, JSX 中的 classfor 属性分别用 classNamehtmlFor 代替
  • 可以使用转换工具 HTML 转换为 JSX

渲染树和依赖树

  • 是表示实体之间关系的常见方式,它们经常用于建模 UI
  • 渲染树表示单次渲染中 React 组件之间的嵌套关系
  • 使用条件渲染,渲染树可能会在不同的渲染过程中发生变化; 使用不同的属性值,组件可能会渲染不同的子组件
  • 渲染树有助于识别顶级组件和叶子组件; 顶级组件会影响其下所有组件的渲染性能,而叶子组件通常会频繁重新渲染; 识别它们有助于理解和调试渲染性能问题
  • 依赖树表示 React 应用程序中的模块依赖关系
  • 构建工具使用依赖树来捆绑必要的代码以部署应用程序
  • 依赖树有助于调试大型捆绑包带来的渲染速度过慢的问题,以及发现哪些捆绑代码可以被优化
渲染树依赖树
渲染树依赖树

引入现有项目

注意, 应用的打包工具必须支持 ES Module, 如 WebpackViteParcel

1
2
# 安装 React
npm install react react-dom
1
2
<!-- 给 React 组件一个容器 -->
<div id="count"></div>
1
2
3
4
5
6
7
8
9
10
11
12
13
// 引入 React
import { createRoot } from 'react-dom/client'
import React, { useState } from 'react'
// 定义组件
function Counter() {
const [count, setCount] = useState(0)
function handleClick() setCount(count + 1)
return <button onClick={handleClick}>点击次数: {count}</button>
}
// 创建虚拟 DOM
const root = createRoot(document.querySelector('#count'))
// 渲染组件
root.render(<Counter />)

虚拟 DOMReact 的核心, 用于描述 DOM 结构, 当 state 发生变化时, React 会比较新旧 DOM 结构, 仅更新需要更新的部分, 以提高性能

Component

添加数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const user = {name: '小叶子', age: 18, class: 'psychology'}
const userStyle = {color: 'red', fontSize: '20px'}
function UserInfo() {
return (
<div
className={user.class} // 从对象中获取属性添加类名
style={
...userStyle, // 使用展开运算符合并样式
fontWeight: 'bold' // 使用驼峰命名法
}
>
<h2>{user.name}</h2>
<p>{user.age >= 18 ? '成年' : '未成年'}</p> // 使用三元运算符
</div>
)
}

注意: 样式必须用小驼峰命名法 (JavaScript 的变量名不能有 -)

条件渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function GreenButton() {
return <button style={{color: 'green'}}>Click me</button>
}
function RedButton() {
return <button style={{color: 'red'}}>Click me</button>
}

const user = { name: '小叶子', age: 18 }
function Button() {
return (
<div> // 三元运算符实现条件渲染
{user.age >= 18 ? <GreenButton /> : ( user.age > 12 ? <RedButton /> : null )}
</div>
)
}

组件在某些情况下可能不需要返回任何内容, 此时可以返回 null

列表渲染

1
2
3
4
5
6
7
8
const list = ['apple', 'banana', 'cherry']
// 使用 map 方法将列表转换为 JSX 元素数组
const listItems = list.map(
(item, index) => <li key={index}>{item}</li>
)
function List() {
return <ul>{listItems}</ul>
}
  • 列表渲染时, 需要为每个子元素添加一个独一无二的 key 属性, 用于帮助 React 识别列表中的每个元素
  • 通常使用来自数据库的唯一 id 作为 key (如 MongoDB_id)
  • 本地数据可以使用一个自增计数器或者 uuid 作为 key
  • key 的值只需在兄弟节点中保持唯一即可, 所以可以使用 index 作为 key

响应事件

1
2
3
4
5
6
function Button() {
function handleClick() {
alert('Hello, React!')
} // 事件处理函数通常在组件内部定义
return <button onClick={handleClick}>Click me</button>
}

事件冒泡

onScroll 外的所有事件都会冒泡, 除非使用 event.stopPropagation() 阻止冒泡

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Button(props) {
return ( // 在回调函数中写了 event.preventDefault()
<button onClick={props.onClick}>
Click me
</button>
)
}

// 也可以不在回调函数中写 event.preventDefault()
// 这样会比较灵活
function Button(props) {
return (
<button onClick={e => {
e.preventDefault()
props.onClick()
}}>
Click me
</button>
)
}

<Suspense>

<Suspense> 组件用于在加载组件时显示一个加载指示器, 以避免显示空白页面

1
2
3
4
5
import { Suspense } from 'react'

<Suspense fallback={<Loading />}>
<SomeComponent />
</Suspense>

Props

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
import { useState } from 'react'

// 在父元素中定义 state
function App() {
const [count, setCount] = useState(0)
function handleClick() {
setCount(count + 1)
}

return (
<div>
<Counter count={count} onClick={handleClick} />
<Counter count={count} onClick={handleClick} />
</div>
)
}

// 子组件接收 props
function Counter({ count, onClick }) {
return (
<div>
<button onClick={onClick}>点击次数: {count}</button>
</div>
)
}
  • JSXHTML 标签中, 属性实际上也是 props, 如 <img src="xxx" alt="xxx" /> 中的 srcalt 就是 props 对象的属性
  • <Counter count={count} onClick={handleClick} /> 中的 countonClickprops 对象的属性名, 并不会直接作用于子组件
  • props 对象是子组件函数的唯一参数 (可通过解构赋值获取内部元素)
  • props 也可以像函数形参那样有默认值, 如 function Counter({ count = 0, onClick })

props.children

props.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
24
function Card({ title, children }) {
return (
<div>
<h2>{title}</h2>
<div>{children}</div>
</div>
)
}

function Counter() {
const [count, setCount] = useState(0)
function handleClick() {
setCount(count + 1)
}
return <button onClick={handleClick}>点击次数: {count}</button>
}

export default function App() {
return (
<Card title="标题">
<Counter />
</Card>
)
}

最终渲染结果

1
2
3
4
5
6
7
<div>
<h2>标题</h2>
<div>
<!-- Counter 组件 -->
<button>点击次数: 0</button>
</div>
</div>

Hooks

Hook 是特殊的函数, 只在 React 渲染时有效, 能让你 HookReact 的特性, 如 state、生命周期等

Hookuse 开头, 如 useState, 只能在组件或自定义 Hook 的顶层调用, 不能在循环、条件语句中调用 (可以理解为在组件顶部导入 模块)

自定义 Hook

自定义 Hook 是一个函数, 其名称以 use 开头, 函数内部可以调用其他 Hook, 用以复用一些逻辑, 例如下面的 useOnlineStatus; 自定义 Hook 也应当是纯函数

useState

useStateReact 提供的一个内置 Hook, 用于定义组件的 state, 让组件能够保存和更新数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 引入 useState
import { useState } from 'react'
// 定义组件
function Counter() {
// 定义 state 变量
const [count, setCount] = useState(0)
// 定义事件处理函数
const handleClick = () => setCount(count + 1)
return (
<div>
// 使用 state 和 setState
<button onClick={handleClick}>点击次数: {count}</button>
</div>
)
}
1
2
3
// 使用 TypeScript
// 初始值非 any 类型时, 无需指定泛型类型
const [count, setCount] = useState<number>(0)
  • useState 接收一个参数, 即 state 的初始值
  • useState 返回一个数组, 第一个元素是 state, 第二个元素是更新 state 的函数
  • state 只能通过 setState 函数更新, 直接修改 state 不会触发重新渲染
  • 一个组件可以有多个 state; 每个组件的 state 是独立和私有的, 由 React 管理
  • statesetState 可以作为 props 传递给子组件, 以实现数据共享; 这种行为称为 状态提升

渲染机制

组件显示到屏幕之前,其必须被 React 渲染; 有两种情况会导致组建的渲染: 组件的初次渲染组件或其祖先的状态 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
// 初次渲染
ReactDOM.createRoot(document.querySelector('#root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
) // 这是 vite 创建项目时的 main.jsx 文件

// 状态发生变化
import { useState } from 'react'
export default function Form() {
// 定义 state 变量
const [isSent, setIsSent] = useState(false)
if (isSent) {
return <h1>Your message is on its way!</h1>
}
return (
<form onSubmit={e => {
e.preventDefault()
setIsSent(true)
}}>
// 点击按钮后, state 发生变化, 组件重新渲染
// 页面显示 "Your message is on its way!"
<button type="submit">Send</button>
</form>
)
}
  • React.StrictMode 是一个用于检测 React 应用中潜在问题的工具, 会在开发环境下检测副作用
  • 重新渲染一个组件时, React 会再次调用组件函数, 组件函数返回新的 JSX 元素, React 会比较新旧 JSX 元素, 仅更新需要更新的部分 (而不是整个元素乃至整个 DOM)
  • 事件处理函数执行完毕后才会触发重新渲染, 因此可以在事件处理函数中修改多个 state 变量, 只会触发一次重新渲染
  • setState 函数不会立即更新 state, 只会改变下次渲染时的 state, 见以下示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const [number, setNumber] = useState(0)
return (
<>
<h1>{number}</h1>
<button onClick={() => {
// 此时 number 为 0
setNumber(number + 1)
// 此时 number 仍为 0, 下次渲染时 number 为 0 + 1
setNumber(number + 1)
// 此时 number 仍为 0, 下次渲染时 number 为 0 + 1
setNumber(number + 1)
// 此时 number 仍为 0, 下次渲染时 number 为 0 + 1
alert(number) // 0
setTimer(() => {
alert(number) // 还是 0!! number 依照调用时的值
}, 10000)
}}>+3</button>
</>
) // 下次渲染时, number 为 1

总之, 一个 state 变量的值永远不会在一次渲染的内部改变

渲染函数

如果需要在下次渲染前, 多次更新同一个 state, 可以将一个函数传递给 setState

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const [number, setNumber] = useState(0)
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 1) // 相当于 (n => number + 1)
// 将 number = 1 加入队列
setNumber(number + 1)
// 将 number = 1 加入队列
setNumber(n => n + 1)
// 将 number = number + 1 加入队列
setNumber(n => n + 1)
// 将 number = number + 1 加入队列
setNumber(n => n + 1)
// 将 number = number + 1 加入队列
}}>+3</button>
</>
) // 下次渲染时, number 为 4

当渲染函数被传递给 setState 时, React 会将此函数加入队列, 以便在事件处理函数中的所有其他代码运行后进行处理; 在下一次渲染期间,React 会遍历队列并给你更新之后的最终 state

更新对象

state 中可以保存任意类型的 JavaScript 值,包括对象; 但是,你不应该直接修改存放在 React state 中的对象; 相反,当你想要更新一个对象时,你需要创建一个新的对象(或者将其拷贝一份),然后将 state 更新为此对象

简而言之, 应把 state 视作 Object.freeze 的对象, 不要直接修改 state 的属性, 而是同样使用 setState 函数给 state 赋新值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const [potion, setPotion] = useState({ x: 0, y: 0 })

// 错误示例
onPointerMove={e => {
potion.x = e.clientX
potion.y = e.clientY
}}

// 正确示例
onPointerMove={e => {
setPotion({
x: e.clientX,
y: e.clientY
})
}}
使用展开运算符
1
2
3
const [potion, setPotion] = useState({ x: 0, y: 0, z: 0 })

updateX = x => setPotion({ ...potion, x })

注意: 展开运算符只会复制对象的第一层属性 (浅拷贝)

使用 Immer 库
1
pnpm add use-immer
1
2
3
4
5
6
7
8
import { useImmer } from 'use-immer'
// 使用 useImmer 替代 useState

const [potion, setPotion] = useImmer({ x: 0, y: 0, z: 0 })

updateX = x => setPotion(draft => {
draft.x = x
})

更新数组

更新数组时, 也应该创建一个新数组, 而不是直接修改原数组

目的避免使用推荐使用
添加元素push, unshiftconcat, [...arr, item]
删除元素pop, shift, splicefilter, slice
替换元素splice, arr[i] = itemmap
排序sort, reverse先复制一份, 再排序
  • 如果数组中的元素是基本类型, 可以直接使用 ... 运算符复制数组, 然后修改其中的元素
  • 如果数组中的元素是对象, 可以在 map 方法中使用展开运算符复制对象, 然后修改其中的属性
  • 同样, 可以使用 useImmer 库来更新数组

状态管理

React 控制 UI 的方式是声明式的; 不必直接操作 DOM, 只需声明 UI 在给定状态下应该是什么样子, React 会自动处理 UI 的更新

赛博画师小叶子 为例, 命令式编程为 点击生成按钮 -> 禁用按钮 -> 设置按钮文本 -> ... -> 重置按钮文本 -> 启用按钮; 而声明式编程为 点击生成按钮 -> 生成按钮变为加载状态 -> ... -> 生成按钮变为可点击状态

状态的改变可能是因为用户的操作, 也可能是因为网络请求、定时器等外部因素

使用 state 的一些原则
  • 合并关联的 state: 如果你总是同时更新两个或更多的 state 变量,请考虑将它们合并为一个单独的 state 变量
  • 避免互相矛盾的 state: 当 state 结构中存在多个相互矛盾或不一致的 state 时,你就可能为此会留下隐患; 应尽量避免这种情况
  • 避免冗余的 state: 如果你能在渲染期间从组件的 props 或其现有的 state 变量中计算出一些信息,则不应将这些信息放入该组件的 state
  • 避免重复的 state: 当同一数据在多个 state 变量之间或在多个嵌套对象中重复时,这会很难保持它们同步; 应尽可能减少重复
  • 避免深度嵌套的 state: 深度分层的 state 更新起来不是很方便; 如果可能的话,最好以扁平化方式构建 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
// 合并关联的 state, 错误示例
const [x, setX] = useState(0)
const [y, setY] = useState(0)
const [z, setZ] = useState(0)
// 正确示例
const [state, setState] = useState({ x: 0, y: 0, z: 0 })

// 避免互相矛盾的 state, 错误示例
const [isSent, setIsSent] = useState(false)
const [isError, setIsError] = useState(false)
// 正确示例
const [status, setStatus] = useState('sent')

// 避免冗余的 state, 错误示例
const [firstName, setFirstName] = useState('')
const [lastName, setLastName] = useState('')
const [fullName, setFullName] = useState('')
// 正确示例
const [firstName, setFirstName] = useState('')
const [lastName, setLastName] = useState('')
const fullName = `${firstName} ${lastName}`

// 避免重复的 state, 错误示例
const [user, setUser] = useState({ name: 'xiaoyezi', age: 18 })
const [selectedUser, setSelectedUser] = useState({ name: 'xiaoyezi', age: 18 })
// 正确示例
const [user, setUser] = useState({ name: 'xiaoyezi', age: 18 })
const [selectedUserName, setSelectedUserName] = useState('xiaoyezi')

// 避免深度嵌套的 state
// 可以使用 useImmer 库

状态的保留和重置

  • 一个组件被卸载后, 其 state 会被销毁, 重新挂载时会重新初始化
  • 但在 UI 树中相同位置的相同组件 (但传入的属性可能不同, 如切换不同的显示状态或样式等) 会使 state 保留
  • 而在 UI 树中相同位置的不同组件 (如 divp) 会使 state 重新初始化 (因为此时是销毁后重新挂载, 而上面是更新); 这也是为什么不要在组件内部定义组件
  • 如果在相同位置重置 state, 可以将组件渲染在不同的位置 (见下面的示例), 或者在 key 属性中传入一个随机值
  • key 属性是 React 用来识别组件的唯一标识, 当 key 改变时, React 会销毁原有组件, 并重新创建新的组件 注意: key只需在兄弟节点中保持唯一即可
  • 如果想在组件销毁后保留 state, 可以使用状态提升, 不销毁而是隐藏组件, 存入 localStorage 等方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 渲染在同一个位置
return (
<div>
{isPlayerA ? <Player role="A" /> : <Player role="B" />}
</div>
)

// 渲染在不同的位置
return (
<div>
{isPlayerA && <Player role="A" />}
{isPlayerA || <Player role="B" />}
</div>
)

// 使用 key 属性
return (
<div>
<Player key={isPlayerA ? 'A' : 'B'} role={isPlayerA ? 'A' : 'B'} />
</div>
)

flushSync

在异步函数 (Promise)、定时器、自定义事件中, setState 会使 state 立即更新, 页面立即重新渲染; React18 之后, 也引入了在这些情况下的 batchedUpdates 机制, 会在安全的情况下将多个 setState 合并为一个更新

通常 React 能够正确判断是否需要合并更新 (如在 赛博画师小叶子 中, 提交表单后的事件处理函数内的 setState 不会被合并, 正如我期望的那样); 但是, 如果你需要在某些情况下立即更新 state, 可以使用 flushSync 函数

1
2
3
4
5
6
7
8
9
import { flushSync } from 'react-dom'

// 在异步函数中立即更新 state
async function fetchData() {
const data = await fetch('https://api.example.com/data')
flushSync(() => {
setData(data)
})
}

参见这篇文章

useReducer

对于拥有许多 state 更新逻辑的组件来说,过于分散的事件处理程序可能会令人不知所措; 对于这种情况,可以将组件的所有 state 更新逻辑整合到一个外部函数中,这个函数叫作 reducer

要将 state 更新逻辑整合到 reducer 中,可以通过以下三个步骤来实现:

  1. setState(value) 函数替换为 dispatch(action) 函数
  • setState 是告诉 React 要做什么, 而 dispatch 是告诉 React 用户做了什么 (即 action)
  • 传递给 dispatch 的参数称为 action 对象, 通常包含一个 type 属性, 用于描述 action 的类型
  1. 编写一个 reducer 函数
  • reducer 函数接收两个参数: state (当前状态) 和 action, 并返回一个新的 state (新状态)
  • React 会将 state 设置为 reducer 函数的返回值
  • 由于 reducer 接受 state 作为参数, 所以可以在组件外部定义 reducer 函数
  • 推荐在 reducer 函数中使用 switch (action.type) 语句, 以便根据 action.type 执行不同的操作
  1. 使用 useReducer Hook
  • 例如 const [state, dispatch] = useReducer(reducer, initialState)
  • 可以将 reducer 函数放在组件外部, 甚至是单独的文件中, 以便在多个组件之间共享
  • reducer 函数也应当是纯函数, 即输入相同则输出相同, 且不包含异步请求或副作用

use-immer 库提供了一个 useImmerReducer 函数, 用于在 reducer 函数中使用 Immer 库, 此时 reducer 函数接受 draft, action 两个参数, draftstate 的可变副本, 可以直接修改 draft

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 { useReducer } from 'react'

// 定义 reducer 函数
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 }
case 'decrement':
return { count: state.count - 1 }
default:
throw new Error()
}
}

// 使用 useReducer
export default function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 })
return (
<div>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<span>{state.count}</span>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
</div>
)
}

useContext

Context 提供了一种在组件之间共享值的方式, 而不必通过 props 一层层传递数据; 通过以下步骤使用 Context:

  • 首先, 在独立的文件引入 createContext 函数, 创建一个 Context 对象并导出; 例如 export const LevelContext = createContext(1), 其中 1Context 对象的初始值
  • 然后, 在需要共享数据的组件中, 使用 const xxx = useContext(Context) Hook 获取 Context 对象
  • 最后, 如果需要更新 Context 中的数据, 可以在父元素中使用 <Context.Provider value={data}>{children}</Context.Provider> 包裹子元素, 并在子元素中使用 useContext 获取 Context 对象 (此时 Context 对象的值为 value 属性的值)
  • 注意: 如果不使用 Context.Provider 更新 Context 中的数据, 则所有子孙组件中的 Context 对象的值都是初始值
  • 可以使用 const level = useContext(LevelContext)<LevelContext.Provider value={level + 1}>{children}</LevelContext.Provider> 这样的写法实现 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
// LevelContext.js
import { createContext } from 'react'
export const LevelContext = createContext(1)

// App.js
import { Section } from './Section'
export default function App() {
return (
<Section> // 显示当前层级: 1
<Section> // 显示当前层级: 2
<div>
<div>
<Section /> // 显示当前层级: 3
</div>
</div>
</Section>
</Section>
)
}

// Section.js
import { useContext } from 'react'
import { LevelContext } from './LevelContext'
export function Section({ children }) {
const level = useContext(LevelContext)
return (
<LevelContext.Provider value={level + 1}>
<div>当前层级: {level}</div>
{children}
</LevelContext.Provider>
)
}

注意事项

React19 之后, 无需再使用 Context.Provider, 直接使用 Context 即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// LevelContext.js
import { createContext } from 'react'
export const LevelContext = createContext(1)

// App.js
export default function App() {
return (
<>
<LevelContext value={1}>
<Section />
</LevelContext>
</>
)
}
  • Context 对象的 value 属性可以是任何类型的值, 包括函数, 对象, 数组等
  • 不要滥用 Context, 而是优先在层级不多时使用 props 传递数据
  • Context 常用于定义全局主题、用户信息、路由信息等
  • 通常将 reducer 函数和 Context 对象放在同一个文件中搭配使用, 以便管理复杂的状态

与 Reducer 搭配使用

useReducer 创建的 statedispatch 函数放在 Context 中, 可以在任何组件中使用 useContext 获取 statedispatch 函数, 以实现全局状态管理

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
// CounterContext.js
import { createContext, useReducer } from 'react'
const [count, dispatch] = useReducer(reducer, 0)
function reducer(state, action) {
switch (action.type) {
case 'increment':
return state + 1
case 'decrement':
return state - 1
default:
throw new Error()
}
}
const CounterContext = createContext({ count, dispatch })

// App.js
export default function App() {
return (
<>
<Counter />
<Counter />
</>
)
}

// Counter.js
import { useContext } from 'react'
import { CounterContext } from './CounterContext'
export default function Counter() {
const { count, dispatch } = useContext(CounterContext)
return (
<div>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<span>{count}</span>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
</div>
)
}

useRef

当希望在 React 组件中保存一个可变值, 但不希望因为值的改变而触发重新渲染时, 可以使用 useRef

ref 是一种脱围机制, 应当只在需要 跳出React 的情况下使用; 应当避免通过 ref 更改由 React 管理的 DOM 元素, 除非该元素不会被更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { useRef } from 'react'

export default function Counter() {
// 创建 ref
const ref = useRef(0)
// 使用 ref
function handleClick() {
ref.current += 1
alert('你点击了 ' + ref.current + ' 次!')
}
// 渲染
return (
<button onClick={handleClick}>
点击我!
</button>
)
}
1
2
// 使用 TypeScript
const ref = useRef<HTMLDivElement>(null)
  • useRef 返回一个可变的 ref 对象, 其 current 属性被初始化为传入的参数 (如果没有传入参数, 则为 undefined)
  • ref.current 属性可以保存任何值, 并且不会触发重新渲染, 可以用来保存计时器 IDDOM 元素引用、不需要被用来计算 JSX 的值等
  • 不应在渲染时修改或使用 ref.current 的值: 上面的例子中, 如果不是 alert 弹窗, 而是直接在 DOM 中显示点击次数, 则次数会随着点击次数的增加而增加, 但文本不会更新
  • 可以将 ref 视为没有 setStatestate

使用 ref 获取 DOM 元素

当将 ref 对象设置为 JSX 元素的 ref 属性时, ref.current 属性将引用该 DOM 元素; 而且, 当 DOM 元素被卸载时, ref.current 属性将被设置为 null

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { useRef } from 'react'

export default function App() {
// 为 input 元素创建 ref
const inputRef = useRef(null)
// 点击按钮时聚焦 input 元素
function focusInput() {
inputRef.current.focus()
}
// 渲染
return (
<div>
<input ref={inputRef} />
<button onClick={focusInput}>Focus</button>
</div>
)
}

如果需要给不定量的 DOM 元素添加 ref, 可已不将元素本身作为 ref 的值, 而是将一个数组或 Map 作为 ref 的值

将 ref 传递给子组件

React 默认不允许将 ref 属性传递给子组件 (如 <Cover />), 以使代码更健壮; 想要将自己的 DOM 节点暴露的组件必须用另一种方式来定义: forwardRef()

React19 及之后的版本中, ref 可以直接作为 props 传递给子组件, 不再需要 forwardRef 函数

1
2
3
4
5
6
7
8
9
10
// 父组件
export default function App() {
const childRef = useRef(null)
return <Child ref={childRef} />
}

// 子组件
export default function Child(props) {
return <input ref={props.ref} />
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { forwardRef, useRef } from 'react'

// 子组件
const Child = forwardRef((props, ref) => {
return <input ref={ref} />
})

// 父组件
export default function App() {
// 创建 ref
const childRef = useRef(null)
// 聚焦 Child 组件
function focusInput() {
childRef.current.focus()
}
// 渲染, 将 ref 传递给子组件
return (
<div>
<Child ref={childRef} />
<button onClick={focusInput}>Focus</button>
</div>
)
}

useImperativeHandle

useImperativeHandle 可以让你在使用 ref 时只将需要暴露给父组件的实例值暴露出去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { forwardRef, useImperativeHandle, useRef } from 'react'

// 子组件
const Child = forwardRef((props, ref) => {
// 创建自己的 ref
const inputRef = useRef(null)
// 通过 useImperativeHandle
// 只将 inputRef 的 focus 方法暴露给父组件 ref
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus()
}
}))
// 使用自己的 ref 绑定 input 元素
return <input ref={inputRef} />
})

// 父组件同上

ref 清理函数

React19 之后, 给元素添加 ref 属性时, 可以返回一个清理函数, 用于在元素被卸载时执行清理操作

1
2
3
4
5
6
7
8
9
<div ref={divRef => {
// 元素被挂载时执行
console.log('挂载')
return () => {
// 元素被卸载时执行
console.log('卸载')
}
}} />
// 在开发环境中, 将打印: 挂载, 卸载, 挂载

useEffect

Effect 允许组件连接到外部系统并与之同步; 这包括处理网络、浏览器、DOM、动画、使用不同 UI 库 (如 Vue) 编写的小部件以及其他非 React 代码

  • useEffect 用于由渲染本身, 而非用户点击等事件, 触发的副作用的操作; 它接收两个参数: (setup[, dependencies[]])
  • setup 函数在必要时应返回一个 清理函数, 用于清理副作用; 在组件渲染后, React 会调用 setup 函数 (如 连接), 并在组件卸载或更新时调用 清理函数 (如 断开连接)
  • 清理函数一般是断开连接、移除事件监听器、重置动画; 不管指不指定清理函数, 在开发环境中 useEffect 都会运行两次 (这个问题是正常的, 是为了检测副作用是否正确清理)
  • dependencies 数组用于指定 setup 函数的依赖项, 只有当依赖项发生变化时, setup 函数才会重新执行
  • 依赖项可以是 state 变量、propscontext 等任意渲染时可能改变的值; 不传入 dependencies 数组, 则 setup 函数在每次渲染时都会执行; 传入 [], 则 setup 函数只会在组件挂载时执行 (而 清理函数 只会在组件卸载时执行)
  • 自己refsetState 函数等稳定的标识符不需要放入 dependencies 数组中 (因为它们在每次渲染时都是相同的, 永远不会使 setup 函数执行)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 根据屏幕宽度随时更新 swiper 的 slidesPerView
const [slidesPerView, setSlidesPerView] = useState(3)
// 使用 useEffect
useEffect(() => {
// 事件处理函数
function updateSlidesPerView() {
if (window.innerWidth < 1000) {
setSlidesPerView(1)
} else if (window.innerWidth < 1500) {
setSlidesPerView(2)
} else {
setSlidesPerView(3)
}
}
// 初始化
updateSlidesPerView()
// 监听 resize 事件
window.addEventListener('resize', updateSlidesPerView)
// 清理
return () => {
window.removeEventListener('resize', updateSlidesPerView)
}
}, [])

移除不必要的 Effect

  • useEffect 通常只应用于与外部系统交互, 例如这段代码会陷入死循环: useEffect(() => setCount(count + 1))
  • 如果使用了 Next.js 等框架, 推荐使用这些框架提供的数据获取机制取代 useEffect
  • 某些逻辑如果只需要在应用启动时执行一次, 可以将他们放于组件外部, 而不是使用 useEffect (顶层代码会在组件被导入时执行一次, 但也不要滥用这种方式)
  • 一些昂贵的计算, 可以使用 useMemo Hook 缓存计算结果, 而不是在 useEffect 中计算
  • 如果想要在 props 变化时重置 state, 不要用 useEffect(() => setState(''), [props]) 这样的写法, 而是使用组件的 key 属性
  • 能放进事件处理函数的逻辑, 尽量不要放在 useEffect (灵活使用状态提升、Context 等)
  • 如果需要订阅外部 state 变化, 可以使用 useSyncExternalStore Hook, 而不是在 useEffect 中订阅
  • 在使用 useEffect 异步获取数据时, 可能出现数据竞争问题, 除了使用 Next.js 等框架提供的数据获取机制, 还可以使用以下自定义 Hook

未来版本的 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
function SearchResults({ query }) {
const [page, setPage] = useState(1)
const params = new URLSearchParams({ query, page })
const results = useData(`/api/search?${params}`)

function handleNextPageClick() {
setPage(page + 1)
}
// ...
}

function useData(url) {
const [data, setData] = useState(null)
useEffect(() => {
let ignore = false
fetch(url)
.then(response => response.json())
.then(json => {
if (!ignore) {
setData(json)
}
})
// 返回一个清理函数
// 忽略较早返回的异步请求
// 只有最新的请求才会更新数据
return () => {
ignore = true
}
}, [url])
return data
}

Effect 的生命周期

  • 组件的生命周期有三个阶段: 挂载、更新、卸载
  • useEffect 的生命周期有两个阶段: 开始同步 (主体代码) 和停止同步 (清理代码)
  • Effect 的生命周期与组件的生命周期不完全对应, 同步和停止同步都可能在组件的一个渲染周期内多次执行 (如果 dependencies 发生变化, 且该变化不会引起组件重新渲染)
  • 通过 useSyncExternalStore 获取的全局变量、ref.current 不能放入 dependencies 数组中, 因为它们打破了组件应是纯函数的原则; 配置了 eslint 时, 会有警告
  • dependencies 必须包含所有 useEffect 中使用的响应式值, eslint 会检测并给出警告, 此时应添加所有依赖项或eslint 证明其不需要这个依赖项 (即该变量是非响应式的), 如在组件外部定义该变量或函数
  • 一个 useEffect 应当只做一件事, 如果需要做多件事, 应当拆分成多个 useEffect, 从而使依赖项更加清晰和避免交叉干扰

useEffectEvent

useEffectEvent 是一个尚未在稳定版发布的实验性 Hook

如果在 useEffect 中同时包含一些响应式事件和非响应式事件, 那可能导致发生非响应式事件时, 由于 dependencies 的变化, 而不必要地触发一些行为

useEffectEvent 可以将事件处理函数提取到一个单独的 Hook 中, 以便在 useEffect 中使用

  • useEffectEvent 完全稳定后, 推荐永远不要禁用 eslint 的提示
  • 永远只应在 useEffect 中使用 useEffectEvent, 永远不要把 useEffectEvent 传递给其他 Hook 或组件
  • 通常在 useEffect 旁边定义 useEffectEvent
  • 为什么要用 useEffectEvent: 它是非响应式的, 只有在事件发生时才会执行, 例如, 如果从父组件传递的 props 中获取了一个处理函数, 但不想把它放入 dependencies 数组中, 可以使用 useEffectEvent (此时这个传入的处理函数可以变化, 但不会触发 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
// 错误示例
function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId)
connection.on('connected', () => {
showNotification('Connected!', theme)
})
connection.connect()
return () => connection.disconnect()
}, [roomId, theme]) // ❌ 切换主题时会重新连接

return <h1>Welcome to the {roomId} room!</h1>
}

// 正确示例
function ChatRoom({ roomId, theme }) {
// 声明 onConnected 事件
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme)
})
// 使用 useEffectEvent
useEffect(() => {
const connection = createConnection(serverUrl, roomId)
connection.on('connected', () => {
onConnected()
})
connection.connect()
return () => connection.disconnect()
}, [roomId]) // ✅ 只有 roomId 变化时会重新连接

return <h1>Welcome to the {roomId} room!</h1>
}
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 Page({ url }) {
const { items } = useContext(ShoppingCartContext);
const numberOfItems = items.length;

useEffect(() => {
logVisit(url, numberOfItems);
}, [url]); // 🔴 React Hook useEffect 缺少依赖项: ‘numberOfItems’
// ...
}

// 正确示例
function Page({ url }) {
const { items } = useContext(ShoppingCartContext);
const numberOfItems = items.length;

const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, numberOfItems);
});

useEffect(() => {
onVisit(url);
}, [url]); // ✅ 声明所有依赖项
// ...
}

useMemo

useMemo(calculateValue, dependencies) 用于缓存计算结果, 只有当 dependencies 数组中的值发生变化时, 才会重新计算

  • calculateValue 应当是没有任何参数和副作用的纯函数, React 会将其返回值缓存起来, 并在 dependencies 数组中的值发生变化时重新计算
  • dependencies 数组可以是 state 变量、propscontext
  • 类似于 useEffect, 在开发环境中, useMemo 也会运行两次, 从而帮助开发者检测潜在的错误
  • 除非有特定原因, React 不会丢弃缓存的值
  • 第一次渲染时, useMemo 会计算并缓存值, 然后将其返回; 之后的渲染, useMemo 会比较 dependencies 数组中的值是否发生变化, 如果没有变化, 则直接返回缓存的值
  • 可以用 console.time('xxxA'); xxxxxxx; console.timeEnd('xxxA') 来测试一个操作的耗时, 从而决定是否使用 useMemo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { useMemo, useState } from 'react'

export default function App() {
const [number, setNumber] = useState(1000000)
const sum = useMemo(() => {
let result = 0
for (let i = 1; i <= number; i++) {
result += i
}
return result
}, [number])
return (
<>
<input value={number} onChange={e => setNumber(e.target.value)} />
<div>1 + 2 + ... + {number} = {sum}</div>
</>
)
}

useSyncExternalStore

useSyncExternalStore(subscribe, getSnapshot[, getServerSnapshot]) 用于订阅外部 state 变化, 并在组件卸载时取消订阅

  • store: 一个外部的可以改变的 state
  • subscribe:一个函数,接收一个单独的 callback 参数并把它订阅到 store 上; 当 store 发生改变,它应当调用被提供的 callback; 这会导致组件重新渲染; subscribe 函数会返回清除订阅的函数
  • getSnapshot:一个函数,返回组件需要的 store 中的数据快照; 在 store 不变的情况下,重复调用 getSnapshot 必须返回同一个值; 如果 store 改变,并且返回值也不同了(用 Object.is 比较),React 就会重新渲染组件
  • getServerSnapshot:可选, 一个函数,返回 store 中数据的初始快照; 它只会在服务端渲染时,以及在客户端进行服务端渲染内容的 hydration 时被用到; 快照在服务端与客户端之间必须相同,它通常是从服务端序列化并传到客户端的; 如果你忽略此参数,在服务端渲染这个组件会抛出一个错误
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 订阅 navigator.onLine 的变化
// 设计一个自定义 Hook
import { useSyncExternalStore } from 'react'

export function useOnlineStatus() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot)
return isOnline
}

function subscribe(callback) {
window.addEventListener('online', callback)
window.addEventListener('offline', callback)
return () => {
window.removeEventListener('online', callback)
window.removeEventListener('offline', callback)
}
}

function getSnapshot() {
return navigator.onLine
}
1
2
3
4
5
6
7
8
9
10
11
// 使用自定义 Hook
import { useOnlineStatus } from './useOnlineStatus'

export default function App() {
const isOnline = useOnlineStatus()
return (
<div>
<p>{isOnline ? 'Online' : 'Offline'}</p>
</div>
)
}

本段导航

React19 提供了一些列无需使用 useEffect 就能向 head 标签添加 metatitlelink 等标签的 API

Preload

React19 提供了一系列 preload 方法, 用于在组件加载时预加载资源, 以提高性能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { ... } from 'react-dom'

export default function Component() {
// 预加载脚本
preinit('https://example.com/script.js', { as: 'script' })
// 预加载字体
preload('https://example.com/font.woff', { as: 'font' })
// 预加载样式表
preload('https://example.com/style.css', { as: 'style' })
// 预查询 DNS
prefetchDNS('https://example.com')
// 预连接
preconnect('https://example.com')

return (
<div>
...
</div>
)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 渲染结果 -->
<html>
<head>
<script async src="https://example.com/script.js"></script>
<link rel="preload" as="font" href="https://example.com/font.woff" />
<link rel="preload" as="style" href="https://example.com/style.css" />
<link rel="prefetch-dns" href="https://example.com" />
<link rel="preconnect" href="https://example.com" />
</head>
<body>
<div>
...
</div>
</body>
</html>

React19 可以在组件中引入所需样式表, 并可通过 precedence 属性设置样式表的优先级

1
2
3
4
5
6
7
8
function App() {
return (
<div>
<link rel="stylesheet" href="foo" precedence="high" />
<link rel="stylesheet" href="bar" precedence="default" />
</div>
)
}

Meta

React19 可以在组件中引入所需 meta 标签, 会自动替换 head 中的 meta 标签

1
2
3
4
5
6
7
8
function Component() {
return (
<div>
<meta name="author" content="XiaoYeZi" />
<meta name="keywords" content="React, JavaScript" />
</div>
)
}

Title

React19 可以在组件中引入所需 title 标签, 会自动替换 head 中的 title 标签

1
2
3
4
5
6
7
function Component() {
return (
<div>
<title>React19</title>
</div>
)
}

SSR

React19 正式支持 ServerComponents (太逆天了, Next.js 文档反而先有了这部分内容), 用于服务端渲染 React 组件

🚧’use client’ & ‘use server’

🚧ServerComponents

🚧ServerActions

⭐Next.js

Next.js 是一个基于 React 的全栈框架, 用于构建 React 应用程序, 由 Vercel 开发和维护; 详见官方文档

1
2
# 使用 create-next-app 创建项目
pnpm create next-app

服务端组件

Next.js 默认进行服务端渲染 SSR, 因此组件必须是服务端组件 (Server Components), 以便在服务端渲染时使用; 它有以下特点:

  • 没有状态和生命周期,也就不能使用 useState()useReducer()useEffect()useLayoutEffect()
  • 不能使用浏览器相关的 API,如 windowdocumentnavigator
  • 不能使用基于状态和生命周期的自定义 hook,以及浏览器相关的工具库
  • 可以使用服务端数据源,如文件系统、数据库、内部微服务
  • 可以渲染其他服务端组件、客户端组件以及原生 DOM 元素
  • 要使用客户端组件,必须在组件或其祖先组件的第一行添加 'use client'

🚧路由

Next.js 中, 不需要手动配置路由, 每个 app 目录下的文件夹都会被映射为一个路由

  • app/ 对应 /
  • app/about/ 对应 /about
  • 每个路由文件夹内都有一些特定名称的文件, 可以是 jsjsxtsx:
    1. page.jsx: 该路由的主组件
    2. layout.jsx: 该路由及其子路由的的组件会被作为 layout 组件的子组件; 用于定义共享 UI
    3. loading.jsx: 该组件及其子组件在加载时会显示的组件
    4. not-found.jsx: 该组件及其子组件在路由未找到时会显示的组件
    5. error.jsx: 该组件及其子组件在路由发生错误时会显示的组件
    6. route.js: 服务端 API 的路由文件 (只能和 page.jsx 二选一)
  • 注意: 没有 page.jsxroute.js 的文件夹不会被映射为路由
  • 此外, /public 文件夹下的文件也会被映射为路由

⭐Docusaurus

Docusaurus 是一个用于构建文档或博客网站的开源工具, 类似于 Vue 领域的 VuePress

⭐React Router

⭐Solid

设计挺不错的, 但是生态不如 ReactVue; 文档也没 React 写得好, 太太太简略了

Solid 是一个更高性能 React 替代品, 二者的设计理念和语法非常相似

Solid 组件是彼此完全独立的, 并且组件函数只会被调用一次,

1
2
3
4
# 使用 Vite 创建 Solid 项目
pnpm create vite
# 选择 Solid 模板
# 你会发现项目结构和 React 项目一模一样
1
2
3
4
5
6
// 入口文件 index.jsx
import { render } from 'solid-js/web'
import './index.css'
import App from './App'
const root = document.getElementById('root')
render(() => <App />, root)

官方的迁移建议

Solid 的更新模型与 React 完全不同,甚至与 React + MobX 完全不同; 不要将函数组件视为 render 函数,而是将它们视为 constructor

Solid 中,propsstoreproxy 对象,它们依赖于属性访问来进行跟踪和响应式更新; 注意解构或提前属性访问,这可能导致这些属性失去反应性或在错误的时间触发

Solidprimitive 没有像 Hook 规则这样的限制,因此您可以随意嵌套它们

你不需要使用列表行上的显式 key 来实现具有 key 的行为

React 中,每当修改输入字段时都会触发 onChange,但这不是 onChange 原生工作的方式; 在 Solid 中,使用 onInput 订阅每个值的更改

Props

Solid 的组件也是一个函数, 其参数也是 props, 但不应通过解构 props 来访问属性, 而应当直接访问 props 对象以确保属性的响应性

如果要传递给子组件的 props 比较多, 可以使用 props 对象和展开运算符 {...props} 传递

children

props.children 是一个特殊的 props, 用于访问组件的子组件

Solid 如此高性能的部分原因是 Solid 的组件基本上只是函数调用; 这意味着这些 props 会被惰性求值; props 的访问将被推迟到某些地方有用到它们; 这保留了响应性,而不会引入无关的封装代码或同步行为; 然而,这意味着存在子组件或元素的情况下,重复访问可能会导致重新创建

大多数情况下,你只是将这些入参属性插入到 JSX 中,所以不会有问题; 但是由于 children 元素可能会被重复创建,所以当你处理 children 时需要格外小心

出于这个原因,Solid 提供了 children 工具函数; 此方法既会根据 children 访问创建 memo 缓存,还会处理任何嵌套的子级响应式引用,以便可以直接与 children 交互

1
2
3
4
5
6
// child.jsx
export default function ColoredList(props) {
const c = children(() => props.children)
createEffect(() => c().forEach(item => item.style.color = props.color))
return <>{c()}</>
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// parent.jsx
import ColoredList from './child'

export default function App() {
const [color, setColor] = createSignal("purple")

return <>
<ColoredList color={color()}>
<For each={["Most", "Interesting", "Thing"]}>{item => <div>{item}</div>}</For>
</ColoredList>
<button onClick={() => setColor("teal")}>Set Color</button>
</>
}

mergeProps

mergeProps 函数用于将 props 对象与其他对象合并, 并确保 props 对象的响应性

1
2
3
4
5
6
import { mergeProps } from 'solid-js'

export default function MyComponent(props) {
const mergedProps = mergeProps(props, { className: 'my-class' })
return <div {...mergedProps}>Hello, World!</div>
}

splitProps

splitProps 函数用于将 props 对象拆分为两个对象而不失去响应性

1
2
3
4
5
6
import { splitProps } from 'solid-js'

export default function MyComponent(props) {
const [myProps, divProps] = splitProps(props, ['myProp'])
return <div {...divProps}>Hello, World!</div>
}

响应式接口

Signal

相当于 ReactuseState,但是 createSignal 返回的是 gettersetter, 因此要使用某个值, 是 xxx() 而不是 xxx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { createSignal } from 'solid-js'

export default function Counter() {
const [count, setCount] = createSignal(0)
return (
<div>
{/* 传入新值 */}
<button onClick={() => setCount(count() + 1)}>+</button>
<span>{count()}</span>
{/* 传入函数 */}
<button onClick={() => setCount(c => c - 1)}>-</button>
</div>
)
}

Effect

相当于 ReactuseEffect, 但不需要传入依赖项数组 (内部用到的 Signal 会自动成为依赖项), 会在渲染完成会执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { createEffect } from 'solid-js'

export default function Counter() {
const [count, setCount] = createSignal(0)
createEffect(() => {
console.log('count:', count())
})
return (
<div>
<button onClick={() => setCount(count() + 1)}>+</button>
<span>{count()}</span>
<button onClick={() => setCount(count() - 1)}>-</button>
</div>
)
}

Memo

相当于 ReactuseMemo, 传入一个计算函数 (同样无需传入依赖项数组), 返回一个 getter 函数 (只读)

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
// 以计算斐波那契数列为例
import { createMemo, createSignal } from 'solid-js'

function fibonacci(n) {
if (n <= 1) return n
return fibonacci(n - 1) + fibonacci(n - 2)
}

// 如果不用 memo, 每次点击按钮都会计算 5 次斐波那契数列
export function Fibonacci() {
const [n, setN] = createSignal(0)
const fib = () => fibonacci(n())
return (
<div>
<input type="number" value={n()} onInput={e => setN(+e.target.value)} />
<div>{fib()}{fib()}{fib()}{fib()}{fib()}</div>
</div>
)
}

// 使用 memo, 只会计算一次
export function Fibonacci() {
const [n, setN] = createSignal(0)
const fib = createMemo(() => fibonacci(n()))
return (
<div>
<input type="number" value={n()} onInput={e => setN(+e.target.value)} />
<div>{fib()}{fib()}{fib()}{fib()}{fib()}</div>
</div>
)
}

Store

StoreSolid 处理嵌套响应式的解决方案; Store 是代理对象, 其属性可以被跟踪, 并且可以包含其他对象, 这些对象会自动包装在代理中, 等等

为了让事情变得简单, Solid 只为在跟踪范围内访问的属性创建底层 Signal; 因此, Store 中的所有 Signal 都是根据要求延迟创建的

1
2
3
4
5
6
7
8
9
import { createStore } from 'solid-js/store'

const [store, setStore] = createStore({ todos: [] })
const addTodo = text => {
setStore('todos', todos => [...todos, { id: ++todoId, text, completed: false }])
}
const toggleTodo = id => {
setStore('todos', todo => todo.id === id, 'completed', completed => !completed)
}
produce

produce 函数用于在 Store 中进行复杂的更新, 类似于 useImmer

1
2
3
4
5
6
7
8
9
10
11
12
13
import { createStore, produce } from 'solid-js/store'

const [store, setStore] = createStore({ todos: [] })
const addTodo = text => {
setStore('todos', produce(todos => {
todos.push({ id: ++todoId, text, completed: false })
}))
}
const toggleTodo = id => {
setStore('todos', todo => todo.id === id, produce(todo => {
todo.completed = !todo.completed
}))
}

Context

ContextSolid 的上下文解决方案, 用于在组件树中传递数据, 而不必一级一级手动传递 props

ContextReactContext 类似, 但是 SolidContext 是响应式的, 因此可以在 Context 中使用 SignalStore

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
// counter.jsx
import { createSignal, createContext, useContext } from 'solid-js'

const CounterContext = createContext()

export function CounterProvider(props) {
const [count, setCount] = createSignal(props.count || 0)
const store = [
count,
{
increment() {
setCount(c => c + 1);
},
decrement() {
setCount(c => c - 1);
}
}
]
return (
<CounterContext.Provider value={store}>
{props.children}
</CounterContext.Provider>
)
}

export function useCounter() { return useContext(CounterContext) }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// app.jsx
import { CounterProvider, useCounter } from './counter'

export default function App() {
const [count, { increment, decrement }] = useCounter()

return (
<CounterProvider count={10}>
<h1>Welcome to Counter App</h1>
<div>{count()}</div>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</CounterProvider>
)
}

流程控制

除了像 React 一样的条件渲染和循环渲染, Solid 还提供了一些更加直观的流程控制组件

Show

Show 是一个条件渲染组件, 比逻辑中断和三元表达式更加直观

1
2
3
4
5
6
7
8
9
<Show 
when={condition}
fallback={<div>条件不满足时显示的内容</div>}
>
<div>条件满足时显示的内容</div>
</Show>

// 相当于
{condition ? <div>条件满足时显示的内容</div> : <div>条件不满足时显示的内容</div>}

For

For 是一个循环渲染组件, 可以用来遍历数组或对象; index 是一个 Signal, 所以将在移动 index 时重新渲染, 而 item 不是

1
2
3
4
5
6
7
8
9
10
11
12
<For 
each={array}
fallback={<div>数组为空时显示的内容</div>}
>
{(item, index) => (
<div>{index}: {item}</div>
)}
</For>

// 相当于
const content = array.map((item, index) => <div>{index()}: {item}</div>)
{content.length ? content : <div>数组为空时显示的内容</div>}

Index

类似于 For, 但 index 不是 Signalitem

1
2
3
4
5
6
7
8
<Index
each={array}
fallback={<div>数组为空时显示的内容</div>}
>
{(item, index) => (
<div>{index}: {item()}</div>
)}
</Index>

Switch

Switch 用于处理多个条件的情况 (此时用 Show 会显得很臃肿)

1
2
3
4
5
6
7
8
9
10
11
12
13
<Switch
fallback={<div>所有条件都不满足时显示的内容</div>}
>
<Match when={condition1}>
<div>条件 1 满足时显示的内容</div>
</Match>
<Match when={condition2}>
<div>条件 2 满足时显示的内容</div>
</Match>
<Match when={condition3}>
<div>条件 3 满足时显示的内容</div>
</Match>
</Switch>

Dynamic

<Dynamic> 标签处理根据数据渲染时很有用; <Dynamic> 可以让你将元素的字符串或组件函数传递给它,并使用提供的其余 props 来渲染组件

这通常比编写多个 <Show><Switch> 组件更简练

1
2
3
4
5
6
7
8
9
10
11
12
13
// 用 Switch
<Switch fallback={<BlueThing />}>
<Match when={selected() === "red"}>
<RedThing />
</Match>
<Match when={selected() === "green"}>
<GreenThing />
</Match>
</Switch>

// 用 Dynamic
<Dynamic component={options[selected()]} />
// options = { red: RedThing, green: GreenThing }

Portal

类似于 <dialog open>,并且会把元素提取出来放到 body 中,以避免 z-index 问题

1
2
3
<Portal>
<div>这个 div 会被放到 body 中</div>
</Portal>

ErrorBoundary

源自 UIJavaScript 错误不应破坏整个应用程序; 错误边界 ErrorBoundary 是一个可以捕获子组件树任何位置产生的 JavaScript 错误,错误边界 ErrorBoundary 会记录这些错误,并显示回退 UI 而非崩溃的组件树

1
2
3
4
5
<ErrorBoundary
fallback={<div>出现错误时显示的内容</div>}
>
<SomethingMightBroken />
</ErrorBoundary>

生命周期

本段导航

Solid 中只有少量的生命周期,因为一切的存活销毁都由响应系统控制; 响应系统是同步创建和更新的,因此唯一的调度就是将逻辑写到更新结束的 Effect

onMount

onMount 就是只执行一次的 Effect, 相当于 useEffect(() => {}, []) 的开始阶段

1
2
3
4
5
6
7
8
9
10
11
12
import { onMount } from 'solid-js'

export default function Counter() {
onMount(() => {
console.log('mounted')
})
return (
<div>
Hello World
</div>
)
}

声明周期仅在浏览器中运行, 因此将代码放在 onMount 会让他们在 SSR 时不会运行

onCleanup

在任何地方都可以使用 onCleanup,包括组件Effect 中,它会在组件销毁或 Effect 重新运行时运行

1
2
3
4
5
6
7
8
9
10
11
12
import { onCleanup } from 'solid-js'

export default function Counter() {
onCleanup(() => {
console.log('cleaned up')
})
return (
<div>
Hello World
</div>
)
}

绑定

本段导航

事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function App() {
const [pos, setPos] = createSignal({x: 0, y: 0})

function handleMouseMove(event) {
setPos({
x: event.clientX,
y: event.clientY
})
}

return (
<div onMouseMove={handleMouseMove}>
The mouse position is {pos().x} x {pos().y}
</div>
)
}

样式

Solid 传入对象设置样式时, 不采用驼峰命名法, 而是与 CSS 一致的短横线命名法

1
2
3
4
5
6
7
8
9
<div 
style={{
color: 'red',
'font-size': '20px',
'--custom-color': '#333'
}}
>
Hello World
</div>
类名

Solid 支持使用 classclassName 来以字符串形式设置类名; 但提供了一个 classList 属性, 用于以对像 { 'class-name': isEnable, ... } 的形式设置类名

1
2
3
4
5
6
7
8
9
10
11
12
// 以字符串形式设置类名
<div
class={isEnable ? 'class-name' : ''}
></div>

// 以对象形式设置类名
<div
classList={{
'class-name': isEnable,
'another-class-name': !isEnable
}}
></div>

Ref

类似于 ReactuseRef, 但 Solid 不需要声明这是一个 ref, 只需将任意变量传递给组件的 ref 属性即可

1
2
3
4
5
6
7
8
9
export default function App() {
let ref
return (
<div>
<span ref={ref}>Hello World</span>
<button onClick={() => ref.textContent = 'Clicked'}>Click</button>
</div>
)
}
Forward Ref

Solid 也支持 forwardRef, 但是不需要使用 forwardRef 函数, 只需将 ref 传递给子组件, 子组件通过 props.ref 获取 ref 并绑定到元素上即可

1
2
3
4
5
6
7
8
9
10
11
12
13
function Child(props) {
return <div ref={props.ref}>Hello World</div>
}

export default function App() {
let ref
return (
<div>
<Child ref={ref} />
<button onClick={() => ref.textContent = 'Clicked'}>Click</button>
</div>
)
}

use:

Solid 通过 use:xxx 语法支持自定义指令, 实际上是 ref 的一个语法糖, 支持在同一个元素上有多个绑定而不会冲突 (就像 addEventListener 一样)

自定义指令函数接受两个参数: elementvalueAccesor, element 是当前元素, valueAccesor 是一个获取绑定值的函数

use: 需要被编译器检测并进行转换,并且函数需要在作用域内,因此不能作为传值的一部分或在组件上使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// App.jsx
import { createSignal, Show } from "solid-js"
import clickOutside from "./click-outside"

function App() {
const [show, setShow] = createSignal(false)

return (
<Show
when={show()}
fallback={<button onClick={(e) => setShow(true)}>Open Modal</button>}
>
<div class="modal" use:clickOutside={() => setShow(false)}>
Some Modal
</div>
</Show>
)
}
1
2
3
4
5
6
7
8
9
10
11
// click-outside.js
import { onCleanup } from "solid-js"

export default function clickOutside(el, accessor) {
// accesor()?.() 是什么逆天东西
const onClick = (e) => !el.contains(e.target) && accessor()?.()

document.body.addEventListener("click", onClick)

onCleanup(() => document.body.removeEventListener("click", onClick))
}

🚧响应性

🚧异步

  • 标题: React/Solid学习笔记
  • 作者: 小叶子
  • 创建于 : 2024-03-18 20:25:46
  • 更新于 : 2024-05-20 15:13:42
  • 链接: https://blog.leafyee.xyz/2024/03/18/React学习笔记/
  • 版权声明: 版权所有 © 小叶子,禁止转载。
评论