深入理解组件
组件是 React 的基本构建块。每个组件可以看作是一个独立的、可复用的 UI 单位。组件分为两类:类组件(Class Component)和函数组件(Function Component)。类组件使用 ES6 类来定义,包含状态和生命周期方法;函数组件则是简单的 JavaScript 函数,可以使用 React Hooks 来管理状态和副作用。
自从 React 16.8 引入了 Hooks 之后,函数组件变得更加强大和灵活,能够处理以前只有类组件才能处理的复杂状态逻辑和生命周期方法。
- 类组件
- 函数组件
定义组件
第一步:导出组件
export default
前缀是一种 JavaScript 标准语法(非 React 的特性)。它允许你导出一个文件中的主要函数以便你以后可以从其他文件引入它。
第二步:定义函数
使用 function Welcome() { }
定义名为 Profile 的 JavaScript 函数。
React 组件是常规的 JavaScript 函数,但组件的名称必须以大写字母开头,否则它们将无法运行!
第三步:添加标签
return <div>Hello World!</div>;
返回语句可以全写在一行上,但是,如果你的标签和 return
关键字不在同一行,则必须把它包裹在一对括号中,如下所示:
return (
<div>
<h1>Hello World!</h1>
</div>
);
没有括号包裹的话,任何在 return
下一行的代码都将被忽略!
嵌套和组合组件
无论类组件还是函数组件都可以嵌套和组合,以创建更复杂的 UI。以函数组件为例:
function App() {
return (
<div>
<Header />
<Main />
<Footer />
</div>
);
}
function Header() {
return <h1>Header</h1>;
}
function Main() {
return (
<div>
<Sidebar />
<Content />
</div>
);
}
function Sidebar() {
return <div>Sidebar</div>;
}
function Content() {
return <div>Content</div>;
}
function Footer() {
return <h1>Footer</h1>;
}
组件可以渲染其他组件,但是请不要嵌套他们的定义:
function App() {
// 永远不要在组件中定义组件
function Header() {
return <h1>Header</h1>;
}
return (
<div>
<Header />
</div>
);
}
组件的导入导出
这是 JavaScript 里两个主要用来导出值的方式:默认导出和具名导出。到目前为止,我们的示例中只用到了默认导出。但你可以在一个文件中,选择使用其中一种,或者两种 都使用。一个文件里有且仅有一个默认导出,但是可以有任意多个具名导出。
- 默认导出
- 具名导出
- 默认导入
- 具名导入
组件的 props
props
(属性)是 React 组件的输入参数,用于传递数据和事件处理函数。props
是从父组件传递给子组件的,因此子组件不能修改 props
,它们是只读的。
如何使用 props?
- 传递 props
父组件可以通过 JSX 属性语法传递 props
给子组件。
import Profile from './Profile.jsx';
function App() {
return <Profile name="MoFan" age="18" address="China" />;
}
- 访问 props
子组件可以通过函数参数(对于函数组件)或 this.props
(对于类组件)访问 props
。
function Profile(props) {
return (
<h1>
我是 {props.name}, 今年 {props.age}, 居住在 {props.address}
</h1>
);
}
export default Profile;
通常你不需要整个 props 对象,所以可以将它解构为单独的 props。
function Profile({ name, age, address }) {
return (
<h1>
我是 {name}, 今年 {age}, 居住在 {address}
</h1>
);
}
export default Profile;
如果你想在没有指定值的情况下给 prop
一个默认值,你可以通过在参数后面写 =
和默认值来进行解构:
function Profile({ name, age = 20, address }) {
return (
<h1>
我是 {name}, 今年 {age}, 居住在 {address}
</h1>
);
}
export default Profile;
如果子组件 Profile 仍需要传递 props 给下一个子组件 Avatar,如下:
import Avatar from './Avatar.jsx';
function Profile({ name, age, address }) {
return (
<div className="card">
{/*不推荐写法*/}
<Avatar name="MoFan" age="18" address="China" />
</div>
);
}
重复代码没有错(它可以更清晰)。但有时你可能会重视简洁。一些组件将它们所有的 props 转发给子组件,正如 Profile 转给 Avatar 那样。因为这些组件不直接使用他们本身的任何 props,所以使用更简洁的“展开”语法是有意义的:
import Avatar from './Avatar.jsx';
function Profile(props) {
return (
<div className="card">
{/*推荐写法*/}
<Avatar {...props} />
</div>
);
}
props 的类型
props
可以是任意类型的数据,包括字符串、数字、数组、对象、函数等。
function App() {
const user = {
name: 'MoFan',
age: 25,
};
return <Greeting user={user} />;
}
function Greeting(props) {
return (
<div>
<h1>Hello, {props.user.name}</h1>
<p>Age: {props.user.age}</p>
</div>
);
}
props 验证
可以使用 prop-types
库对 props
进行类型检查,确保传递的 props
符合预期。
- npm
- Yarn
- pnpm
npm i prop-types
yarn add prop-types
pnpm add prop-types
import PropTypes from 'prop-types';
function Greeting(props) {
return <h1>Hello, {props.name}</h1>;
}
Greeting.propTypes = {
name: PropTypes.string,
};
传递子组件(Children)
React 有一个特殊的 props
,称为 children
,用来传递嵌套在组件中的子元素。
function Wrapper(props) {
return <div className="wrapper">{props.children}</div>;
}
function App() {
return (
<Wrapper>
<h1>Hello, world!</h1>
</Wrapper>
);
}
在上述示例中,<h1>
标签作为 Wrapper 组件的子元素,通过 props.children
传递给 Wrapper 组件。
子组件传递 props 到父组件
子传父的原理也是 props,只是变换思维,父组件向子组件传递函数,子组件调用函数,传递参数给父组件。
// useState 是一个 hook 用于保存状态的,后续会有所讲解
import { useState } from 'react';
import SonComponent from './SonComponent.jsx';
function FatherComponent() {
const [user, setUser] = useState(null);
function getUser(user) {
console.log(user);
setUser(user);
}
return (
<div>
<h1>父组件: {JSON.stringify(user)}</h1>
<SonComponent getUser={getUser} />
</div>
);
}
export default FatherComponent;
import React from 'react';
function SonComponent(props) {
function transferDataForFather() {
props.getUser({
name: '用户名',
age: '18',
address: 'China',
});
}
return (
<button onClick={transferDataForFather}>
子组件: 点击传递数据给父组件
</button>
);
}
export default SonComponent;
组件的状态(state)
在 React 中,组件的状态(state)是组件内部的数据源,它是一个能够改变组件外观和行为的对象。状态与 props
的不同之处在于,props
是由父组件传递的是只读的;而状态是由组件自身管理的,可以通过特定的方法进行更新。状态主要用于需要动态更新或变化的数据。
useState
Hook 提供了这两个功能:
- State 变量 用于保存渲染间的数据。
- State setter 函数 更新变量并触发 React 再次渲染组件。
import { useState } from 'react';
function App() {
let [count, setCount] = useState(0);
function handleAdd() {
setCount(++count);
}
return (
<div>
<h1>计算 {count}</h1>
<button onClick={handleAdd}>+</button>
</div>
);
}
export default App;
这里的 [
和 ]
语法称为数组解构,它允许你从数组中读取值。 useState
返回的数组总是正好有两项。
剖析 useState
当你调用 useState
时 ,你是在告诉 React 你想让这个组件记住一些东西:
let [count, setCount] = useState(0);
惯例是将这对返回值命名为 const [thing, setThing]
。你也可以将其命名为任何你喜欢的名称,但遵照约定俗成能使跨项目合作更易理解。
赋予一个组件多个 state 变量
- 示例
- React 如何知道返回哪个 state
const [index, setIndex] = useState(0);
const [showMore, setShowMore] = useState(false);
你可能已经注意到,useState
在调用时没有任何关于它引用的是哪个 state
变量的信息。没有传递给 useState
的“标识符”,它是如何知道要返回哪个 state
变量呢?它是否依赖于解析函数之类的魔法?答案是否定的。
相反,为了使语法更简洁,在同一组件的每次渲染中,Hooks 都依托于一个稳定的调用顺序。这在实践中很有效,因为如果你遵循上面的规则(“只在顶层调用 Hooks”),Hooks 将始终以相同的顺序被调用。此外,linter 插件也可以捕获大多数错误。
在 React 内部,为每个组件保存了一个数组,其中每一项都是一个 state
对。它维护当前 state
对的索引值,在渲染之前将其设置为 “0”。每次调用 useState
时,React 都会为你提供一个 state 对并增加索引值。你可以在文章 React Hooks: not magic, just arrays 中阅读有关此机制的更多信息。
State 是隔离且私有的
State 是屏幕上组件实例内部的状态。换句话说,如 果你渲染同一个组件两次,每个副本都会有完全隔离的 state
!改变其中一个不会影响另一个。
试着点击每个计算按钮。你会注意到它们的 state
是相互独立的
import { useState } from 'react';
function Count() {
let [count, setCount] = useState(0);
function handleAdd() {
setCount(++count);
}
return (
<div>
<h1>计算 {count}</h1>
<button onClick={handleAdd}>+</button>
</div>
);
}
function App() {
return (
<div>
<Count />
<Count />
</div>
);
}
export default App;
更新 state 中的对象
state
中可以保存任意类型的 JavaScript 值,包括对象。但是,你不应该直接修改存放在 React state 中的对象。相反,当你想要更新一个对象时,你需要创建一个新的对象(或者将其拷贝一份),然后将 state
更新为此对象。
什么是 mutation?
mutation 指的是对数据进行直接修改或改变, 当你直接修改 state 对象时,就制造了一个 mutation:
const [position, setPosition] = useState({ x: 0, y: 0 });
position.x = 5;
然而,虽然严格来说 React state 中存放的对象是可变的,但你应该像处理数字、布尔值、字符串一样将它们视为不可变的。因此你应该替换它们的值,而不是对它们进行修改。
将 state 视为只读的
换句话说,你应该把所有存放在 state 中的 JavaScript 对象都视为只读的。
通过使用 setPosition,你在告诉 React:使用这个新的对象替换 position 的值,使用这个新的对象替换 position 的值。
setPosition({
x: e.clientX,
y: e.clientY,
});
更新 state 中的数组
数组是另外一种可以存储在 state
中的 JavaScript 对象,它虽然是可变的,但是却应该被视为不可变。同对象一样,当你想要更新存储于 state
中的数组时,你需要创建一个新的数组(或者创建一份已有数组的拷贝值),并使用新数组设置 state
。
在没有 mutation 的前提下更新数组
在 JavaScript 中,数组只是另一种对象。同对象一样,你需要将 React state 中的数组视为只读的。这意味着你不应该使用类似于 arr[0] = 'bird'
这样的方式来重新分配数组中的元素,也不应该使用会直接修改原始数组的方法,例如 push()
和 pop()
。
相反,每次要更新一个数组时,你需要把一个新的数组传入 state
的 setting
方法中。为此,你可以通过使用像 filter()
和 map()
这样不会直接修改原始值的方法,从原始数组生成一个新的数组。然后你就可以将 state 设置为这个新生成的数组。
下面是常见数组操作的参考表。当你操作 React state 中的数组时,你需要避免使用左列的方法,而首选右列的方法:
避免使用 (会改变原始数组) | 推荐使用 (会返回一个新数组) | |
---|---|---|
添加元素 | push ,unshift | concat ,[...arr] 展开语法 |
删除元素 | pop ,shift ,splice | filter ,slice |
替换元素 | splice ,arr[i] = ... 赋值 | map |
排序 | reverse ,sort | 先将数组复制一份 |
- 添加元素
- 删除元素
- 替换元素
- 插入元素
更新数组内部的对象
在 React 中更新数组内部的对象,通常需要找到要更新的对象,然后创建该对象的副本,对其进行修改,再将更新后的对象放回数组中。最后,通过调用 setState
来更新状态,从而触发重新 渲染。以下是一些示例,演示如何更新数组内部的对象。
在下面的例子中,两个不同的艺术品清单有着相同的初始 state
。他们本应该互不影响,但是因为一次 mutation,他们的 state
被意外地共享了,勾选一个清单中的事项会影响另外一个清单:
import { useState } from 'react';
let nextId = 3;
const initialList = [
{ id: 0, title: 'Big Bellies', seen: false },
{ id: 1, title: 'Lunar Landscape', seen: false },
{ id: 2, title: 'Terracotta Army', seen: true },
];
export default function BucketList() {
const [myList, setMyList] = useState(initialList);
const [yourList, setYourList] = useState(initialList);
function handleToggleMyList(artworkId, nextSeen) {
const myNextList = [...myList];
const artwork = myNextList.find(a => a.id === artworkId);
artwork.seen = nextSeen;
setMyList(myNextList);
}
function handleToggleYourList(artworkId, nextSeen) {
const yourNextList = [...yourList];
const artwork = yourNextList.find(a => a.id === artworkId);
artwork.seen = nextSeen;
setYourList(yourNextList);
}
return (
<>
<h1>艺术愿望清单</h1>
<h2>我想看的艺术清单:</h2>
<ItemList artworks={myList} onToggle={handleToggleMyList} />
<h2>你想看的艺术清单:</h2>
<ItemList artworks={yourList} onToggle={handleToggleYourList} />
</>
);
}
function ItemList({ artworks, onToggle }) {
return (
<ul>
{artworks.map(artwork => (
<li key={artwork.id}>
<label>
<input
type="checkbox"
checked={artwork.seen}
onChange={e => {
onToggle(artwork.id, e.target.checked);
}}
/>
{artwork.title}
</label>
</li>
))}
</ul>
);
}
你可以使用 map
在没有 mutation 的前提下将一个旧的元素替换成更新的版本。
setMyList(
myList.map(artwork => {
if (artwork.id === artworkId) {
// 创建包含变更的*新*对象
return { ...artwork, seen: nextSeen };
} else {
// 没有变更
return artwork;
}
}),
);
使用 Immer 编写简洁的更新逻辑
Immer 是一个帮助处理不可变数据结构的库,它允许你以更加简洁和直观的方式编写更新逻辑。通过使用 Immer,你可以直接“修改”状态,并且 Immer 会在幕后处理不可变性。
- npm
- Yarn
- pnpm
npm install immer
yarn add immer
pnpm add immer
import { produce } from 'immer';
import { useState } from 'react';
let nextId = 3;
const initialList = [
{ id: 0, title: 'Big Bellies', seen: false },
{ id: 1, title: 'Lunar Landscape', seen: false },
{ id: 2, title: 'Terracotta Army', seen: true },
];
export default function BucketList() {
const [myList, setMyList] = useState(initialList);
const [yourList, setYourList] = useState(initialList);
function handleToggleMyList(artworkId, nextSeen) {
const nextState = produce(myList, draft => {
const artwork = draft.find(a => a.id === artworkId);
artwork.seen = nextSeen;
});
setMyList(nextState);
}
function handleToggleYourList(artworkId, nextSeen) {
const yourNextList = [...yourList];
const nextState = produce(yourNextList, draft => {
const artwork = draft.find(a => a.id === artworkId);
artwork.seen = nextSeen;
});
setYourList(nextState);
}
return (
<>
<h1>艺术愿望清单</h1>
<h2>我想看的艺术清单:</h2>
<ItemList artworks={myList} onToggle={handleToggleMyList} />
<h2>你想看的艺术清单:</h2>
<ItemList artworks={yourList} onToggle={handleToggleYourList} />
</>
);
}
function ItemList({ artworks, onToggle }) {
return (
<ul>
{artworks.map(artwork => (
<li key={artwork.id}>
<label>
<input
type="checkbox"
checked={artwork.seen}
onChange={e => {
onToggle(artwork.id, e.target.checked);
}}
/>
{artwork.title}
</label>
</li>
))}
</ul>
);
}
条件渲染
通常你的组件会需要根据不同的情况显示不同的内容。在 React 中,你可以通过使用 JavaScript 的 if
语句、&&
和 ?:
运算符来选择性地渲染 JSX。
使用 if 语句
使用 if
语句来条件渲染时,可以在渲染逻辑中放置 if
语句并返回不同的 JSX。
function Greeting(props) {
const isLoggedIn = props.isLoggedIn;
if (isLoggedIn) {
return <h1>Welcome back!</h1>;
} else {
return <h1>Please sign up.</h1>;
}
}
function App() {
return <Greeting isLoggedIn={true} />;
}
选择性地返回 null
在一些情况下,你不想有任何东西进行渲染。比如,你不想显示已经打包好的物品。但一个组件必须返回一些东西。这种情况下,你可以直接返回 null
。
function Greeting(props) {
const isLoggedIn = props.isLoggedIn;
if (isLoggedIn) {
return <h1>Welcome back!</h1>;
} else {
return null;
}
}
function App() {
return <Greeting isLoggedIn={true} />;
}
使用逻辑 && 运算符
逻辑 &&
运算符常用于在某个条件为真时渲染某些内容。
function Welcome(props) {
const { name, isShowName } = props;
return <h1>欢迎你 {isShowName && name}</h1>;
}
function App() {
return <Welcome name="mofan" isShowName={true} />;
}