Primary React (1)

Primary React (1)

React 是我在去年十月份的时候学的,当时为参加一个相关的项目做准备,可惜后面项目黄了

这学期 chp 的 web 课程需要我捡起 React 里的相关知识,所以作此笔记用于复习学习

内容主要是 mosh 的 React 初级课程,中级课程当时只学了一半,后面学的时候会把剩余的加上

本文是 React 基础笔记的第一部分,包含 React 总览与 React 的基础概念两部分

React 总览

这部分内容包含对 React 总体知识的概括总结

何为 React

React 是一个组件式的 JavaScript 库,用于开发 Web 应用程序

React 文档

创建 React 框架 App

使用 vite 创建一个基于 React 的 app:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ npm create vite@4.1.0

> npx
> cva

✔ Project name: … react-app
✔ Select a framework: › React
✔ Select a variant: › TypeScript

Scaffolding project in /Users/qiushao/react-app...

Done. Now run:

cd react-app
npm install
npm run dev

按照指示进行即可,我们这里选择的是 typescript 语言,比 js 多了类型检查

1
2
$ npm i
$ npm run dev

如此便可在本地端口看见我们的 React 应用框架

React 项目框架结构

使用 cursor/vs code 打开我们的 React 项目:

1
$ cursor .

  • node_modules:所有的第三方库,可以在命令行中继续引入
  • public:放置网站公共资源的地方,例如图片,视频等等
  • src:源代码文件夹,我们写的所有代码都在这里面
  • index.html:用于呈现最终效果的 HTML 文件,里面包含了我们的 React 组件
  • json配置文件:包括我们的依赖项、开发依赖项、ts 配置项等等,一般不用管
    • 注意:在安装别的依赖包如 antd 的时一般需要加上--save选项来保存在 json 中
    • 这样别人拿到项目源码后可以使用npm install命令直接安装我们的所有依赖

创建第一个 React 组件

React 是一个组件式的开发框架,旨在让我们在开发 Web 应用程序的时候,可以将前端的开发需求转换成一个个的组件来开发,最后再将组件组合起来即得到我们最终的界面

回到 src 源代码文件夹,创建 Message.tsx 文件用于放置我们的第一个组件

目前推荐的方式是使用函数式组件,即将组件开发成函数的形式,不同层级的函数组件中间使用接口来传递

1
2
3
4
5
6
7
8
9
10
11
function Message() {
const name = "World";
return (
<div>
<h1>Hello {name} !</h1>
</div>
);
}

export default Message;
// src/Message.tsx

这种语法叫做 jsx(java script XML),在编译部署的时候将会被编译成 javascript 语言,并最终呈现在浏览器上

我们可以使用大括号 {} 来包含任何想要表示的语句,如变量,表达式,甚至可以是函数等

同样的,我们的组件也可以用类似 HTML 的方式直接写在主组件 app 里:

1
2
3
4
5
6
7
8
import Message from "./Message";

function App() {
return <Message />;
}

export default App;
// src/App.tsx

React 的工作方式

我们的组件经过层层堆叠,最后会呈现出组件树的形式,而组件树又会被 React 本身翻译成一个虚拟 dom 树

这里的虚拟 dom 树不同于浏览器的 dom,这里的 dom 仅仅是我们的组件的一个轻量级的表示,当组件的状态或者数据发生更改时,React 负责找寻哪些节点发生了更改,并最终在浏览器的实际 dom 中呈现出来

实际上这部分内容由 React-dom 库来完成,可以在对应的 json 文件里找到

另一个值得注意的点是,不同于 vue 这样的开发框架,我们的 React 只是一个。如果说开发框架是一个工具集的话那么库本身就只是一个具体的工具

不过 React 好就好在它有一系列配套的库以及其他工具如 router,query 等,共同构建了 React 的开发生态

React 基础概念

本部分内容以创建一个实际的列表组件为例讲解 React 的基础概念,如创建组件,引入其他框架,条件渲染,处理 events 等

创建组件

使用 HTML 默认的格式去创建应用程序外观可能不太美观,因此我们需要引入一些第三方库来美化我们的外观

引入第三方库

以 CSS 库 bootstrap 为例:

1
$ npm i bootstrap@5.2.3

这时候我们可以删除原有的 CSS 文件,并在 main.tsx 里引入我们的新的 CSS 样式:

1
2
3
4
5
6
7
8
9
10
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
import "bootstrap/dist/css/bootstrap.css";

createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>
);

创建新的组件

通常我们会在我们的项目 src 目录下再创建 components 目录来存放我们的组件:

这里有一个快捷件 rafce,可以快速写出一个组件的框架及导出,需要安装 ES7 插件

新的组件如果要使用新的样式,我们可以通过添加具体的 CSS 类来说明,这里具体怎么写可以通过查阅 bootstrap 官方文档来说明

1
2
3
4
5
6
7
8
9
10
11
12
function ListGroup() {
return (
<ul className="list-group">
<li className="list-group-item">111</li>
<li className="list-group-item">222</li>
<li className="list-group-item">333</li>
<li className="list-group-item">444</li>
</ul>
);
}

