Primary React (2)

Primary React (2)

本文是 mosh 的 React 基础课的第二部分笔记,包括设计组件样式和管理组件状态两部分

Styling Components

这部分内容包括 React 中和组件样式相关的知识

裸 CSS

虽然通常不建议这种写法,因为有很多很好的现成的 CSS 库以供调用,但是如果有时候自己有一些微调 CSS 的需求就会用到裸 CSS 的写法

下面我们看看在 React 中是如何实现的:

list

通常我们会把相关的组件和 CSS 文件放在同一个文件夹下,这里注意一下这里的 index.ts 文件:

1
2
3
import ListGroup from "./ListGroup";

export default ListGroup;

这么做的目的是为了保持其他地方引入组件的时候可以不用更改代码

因为若我引入的是一个文件夹,那么编译器会自动寻找这个文件夹下的 index.ts 文件进行导入

然后我们就可以设计自己的 CSS 样式了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.list-group {
list-style: none;
padding: 0;
margin: 0;
}

.list-group-item {
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}

.active {
background-color: #0d6efd;
color: #fff;
}

注意不要忘了删掉在 main.tsx 里对 CSS 库的导入

最后在组件里导入这里的 CSS 文件即可:

1
import "./ListGroup.css";

CSS Module

使用裸 CSS 的时候可能会出现命名重复的情况,这时候若强行导入可能会导致渲染时故障

解决这个问题的方法是使用 CSS Moudle,这样在导入的时候相当于导入的就是普通的 js 对象了

具体操作是更改文件后缀,并更换导入的方式:

1
2
文件名修改为:xxx.moudle.css
导入方式修改为:import styles from "./ListGroup.module.css";

使用的时候按照正常对类的方法去解构即可,注意这时候为了避免连字符-带来的歧义,我们通常对这种情况下的 CSS 文件使用驼峰命名:

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";
import styles from "./ListGroup.module.css";

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={styles.listGroup}>
{items.map((item, index) => (
<li
key={index}
className={
index === selectedIndex
? [styles.listGroupItem, styles.active].join(" ")
: styles.listGroupItem
}
onClick={() => {
setSelectedIndex(index);
onSelectItem(item);
}}
>
{item}
</li>
))}
</ul>
</>
);
}

export default ListGroup;

CSS-in-JS

有时候我们也想把 CSS 和组件自己的 tsx/jsx 文件集成在一起,这样在增删查改的时候就会相当的方便

对应的方法称之为 CSS-in-JS,我们需要安装两个相应的包:

1
$ npm i styled-components @types/styled-components --save

然后我们就可以把 CSS 属性封装成 React 对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { ReactNode } from "react";
import styled from "styled-components";

const StyledAlert = styled.div`
background-color: #0d6efd;
color: #fff;
`;

interface AlertProps {
children: ReactNode;
}

const Alert = ({ children }: AlertProps) => {
return <StyledAlert>{children}</StyledAlert>;
};

export default Alert;

注意这里的语法,使用的是反引号

这样我们得到的 StyledAlert 本身就是一个 React 对象,我们按照和其他组件一致的方式去使用它即可

想要设置组件状态管理等操作?同样方便,写一个 interface 传进去就行:

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

interface StyledAlertProps {
active: boolean;
}

const StyledAlert = styled.div<StyledAlertProps>`
background-color: ${(props) => (props.active ? "#0d6efd" : "#f8f9fa")};
color: #f8f;
padding: 10px;
border-radius: 5px;
cursor: pointer;
`;

interface AlertProps {
children: ReactNode;
}

const Alert = ({ children }: AlertProps) => {
const [active, setActive] = useState(false);
return (
<StyledAlert active={active} onClick={() => setActive(!active)}>
{children}
</StyledAlert>
);
};

export default Alert;

(这里的颜色是随便打的,估计很丑)

添加图标

这部分介绍了一个常用的 React 图标组件库——react-icons,同样我们先安装一下:

1
$ npm i react-icons@4.7.1 --save

事实上后面我们使用 UI 库做真正开发的时候也是这样的流程:

  • 先物色好要用的库,进行安装
  • 查文档,找到要用的组件以及名称
  • 在自己的组件中进行导入和使用,封装成为自己的组件

现在假设我们需要添加一个小心心图标:

上文档搜一下先,到一个还不错的,看看导入方式,然后引入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { FaRegHeart, FaHeart } from "react-icons/fa";
import { useState } from "react";
const Heart = () => {
const [liked, setLiked] = useState(false);
return (
<div>
{liked ? (
<FaHeart color="red" onClick={() => setLiked(!liked)} />
) : (
<FaRegHeart onClick={() => setLiked(!liked)} />
)}
</div>
);
};

export default Heart;

这样我们就做好了一个可以点赞的爱心!

Managing Component State

我们在学习 React 基础概念的时候曾简略学习了 State Hook 的操作,这一节我们来详细学习这部分,了解如何细致地管理组件状态

理解 State Hook 的原理

  • React 更新组件的状态是异步的:它会一次性收集好所有的需要更新的地方,并完成组件的重新渲染
  • State 是储存在组件之外的:它们并非是局部变量,相反,局部变量在每次组件渲染后就会被销毁
  • Hooks 必须在组件顶层使用:如果在语句中或者循环内使用,就会造成渲染紊乱的问题

设计 State Hook 的小技巧

