在本章,我们将学习React应用程序中的关键构建模块:组件。在本章中,我们主要学习最简单的组件类型,即无状态组件(stateless componen)。我们将在第下章中学习更复杂的组件,即状态组件 ( stateful components)。我还将在本章中解释props特性的工作原理,props允许一个组件向另一个组件传递渲染其内容所需的数据,以及触发事件时应该调用的函数。下表列出了上下文中的无状态组件和Props:
问题
答案
| | —- | —- | | 组件和Props是什么? | 组件是React应用程序中的关键构建块。无状态组件是JavaScript函数,用 于渲染React呈现给用户的内容。Props是一个组件向另一个组件传递数据 的方法,用于调整渲染的内容 | | 组件和Props有什么用? | 组件非常有用,因为组件通过组合JavaScript、HTML和其他组件来提供对 React支持的访问,从而创建特性。Props 很有用,因为props允许组件调整 组件生成的内容。 | | 如何使用组件和 Props | 无状态组件是返回React元素的JavaScript函数,该元素通常使用JSX格式的 HTML定义。props是(组件)元素的属性。 | | 组件和Props有什么陷阱或限制? | React要求组件以特定的方式运行,例如返回单个React元素并始终返回结 果,要适应这些用法可能需要时间。props最常见的缺陷是在需要使用 JavaScript表达式时必须要指定文本值。 | | 还有其他选择吗? | 组件是React应用程序中的关键构建模块,是必不可少的。如后面内容所述,在更大和更复杂的项目中,有一些可以替代props的方 法。 |
下表是章节摘要
问题
解决方案
| | —- | —- | | 向React应用程序添加内容 | 定义一个返回HTML元素或调用 React.createElement方法的函数 | | 使用子组件向React应用程序添加其他功能 | 定义组件并使用与组件名称相一致的元素以父子 关系进行组合 | | 配置子组件 | 应用组件时定义props | | 渲染HTML元素 | 使用map方法创建元素,确保元素具有key属性 | | 渲染组件中的多个元素 | 使用React.Fragment元素或使用空标签 | | 渲染空内容 | 返回null | | 从子组件中接收通知 | 使用函数prop配置组件 | | 向子组件传递props | 使用从父组件接收的props值或使用解构运算符 | | 定义默认props值 | 使用defaultProps属性 | | 检查 prop 类型 | 使用propTypes属性 |
准备本章
要创建本章的示例项目,请打开一个新的命令窗口,进入项目所在的文件,然后运行下列命令:
npx create-react-app components
打开命令提示符运行下面的命令,打开项目文件夹并将Bootstrap 包添加到项目中:cd components
npm install bootstrap@4.1.2
要在应用程序中使用Bootstrap,需要将下面的代码添加到可以在 src文件夹中index.js文件中来引入Bootstrap:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import 'bootstrap/dist/css/bootstrap.css';
ReactDOM.render(<App />, document.getElementById('root'));
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: http://bit.ly/CRA-PWA
serviceWorker.unregister();
在命令提示符中打开components文件夹中运行下面的命令来启动开发工具:
npm start
项目的初始准备完成后,浏览器将会自动跳转到:http://localhost:3000 将显示如下图所示的内容:
了解组件
从零开始学习组价的的最佳方法是定义一个组件并查看它是如何工作的。我在 了App.js文件中重新创建了一个简单的组件:
export default function App(){
return "Hello Adam";
}
这是一个简单无状态组件:React将将函数返回要显示给用户的内容,这就是所谓的渲染。 当应用程序启动时将会执行 index.js 文件以及渲染应用程序组件的代码语句。React调用函数并将渲染结果显示给用户,如图示:
尽管内容非常简单,但这揭示了组件的关键用途,即组件用于向用户显示内容。
渲染HTML内容
当一个组件渲染一个字符串值时,组件被作为文本内容包含在父元素中。当组件返回HTML内容时,组件就会变得更加有用,这一点最容易通过利用JSX 的优势,以及组件允许HTML与JavaScript代码混合的方式来实现。在下面的代码中,我改变了该组件的结果,使其渲染了一个HTML片段。
■ 技巧 当你使用JSX时,你必须在react模块中声明对React的依赖性,如列表中所示。如果你忘记了,你会收到一个警告。
export default function App() {
return <h1 className="bg-primary text-white text-center p-2">
Hello Adam
</h1>
}
记住在组件函数中要使用return关键字来返回渲染结果。这可能会感觉很别扭,但请记住,JSX文件中的HTML片段被转换为对createElement方法的调用,产生一个React可以显示给用户的对象。
当你考虑到HTML片段在构建过程中被替换成createElement方法时,使用return关键字就是有意义的。
import React from "react";
export default function App() {
return React.createElement(
"h1",
{ className: "bg-primary text-white text-center p-2" },
"Hello Adam"
);
}
组件函数返回React.createElement方法,这个方法是React向文档对象模型(DOM)添加内容的元素。 如果使用return关键字返回多行HTML内容,那么要使用圆括号包裹要返回的HTML内容:
import React from "react";
export default function App() {
return <h1 className="bg-primary text-white text-center p-2">Hello Adam</h1>;
}
尽管圆括号可能会让一些开发人员感到不舒服,但这可以使HTML元素保持缩进一致。还可以使用胖箭头函数定义组件函数,这种语法省略return关键字:
import React from "react";
export default () => (
<h1 className="bg-primary text-white text-center p-2">Hello Adam</h1>
);
在上面的代码中导出的组件建函数没有函数名,这在示例程序中是可行的,因我们在 index.js文件中从App.js文件中导入该组件的语句使用了默认导出,就像这样:
...
import App from './App';
...
按名称导出一个胖箭头函数作为默认值需要使用export default
声明导出组件,如下列代码所示:
export const App = () =>
<h1 className="bg-primary text-white text-center p-2">
Hello Adam
</h1>
export default App;
使用cosnt
创建了一个按名称导出的胖箭头函数组件即App组件,然后在单独的语句中使用export default
对组件进行导出声明, 默认导出声明允许我们在父组件中按组件名称导入组件并将导入的组件作为默认值。
■注意 我包括这个例子,因为模块导出会引起混淆,但在实际项目中,大多数情况下都使用命名导出或默认导出,不必同时适应两种工作方式。我更喜欢使用命名导出,这也是我在本书的例子中所采取的方法。
在本章中,我使用常规函数,并使用圆括号让HTML内容更具可读性,但本节中的所有示例运行结果都是相同的,如截图所示:
渲染其它组件
React最重要的一个特性就是组件渲染的内容中可以包含其他组件,因此我们可以将不同的组件组合在一起创建复杂的应用程序。我在Message.js文件中创建了Message组件:
import React from 'react';
export function Message(){
return(
<h4 className="bg-success text-white text-center p-2">这是一个消息组件</h4>
)
}
Message组件渲染包含消息的h4元素。我更新了App组件,App组件将Message 组件内容作为其内容的一部分进行渲染:
import React from 'react';
import { Message } from './Message';
export default function App() {
return (
<div>
<h1 className="bg-primary text-white text-center p-2">Hello World!</h1>
<Message />
</div>
)
}
import语句声明了Message组件和App组件的依赖关系,Message组件是使用Message元素渲 的。当React接收到App组件渲染的内容时,App组件中的Message元素调用Message组件函数并使用Message组件渲染的内容替换App组件中的Message元素,最终结果如图所示:
当一个组件像这样调用另一个组件时,就形成了父子关系。在本例中,App组件是Message组件的父组件,而Message组件是App组件的子组件。我们可以给子组件定义多个元素进行重复调用:
import React from 'react';
import { Message } from './Message';
export default function App() {
return (
<div>
<h1 className="bg-primary text-white text-center p-2">Hello World!</h1>
<Message />
<Message />
<Message />
</div>
)
}
每次React执行到Message元素时,就会调用Message组件并使用其渲染的内容来替换Message元素,如图所示:
一个父组件可以有不同类型的子组件,这意味着一个组件可以使用多个组件提供的功能。在src文件夹中,在新建的Summary.js文件创建了Summary组件,代码如下:
import React from 'react';
export function Sumarry() {
return <h4 className="bg-info text-white text-center p-2">这是Sumarry组件</h4>
}
我在App组件文件中引入Sumarry组件,并声明了App组件和Summary组件的依赖关系,并使用Summary元素渲染Sumarry组件的内容。注意,在父组件中引入子组件时,子组件名称使用大括号包裹。组件名称和组件文件名称不一定要相同:
import React from 'react';
import { Message } from './Message';
import {Sumarry} from './Summary';
export default function App() {
return (
<div>
<h1 className="bg-primary text-white text-center p-2">Hello World!</h1>
<Message />
<Message />
<Message />
<Sumarry/>
</div>
)
}
当React处理App组件渲染的内容时,遇到子组件元素后就会调用子组件函数,并用子组件渲染的内容替换子组件元素。在我们上实例中,Message和Summary元素将被Message和Summary元组件的渲染内容所替换,结果如图所示:
理解 Props
当然如果父组件只能渲染子组件提供的内容,那么组件显然没有什么用。幸运的是,React提供了props属性,props是properties(属性)的缩写,props就是组件元素属性,通常是子组件元素属性,用于父组件向其子组件传递数据,以便子组件可以可以使用Props属性来渲其内容。在接下来的部分中,我将解释props的工 作原理,并演示props的不同使用方式。
在父组件中定义props
通过向应用组件的自定义HTML元素添加属性来定义Props。属性的名称就是props的名称,值可以是静态值 也可以是表达式。在下面的代码中,我向App组件的Message子组件元素添加了一些属性。
import React from 'react';
import {Message} from './Message';
import {Summary} from './Summary';
export default function App(){
return <div>
<h1 className='bg-primary text-white text-center p-2'>hello world!</h1>
<Message geetting='hello' name='iebukes'/>
<Message geetting='欢迎访问' name={'iebukes'+'.com'}/>
<Message geetting='这是我的博客网站' name='iebukes.com'/>
<Summary/>
</div>
}
我为每个Message组件都提供了两个props:greeting和name。大多数prop值都是静态值,以字面字符串表示。第二个Message元素上的greeting props值是一个表达式,它将两个字符串值连接起来。(在上面的代码中,你会看到一个关于表达式的linter警告,因为连接字符串字面值是linter所要检测的不良行为之一。为了本章的目的,可以忽略这个linter警告。)
定义props
Props可以用来将静态值或动态表达式的结果传递给子组件。 静态值是按字面意思引用的,像这样:
...
<Message greeting="Hello" name="iebukes" />
...
这个prop为子组件提供名称为iebukes的prop值。如果要使用JavaScript表达式的结果作为prop的值,请使用数据绑定表达式,如下所示:
...
<Message greeting="欢迎访问" name={ 'iebukes'+'.com' } />
...
React将计算该表达式并使用其结果,即本例中两个字符串的连接。例子中两个字符串的连接,作为prop的值。一个常见的错误是把JavaScript表达式放在引号里,例如这样:
...
<Message greeting="Hola" name="{ "Alice" + "Smith" }" />
...
React将此解释为请求使用静态值{“Alice”+“Smith”}作为prop的值。使用表达式作为props时,必须记住不要使用引号。如果您不喜欢使用JSX,并且希望使用纯JavaScript创建React元素,那么提供props作为createElement方法的第二个参数,如下所示:
...
React.createElement(Message, { greeting: "Hola", name: "Alice" + "Smith"})
...
如果使用JSX或纯JavaScript没有得到预期的结果,React Devtools浏览器扩展(如第9章所述)可以显示应用程序中每个组件接收到的prop,从而很容易看到哪里出了问题。
给子组件传递props
在组件中,通过定义一个名为props的参数来接收道具(尽管这只是一个惯例,你可以给这个参数起任何合法的JavaScript名称)。每个props对象都有一个props属性,该属性被指定了prop值。下面的子组件元素的geeting和name属性都是props属性:
...
<Message greeting="Hello" name="iebukes" />
...
我们还可以是对象的形式创建props属性:
...
{
greeting:'Hello',
name:'iebukes'
}
...
在下面的代码中,我修改了Message组件,并给它传递一个props参数,用来获取父组件提供的值:
import React from "react";
export function Message(props) {
return (
<h4 className="bg-success text-white text-center p-2">
{props.geeting} {props.name}
</h4>
);
}
子组件不需要考虑prop值是静态值还是表达式,而是像其他的JavaScript对象一样使用props。在上面的代码中中,我在表达式中使用了greeting和name props来设置组件渲染的h4元素的内容,结果如截图所示。
使用JavaScript和Props来渲染内容
在下面的代码中,给App组件中定义的每个Message元素提供的prop产生了不同的内容,从而允许父组件以不同的方式使用相同的功能。
选择性地渲染内容
组件可以使用JavaScript中的if语句来检查一个prop,并根据prop的值渲染不同的内容。在下面的代码中,我使用if语句来更改Message组件渲染的内容。
import React from "react";
export function Message(props) {
if (props.geetting === "hello") {
return (
<h4 className="bg-warning p-2">
{props.geetting} {props.name}
</h4>
);
} else {
return (
<h4 className="bg-success text-white text-center p-2">
{props.geetting} {props.name}
</h4>
);
}
}
如果geetting prop的值是hello,组件将渲染具有不同class 样式的h4元素,如图10-8所示。,如图所示。
这种类型的选择性渲染,即只改变一个prop的值就可以通过将prop的值与HTML的其他部分分开来表达,从而减少重复,如下面的代码所示:
import React from "react";
export function Message(props) {
let classes =
props.name === "iebukes"
? "bg-warning p-2"
: "bg-success text-white text-center p-2";
return (
<h4 className={classes}>
{props.geetting} {props.name}
</h4>
);
}
我使用JavaScript三元条件运算符来给h4元素分配不同的class,并使用className属性的表达式应用这些类。结果与上面相同,但没有复制HTML元素不变的部分。
当组件需要从更复杂的列表中选择内容时,可以使用switch语句,代码如下所示。
import React from "react";
export function Message(props) {
let classes;
switch (props.geetting) {
case "hello":
classes = "bg-warning p-2";
break;
case "欢迎访问":
classes = "bg-secondary text-white text-center p-2";
break;
default:
classes = "bg-success text-white text-center p-2";
}
return (
<h4 className={classes}>
{props.geetting} {props.name}
</h4>
);
}
渲染数组
组件经常需要为数组中的每个一个元素创建HTML元素来显示列表中的项目或作为表格中的行。处理数组所需的技术会引起混淆,需要仔细研究。为了做好准备,我更新了App组件,使其用一个prop来配置Summary组件,如下面的代码所示。(我还删除了App组件中的Message子组件及其元素来简化例子)。
import React from 'react';
//import {Message} from './Message';
import {Sumarry} from './Sumarry';
export default function App(){
return <div>
<h1 className="bg-primary text-white text-center p-2">Hello iebkes</h1>
<Summary name={["Bob","Alice","Dora"]}/>
</div>
}
name属性为Summary组件提供一个字符串值数组。在下面的代码中,我更改了Summary组件渲染的内容,以便为数组中的每个值生成相应的元素。
import React from "react";
function createElements(names) {
let arrayElems = [];
for (let i = 0; i < names.length; i++) {
arrayElems.push(<div>{`${names[i]}含有${names[i].length}`}</div>);
}
return arrayElems;
}
export function Summary(props) {
return (
<h4 className="bg-info text-white text-center p-2">
{createElements(props.name)}
</h4>
);
}
组件函数通过调用createElements函数使用一个表达式来设置h4元素的内容。createElements函数使用一个JavaScript for loop来列举name数组的内容,并将数组结果添加到一个div元素中。
...
arrayElems.push(<div>{`${names[i]} contains ${names[i].length} letters`}</div>)
...
所有div元素的内容通过一个表达式来设置的,这个表达式使用一个模板字符串来创建一个显示数组元素的信息。div元素中的数组作为createElements函数的结果被返回,并作为h4元素的内容,结果如下图所示:
使用Map方法处理数组对象
尽管for循环是大多数程序员用来枚举数组的方式,但它并不是React中处理数组的最优雅的方式。我们可以使用map方法将数组中的对象转换为HTML元素,如下面的代码所示:
import React from "react";
function createElement(names) {
return names.map((name) => <div>{`${name}包含${name.length}个字母`}</div>);
}
export function Summary(props) {
return (
<h4 className="bg-info text-white text-center p-2">
{createElement(props.name)}
</h4>
);
}
map方法的参数是是一个回调函数,对数组的每一项都运行传入的函数。每次调用传递给map方法的函数时,数组中的下一个项就被传递给该函数,我用它来创建代表该对象的元素。每次调用该函数的结果都被添加到一个数组中作为调用map方法的结果。上面代码运行结果不变。
使用map方法时接收其他参数
在清单上面的代码中,map方法的函数参数接收当前数组对象作为其参数。map方法还提供了另外两个参数:对象数组元素索引和完整的对象数组。您可以在本章后面的“渲染多个元素”部分中看到数组索引的示例。
添加Key属性
完成这个例子还需要最后一步。React需要给数组中的对象生成的元素添加一个 key属性,key属性是惟一的标识符,用于帮助React确定元素的更改变化,例如修改、添加和删除。如我在后面的章节中所述。key 属性的值应该是一个表达式,其值在数组中是对象的唯一标识符,通常在map()方法中的元素需要设置 key 属性。如下面的代码所示:
import React from "react";
function createElement(names) {
return names.map((name) =>
<div key={name}>
{`${name}包含${name.length}个字母`}
</div>);
}
export function Summary(props) {
return (
<h4 className="bg-info text-white text-center p-2">
{createElement(props.name)}
</h4>
);
}
我使用了name参数变量的值,当调用传递给map方法的函数时,数组中的每个对象都会被分配到该变量中,这个变量允许React区分从数组对象创建的元素。
React将显示没有key属性的元素,如本节前面的示例所示,但浏览器的JavaScript控制台中将显示一条警告
渲染多个元素
React要求组件返回一个顶层元素,尽管这个元素能够包含应用程序需要的其他元素。例如,Summary组件返回一个顶层的h4元素,其中包含一系列的div元素,这些元素是为名称props中的元素生成的。
有些时候,对单一顶层元素的要求会造成问题。HTML 规范对元素的组合方式进行了限制,这可能与单一的 元素的要求相冲突。为了证明这个问题,我已经改变了由 App组件渲染的内容,使其包含一个表格,其中每个tr元素的内容都由一个子 组件产生,如清单10-25所示。