深入理解组件
组件是 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
- Bun
npm i prop-types
yarn add prop-types
pnpm add prop-types
bun 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 中阅读有关此机制的更多信息。