这部分主要和 coding style 相关,同时也是保持代码不会很快变成屎山的一些技巧

  • 避免冗余变量
1
2
3
const [firstName, setFirstName] = useState(" ");
const [lastName, setLastName] = useState(" ");
const fullName = firstName + " " + lastName; // 冗余变量
  • 使用对象结构

当几个状态属性同属于一个对象时,可以使用对象结构来简化代码

1
2
3
4
5
const [person, setPerson] = useState({
firstName: " ",
lastName: " ",
age: -1,
});
  • 避免深层次嵌套

这一条是针对上一条的拓展,指对象结构不能嵌套太深,否则适得其反,难以更新!

1
2
3
4
5
6
7
8
9
10
11
12
// 错误示例
const [person, setPerson] = useState({
firstName: ' ',
lastName: ' ',
job: {
name: ' ',
address: {
street: ' '
city: ' '
}
}
})

Pure Components

何为 pure

类比纯函数的概念:输入相同则输出相同

那么我们的 pure components 效果是一样的:只要给进去的 props 不变,那么我们的组件呈现结果就也不能变

这也是 React 倡导的开发原则,因为对于纯组件而言,只要 props 没变,我就不用重新渲染该组件,节约了资源

关于 strict mode

回忆我们在主文件里遇到的严格模式:

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>
);

这里的严格模式实际上是帮助开发者“Debug”用的,它会在渲染组件的时候重复两次,第一次用于排除代码本身的 bug,第二次则可以用于看看组件里有没有不 pure 的地方

当然仅对于最终呈现的结果来说,严格模式开启与否不会有任何影响

更新 Object

我们在上面的 state hook 小技巧里提过可以将状态变量设置成对象:

1
2
3
4
5
const [person, setPerson] = useState({
firstName: " ",
lastName: " ",
age: -1,
});

在更新的时候也有小妙招,当对象的属性很多而我们又只想更改其中一两个的时候,可以使用扩展符语法糖:

1
2
const newPerson = { ...person, age: 30 };
setPerson(newPerson);

当然,更简略的做法是:

1
setPerson({ ...person, age: 30 });

对于上文提到的要避免嵌套对象,这是因为更新起来会很麻烦,我们假设对象如下:

1
2
3
4
5
6
7
8
const [person, setPerson] = useState({
firstName: " ",
lastName: " ",
job: {
name: " ",
address: " ",
},
});

由于 JS 的扩展芙运算是浅拷贝(仅引用相同地址的对象),所以当我们想要更新 address 属性的时候,就必须给他一个新的 job 对象,操作方法如下:

1
2
3
4
5
6
7
setPerson({
...person,
job: {
...person.job,
address: "Shanghai",
},
});

更新 Array

数组的更新同样要考虑到浅拷贝的因素,所以我们每次更新的时候都传进去一个新的 array,再在内部使用扩展芙

假设我们数组如下:

1
const [tag, setTag] = useState(["happy", "hotface"]);

add

添加操作是最简单的,直接操作就行了:

1
setTag([...tag, "exciting"]);

remove

去除某元素可以使用数组自带的 filter 方法:

1
setTag(tag.filter((t) => t !== "hotface"));

update

要把数组里的某些项换成新的,可以使用 map 方法:

1
setTag(tag.map((t) => (t === "happy" ? "sad" : t)));

complex case: array of objects

当被更新的数组里的元素是对象的时候,操作起来稍微复杂一点,但是核心思路不变:使用数组自带方法以及扩展芙

1
2
3
4
const [men, setMen] = useState(
{ name: "kobe", age: 4 },
{ name: "41p", age: 20 }
);

现在我要把 kobe 的名字改成牢大,可以这样做:

1
2
3
setMen(
men.map((man) => (man.name === "kobe" ? { ...man, name: "牢大" } : man))
);

在组件之间传递状态

方法之前也提到过:想要在组件之间传递状态,可以将状态提升到父级组件,然后在父级组件统一管理

假设我们要完成这样一个组件:上方的一栏显示物品总数,下方一栏显示具体的物品,同时还有一个按钮来完成如添加等操作。这时候我们就可以把子组件设计成之前说的“只需要完成上级任务”的模式,然后再在父级组件完成具体任务操作

示例代码如下

1
2
3
4
import Products from "./Products";

export default Products;
// index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface NavBarProps {
cartItemsCount: number;
onAddItem: () => void;
}

const NavBar = ({ cartItemsCount, onAddItem }: NavBarProps) => {
return (
<div>
<span>NavBar: {cartItemsCount}</span>
<button onClick={onAddItem}>Add Item</button>
</div>
);
};

export default NavBar;
// NavBar.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface ListProps {
items: string[];
}

const List = ({ items }: ListProps) => {
return (
<>
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</>
);
};

export default List;
// List.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { useState } from "react";
import List from "./List";
import NavBar from "./NavBar";

const Products = () => {
const [cartItems, setCartItems] = useState(["1", "2", "3"]);
const [item, setItem] = useState(4);
const onAddItem = () => {
setItem(item + 1);
setCartItems([...cartItems, item.toString()]);
};
return (
<>
<NavBar cartItemsCount={cartItems.length} onAddItem={onAddItem} />
<List items={cartItems} />
</>
);
};

export default Products;

// Products.tsx

这样你就可以通过按按钮往列表里一直添加递增的数字了

本部分笔记到此结束


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