export default ListGroup;

这样我们得到的列表就会具有一定的样式了,而不再是单独的 HTML 原生样式

Fragment

jsx 格式里一个函数返回的元素是会被编译成 JS 代码的,所以这里只能返回一个元素

但是如果我想要返回多个元素的整体该怎么办呢?可以使用Fragment标签包围起来——或者更简单的,直接用空标签 <></>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function ListGroup() {
return (
<>
<h1>ListGroup</h1>
<ul className="list-group">
<li className="list-group-item">111</li>
<li className="list-group-item">222</li>
<li className="list-group-item">333</li>
<li className="list-group-item">444</li>
</ul>
</>
);
}

export default ListGroup;

动态渲染

到目前为止我们的列表还是使用硬编码的,但是实际情况下我们的数据通常是从后端获得,这时候便需要对列表进行动态的渲染

使用数组的 map 方法来实现这一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function ListGroup() {
const items = ["New York", "San Francisco", "Tokyo", "London", "Paris"];
return (
<>
<h1>ListGroup</h1>
<ul className="list-group">
{items.map((item) => (
<li key={item} className="list-group-item">
{item}
</li>
))}
</ul>
</>
);
}

export default ListGroup;

条件渲染

有时候从后端拿到的数据不敢保证其是否为空,这时候便需要条件渲染,根据数据的情况来判断渲染的模式

当然可以使用 if 语句,但是这样会使得代码变得冗长,我们可以使用 js/ts 语言的一种特殊性质——&&运算符来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function ListGroup() {
let items = ["New York", "San Francisco", "Tokyo", "London", "Paris"];
items = [];
return (
<>
<h1>ListGroup</h1>
{items.length === 0 && <p>No items found</p>}
<ul className="list-group">
{items.map((item) => (
<li key={item} className="list-group-item">
{item}
</li>
))}
</ul>
</>
);
}

export default ListGroup;

注意代码中间的花括号,花括号是一切动态/条件渲染的必需,其内部可以是任何 js 表达式

(到目前为止还没有体现出 ts 的特性)

所以自然也可以使用函数的形式来优化代码(这里就没有必要了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function ListGroup() {
let items = ["New York", "San Francisco", "Tokyo", "London", "Paris"];
// items = [];
function getMessage() {
return items.length === 0 && <p>No items found</p>;
}
return (
<>
<h1>ListGroup</h1>
{getMessage()}
<ul className="list-group">
{items.map((item) => (
<li key={item} className="list-group-item">
{item}
</li>
))}
</ul>
</>
);
}

export default ListGroup;

处理事件

点点鼠标,浏览器便会呈现出各种各样的内容,这种处理事件的操作又是如何实现的?

以点击鼠标为例,每个 React 对象都有一个 onClick 的操作,用来处理被点击后的操作

1
2
3
4
5
6
7
<li
key={item}
className="list-group-item"
onClick={() => console.log("Clicked")}
>
{item}
</li>

这里我们传入了一个箭头函数,作用是在控制台打印相关内容

实际上这里会触发浏览器对应的 event,我们也可以:

1
2
3
4
5
6
7
<li
key={item}
className="list-group-item"
onClick={(event) => console.log(event)}
>
{item}
</li>

看看这个 event 是什么:

在 React 中它是 MouseEvent,我们可以将这段处理的代码抽象成函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { MouseEvent } from "react";

function ListGroup() {
// ...
function handleClick(event: MouseEvent) {
console.log(event);
}
return (
<>
<h1>ListGroup</h1>
{getMessage()}
<ul className="list-group">
{items.map((item) => (
<li key={item} className="list-group-item" onClick={handleClick}>
{item}
</li>
))}
</ul>
</>
);
}

export default ListGroup;

注意把处理函数提升到组件时,便需要指定其参数类型了,否则 ts 编译器会哈气,告诉你这样是不行的

这时候你在后面的 event 后加点也无法弹出任何属性

这便是 typescript 的特性,在参数类型不确定的时候必须显式指出参数对应的类型,这样便可避免掉很多 js 里写代码时会出现的 bug

管理状态

现在我们想要在点击列表项的时候实现高亮——很自然的把这个问题分为两部分:高亮与点击事件,前者是好办的,直接使用 CSS 即可

但是后者呢?直接在组件内处理 onClick 函数么?很自然的想到如下手法:

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
function ListGroup() {
let items = ["New York", "San Francisco", "Tokyo", "London", "Paris"];
// items = [];
let selectedIndex = 1;
function getMessage() {
return items.length === 0 && <p>No items found</p>;
}
return (
<>
<h1>ListGroup</h1>
{getMessage()}
<ul className="list-group">
{items.map((item, index) => (
<li
key={index}
className={
index === selectedIndex
? "list-group-item active "
: "list-group-item"
}
onClick={() => {
selectedIndex = index;
}}
>
{item}
</li>
))}
</ul>
</>
);
}

export default ListGroup;

很遗憾这样并不能成功,因为这里的selectedIndex只是组件内部的一个局部变量,处理 onClick 的时候并不会重新渲染组件

于是我们需要引入 React 的一个特性——钩子函数(Hook)。顾名思义,它能够像一把钩子一样跳出组件本身,去获得组件之外的东西

这里我们用到的钩子函数即为useState,用法如下

1
const [val, setVal] = useState(init_value);

此处前者为值本身,后者为设置该值的函数,二者组成一个套件,可以在组件内部任意使用,也可传递给子组件等

把我们的思路用钩子函数重写出来:

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

function ListGroup() {
let items = ["New York", "San Francisco", "Tokyo", "London", "Paris"];
// items = [];
const [selectedIndex, setSelectedIndex] = useState(-1);
function getMessage() {
return items.length === 0 && <p>No items found</p>;
}

return (
<>
<h1>ListGroup</h1>
{getMessage()}
<ul className="list-group">
{items.map((item, index) => (
<li
key={index}
className={
index === selectedIndex
? "list-group-item active "
: "list-group-item"
}
onClick={() => setSelectedIndex(index)}
>
{item}
</li>
))}
</ul>
</>
);
}

export default ListGroup;

打开浏览器,果然就可以正常运行了

使用 Properties 传参

现在我们要做一些解耦的工作:我们想让我们开发的组件更加独立一点,毕竟我们现在的列表内容还是硬编码的。作为函数式组件,我们当然想像给函数传递参数一样把数据传递给组件,进而组件可以只需要渲染得到的东西即可

传递数据

方式即为使用 interface 定义一个 Props:

1
2
3
4
interface ListGroupProps {
items: string[];
heading: string;
}

然后再把它传递给组件,注意这里可以提前分解 props,这样就不需要更改组件内部的代码:

1
function ListGroup({ items, heading }: ListGroupProps);

在父级组件使用时只需要像给 HTML 元素添加属性一样即可:

1
<ListGroup items={items} heading={heading} />

传递函数

有时候我希望组件做什么是我自己说了算,而不是它说了算,它只需要做好一个忠诚的部下,同时在父组件需要时把信息上传给父组件

同样的我们可以把函数也通过 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
39
40
41
42
import { useState } from "react";

interface ListGroupProps {
items: string[];
heading: string;
onSelectItem: (item: string) => void;
}

function ListGroup({ items, heading, onSelectItem }: ListGroupProps) {
const [selectedIndex, setSelectedIndex] = useState(-1);
function getMessage() {
return items.length === 0 && <p>No items found</p>;
}

return (
<>
<h1>{heading}</h1>
{getMessage()}
<ul className="list-group">
{items.map((item, index) => (
<li
key={index}
className={
index === selectedIndex
? "list-group-item active "
: "list-group-item"
}
onClick={() => {
setSelectedIndex(index);
onSelectItem(item);
}}
>
{item}
</li>
))}
</ul>
</>
);
}

export default ListGroup;
// src/components/ListGroup.tsx

在父组件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import ListGroup from "./components/ListGroup";

function App() {
const items = ["New York", "San Francisco", "Tokyo", "London", "Paris"];
const heading = "Cities";
const onSelectItem = (item: string) => {
console.log(item);
};
return (
<div>
<ListGroup items={items} heading={heading} onSelectItem={onSelectItem} />
</div>
);
}

export default App;
// src/App.tsx

这样我们在点击列表项的时候既通过它组件自身改变了颜色,又通过 props 传递函数把 item 的信息上报给了父组件,并在父组件中于控制台上打印出来(注意这里函数调用的引用关系)

Props VS State

注意 Props 应当在组件内被视作不可变的,我们不能在组件内部更改之,可以视作这是上级领导下达的命令,我们做子组件的只需要严格执行(Render)之,而 state 则是可变的,用来管理组件的一些内容

二者共同点便是,一旦被改变都可以引发浏览器重新渲染组件,并更新我们的 DOM(Props 在父级组件可以改变,如我需要给列表动态地传递需要渲染的内容的时候)

传递 children

有时候我们想像 HTML 元素一样直接往中间“插入”另外的元素/子组件,可以使用的方法是使用 children 传参:

1
2
3
4
5
6
7
8
9
interface AlertProps {
children: string;
}

const Alert = ({ children }: AlertProps) => {
return <div className="alert alert-primary">{children}</div>;
};

export default Alert;

这样我们可以像下方一样使用组件:

1
<Alert>Hello</Alert>

如果我希望传递进去的可以是更多的子组件/HTML 元素呢?使用ReactNode类型即可做到

1
2
3
4
5
6
7
8
9
10
11
import { ReactNode } from "react";

interface AlertProps {
children: ReactNode;
}

const Alert = ({ children }: AlertProps) => {
return <div className="alert alert-primary">{children}</div>;
};

export default Alert;

这样便结束了对 React 基础概念的探索,后面的主题将依次细致分析各个方面,如设置组件样式的不同方法、深入状态管理、Web 表单设计、以及利用 React 同后端交互等话题


Primary React (1)
http://example.com/2025/03/01/ReactP1/
作者
思源南路世一劈
发布于
2025年3月1日
许可协议