使用React和TypeScript来构建用户界面

React公式网站的文档已于2023年3月16日进行了修订(请参阅”Introducing react.dev”)。本文是对入门指南第1章”描述UI”的简要总结,同时加入了TypeScript的代码。然而,我们省略了针对初学者的JavaScript基础解释。

另外,请参考其他本系列解说的文章,名称为“React + TypeScript: 学习React官方文档基础解说《学习React》”。

这篇文章的主题是关于React组件和JSX返回值的写法。React应用程序通过组合组件并使用JSX来绘制页面。我们在CodeSandbox上公开了代码示例,请参考。

制作你的第一个组件

创建React组件的步骤如下:

    1. 组件必须要进行导出。

 

    1. 定义一个函数组件。

导出可以是默认导出或具名导出。

返回值需要在JSX中加入。

JSX描述原则上是一行。

当需要多行描述时,请务必使用圆括号()将其包裹起来。

如果没有圆括号,return语句会被认为是在换行处结束。

React的函数组件是指按照标准JavaScript函数定义的组件。然而,组件名称必须是大驼峰式命名(也称为帕斯卡命名法)(请参考「步骤2:定义函数」)。

以下是一个组件的示例代码(JavaScript模块)。如果使用TypeScript,返回值将会被推断出来。如果想要明确地指定,请使用JSX.Element进行类型注释。

export default function Profile() {
	return (
		<img
			src='https://i.imgur.com/MK3eW3Am.jpg'
			alt='Katherine Johnson'
		/>
	)
}

在React中,我们可以使用具备代码和标记的组件来构建从小型UI部件到整个页面的内容。而React则使用JavaScript来渲染页面。类似Next.js的框架更进一步,能够自动生成HTML页面从React组件中。这意味着在JavaScript代码加载前,我们就能够显示应用程序内容的一部分(参见“Components all the way down”)。

我将这篇文章中提供的代码示例改为了React + TypeScript,并稍作修改后发布在下面的001示例中。

React + TypeScript: 描述界面 01

 

组件的导入和导出(Importing and Exporting Components)

React可以用组件来构建应用程序。它的优点是可以组合和重复使用,易于管理和维护。在前端示例001的应用程序中,让我们简化根模块(src/App.tsx)。将JSX的实质标记分离到另一个模块src/Gallery.tsx中(示例002)。

import { FC } from 'react';
import { Profile } from './Profile';

export const Gallery: FC = () => {
	return (
		<section>
			<h1>Amazing scientists</h1>
			<Profile />
		</section>
	);
};
import React from 'react'
import { Gallery } from './Gallery';
import './styles.css';

export default function App(): JSX.Element {
	return <Gallery />;
}

样例002 ■ React + TypeScript:描述UI 02

 

React组件的导出可以是默认导出或具名导出都可以。在示例中,根组件(App)是默认导出,子组件(Gallery和Profile)是具名导出。请注意,导入语法会稍微变化,请注意。团队统一使用其中一种方式也是一个想法(请参考「默认导出 vs 具名导出」)。另外,默认导出一次只能有一个模块。

使用JSX编写标记

JSX是JavaScript的语法扩展。在JavaScript代码中,可以编写类似HTML的标记。

随着Web变得越来越交互式,内容也开始由逻辑来决定。因此,React将HTML交给了JavaScript,并将渲染逻辑和标记都包含在组件中。

JSX语法几乎遵循HTML,但规则稍微严格一些。

    1. 返回值需要整理为一个根元素。

如果不想将其包含在一个元素中,则可以使用片段(参考” (<></>)”)。

标签必须全部闭合。

空元素也必须要闭合。

要添加到元素的属性通常为驼峰式(参考常见组件,例如

)。

属性的连字符(-)需要去掉,使用驼峰式来编写。

因为历史原因,与HTML相同,aria-*和data-*需要使用连字符(参考aria-*和data-*)。

JSX 看起来像是 HTML,但实际上会被转换为 JavaScript 的标准对象。除非将这两个对象放在一个数组中,否则不能从函数中返回。因此,组件的返回值的 JSX 必须被封装在一个单一的元素中(参考「为什么需要包装多个 JSX 标签?」)。

当你想要将已经编写好的HTML标记转换为JSX时,你还可以使用转换工具(「专业提示:使用JSX转换器」)。

在JSX中使用花括号({})来书写JavaScript代码 (在JSX中使用花括号的JavaScript代码)

在JSX中,用于编写JavaScript表达式的语法是大括号{}。将上述示例002中的组件Profile改写为以下方式时,可以在JSX内引用JavaScript的变量(示例003)。

import { FC } from 'react';

export const Profile: FC = () => {
	const photo = 'https://i.imgur.com/MK3eW3As.jpg';
	const description = 'Katherine Johnson';
	return <img src={photo} alt={description} />;
};

样本003■React + TypeScript:描述UI 03

 

JSX中的花括号{}语法不仅限于JavaScript变量的编写。它可以引用包括函数调用在内的任何JavaScript表达式。另外,可以将在任何模块中始终保持不变的变量(如下所示的copyrightOwner)定义在组件的外部。

import { Gallery } from './Gallery';
import { Footer } from './Footer';
import './styles.css';

export default function App(): JSX.Element {
	return (
		<>
			<Gallery />
			<Footer />
		</>
	);
}
import { FC } from 'react';

const copyrightOwner = 'Fumio Nonaka';
export const Footer: FC = () => {
	const getYear = () => new Date().getFullYear();
	return (
		<footer>
			Copyright &#169;2000-{getYear()} {copyrightOwner}
		</footer>
	);
};

在给JSX提供JavaScript代码对象的情况下,需要使用双花括号{}来进行两次嵌套。另外,请注意CSS属性应使用驼峰命名法(如backgroundColor、fontFamily和lineHeight)。

import { FC } from 'react';

const copyrightOwner = 'Fumio Nonaka';
export const Footer: FC = () => {
	const getYear = () => new Date().getFullYear();
	return (
		<footer
			style={{
				backgroundColor: 'paleturquoise',
				fontFamily: 'Helvetica Neue',
				lineHeight: '2rem'
			}}
		>
			Copyright &#169;2000-{getYear()} {copyrightOwner}
		</footer>
	);
};

此外,为了在处理多个变量(copyrightOwner)和对象(styles)时方便统一,有时候会将它们集中放在一个对象(footerInfo)中进行处理(示例004)。

import { FC } from 'react';

const footerInfo = {
	copyrightOwner: 'Fumio Nonaka',
	styles: {
		backgroundColor: 'paleturquoise',
		fontFamily: 'Helvetica Neue',
		lineHeight: '2rem'
	}
};
export const Footer: FC = () => {
	const getYear = () => new Date().getFullYear();
	const { copyrightOwner, styles } = footerInfo;
	return (
		<footer style={styles}>
			Copyright &#169;2000-{getYear()} {copyrightOwner}
		</footer>
	);
};

样本004■React + TypeScript:描述UI 04

 

将属性传递给组件

React的组件通过称为props的属性将数据传递给子组件。传递方式与HTML标签属性相同的语法。然而,数据不仅限于文本,还包括JavaScript的所有值,如对象、数组和函数等。此外,还可以传递要包含在子组件中的节点(文本)。

import { Gallery } from './Gallery';
import { Footer } from './Footer';
import './styles.css';

export type FooterInfo = {
	copyrightOwner: string;
	styles: {
		backgroundColor: string;
		fontFamily: string;
		lineHeight: string;
	};
};
const footerInfo: FooterInfo = {
	copyrightOwner: 'Fumio Nonaka',
	styles: {
		backgroundColor: 'paleturquoise',
		fontFamily: 'Helvetica Neue',
		lineHeight: '2rem'
	}
};
export default function App(): JSX.Element {
	const getYear = () => new Date().getFullYear();
	return (
		<>
			<Gallery />
			<Footer footerInfo={footerInfo}>
				Copyright &#169;2000-{getYear()} {footerInfo.copyrightOwner}
			</Footer>
		</>
	);
}

子组件接收的参数是一个对象(props),其中包含了从父组件传递过来的所有属性。下面的代码示例中,Footer组件使用对象解构赋值来提取必要的属性(copyrightOwner和styles)。另外,接收的子节点属性是children(类型为React.ReactNode),请参考”将JSX作为children传递”。完整代码示例请见样例005。

import React, { FC } from 'react';
import type { FooterInfo } from './App';

type Props = {
	children: React.ReactNode;
	footerInfo: FooterInfo;
};
export const Footer: FC<Props> = ({
	children,
	footerInfo: { copyrightOwner, styles }
}) => {
	return (
		<footer style={styles}>
			{children}
		</footer>
	);
};

样例005 ■ React + TypeScript:描述UI 05

 

当想要将一个对象中的所有属性分解并传递给子组件时,可以使用扩展语法。下面提取的代码是将前面示例中的属性(footerInfo)用扩展语法重新编写的方式。

export default function App(): JSX.Element {

	return (
		<>

			{/* <Footer footerInfo={footerInfo}> */}
			<Footer {...footerInfo}>
				Copyright &#169;2000-{getYear()} {footerInfo.copyrightOwner}
			</Footer>
		</>
	);
}
type Props = {

	// footerInfo: FooterInfo;
	copyrightOwner: FooterInfo['copyrightOwner'];
	styles: FooterInfo['styles'];
};
export const Footer: FC<Props> = ({

	// footerInfo: { copyrightOwner, styles }
	copyrightOwner,
	styles
}) => {
	return <footer style={styles}>{children}</footer>;
};

每个组件接收的属性(props)的值都可以根据需要进行更改。但是,props本身是不可变的(immutable)。当想要改变给组件的属性值时(例如用户交互或数据更改时),必须依赖于父组件。然后,一个新的数值对象会被创建并传递过来。旧属性会被废弃,JavaScript会为新的对象分配内存(参见”属性如何随时间改变”)。

有条件的渲染

在某些情况下,你可能希望根据条件来显示不同的内容。React可以使用JavaScript条件语法来改变要渲染的JSX内容。首先是if语句。下面的代码示例根据传递给子组件(Item)的属性(isPacked)的布尔值来返回不同的JSX值。

import { Item } from './Item';
import './styles.css';

export default function PackingList() {
	return (
		<section>
			<h1>Sally Ride's Packing List</h1>
			<ul>
				<Item isPacked={true} name="Space suit" />
				<Item isPacked={true} name="Helmet with a golden leaf" />
				<Item isPacked={false} name="Photo of Tam" />
			</ul>
		</section>
	);
}
import { FC } from 'react';

type ItemDescriptions = { isPacked: boolean; name: string };
export const Item: FC<ItemDescriptions> = ({ isPacked, name }) => {
	if (isPacked) {
		return <li className="item">{name}</li>;
	}
	return <li className="item">{name}</li>;
};

然而,根据条件分支返回的JSX几乎是相同的。将共享代码合并为一种方式可以节省修复和修改时的不必要的工作量。在这种情况下可以使用条件(三元)运算符?:(示例006)。

import { FC } from 'react';

type ItemDescriptions = { isPacked: boolean; name: string };
export const Item: FC<ItemDescriptions> = ({ isPacked, name }) => {
	return <li className="item">{isPacked ? name + '' : name}</li>;
};

样例006 ■使用React + TypeScript描述UI 06

 

使用逻辑与(&&)运算符时,代码可以更加简短。当左边表达式的值为布尔值时,如果为真,则返回右边表达式,如果为假,则返回左边值为假。此外,在React中,JSX树中的假值,如false(null和undefined也同样),不会进行渲染(请参阅「逻辑与运算符 (&&)」)。

export const Item: FC<ItemDescriptions> = ({ isPacked, name }) => {
	// return <li className="item">{isPacked ? name + ' ✔' : name}</li>;
	return <li className="item">{name} {isPacked && ''}</li>;
};

请注意的是,逻辑与运算符&&的条件判断和返回值是不同的。左边的表达式作为条件会进行布尔值评估。但是,返回值是左边或右边表达式的值。如果给左边传递一个数值表达式,除了0之外的值会被评估为true,并返回右边表达式的值。问题出现在值为0的情况下。条件判断为false,但返回值却变成了0。这几乎在大多数情况下都不是期望的结果。为了避免这种情况,请将左边作为条件表达式,或者使用Boolean()函数或两个逻辑非否定运算符!将其转换为布尔值。

渲染列表

有时候,您可能需要从确定形式的多个数据中以列表方式显示组件。这种情况下,使用数组存储要处理的数据是很合适的。使用Array.prototype.filter()或Array.prototype.map()等方法,可以从数据中创建组件列表(数组)。

当React在组件的返回值中插入JSX节点数组时,会按顺序渲染这些节点的JSX元素。在此过程中,每个节点的根元素必须具有唯一的key属性。这是因为通过key属性,React可以识别原始数组中的哪个数据对应于JSX中的哪个元素(请参阅「使用key保持列表项顺序」)。关于key,有两个规则必须遵守(请参阅「key的规则」)。

keyの値はJSXノードの配列の中で一意でなければなりません。

他のJSXノードの配列と重複するのは結構です。

一度与えたkeyの値は変えないでください。

もとデータとJSXノードの対応が識別できなくなるからです。
レンダリングしているときに動的にkeyの値を生成するのもいけません。

创建一个与数组中各元素相对应的新元素数组,可以使用Array.prototype.map()方法。我们将使用下一个模块src/data.ts中的people作为原始数据数组。

export type Person = {
	id: number; // JSXでkeyとして用いる
	name: string;
	profession: string;
	accomplishment: string;
	imageId: string;
};
export const people: Person[] = [
	{
		id: 0,
		name: 'Creola Katherine Johnson',
		profession: 'mathematician',
		accomplishment: 'spaceflight calculations',
		imageId: 'MK3eW3A'
	},
	// ...[略]...
];

在根模块src/App.tsx中,组件List会从数组people中创建一个JSX节点数组listItems,并返回。

import { people } from ./data;
import { getImageUrl } from ./utils;
import type { Person } from ./data;
import ./styles.css;

export default function List() {
	const listItems = people.map((person: Person) => {
		const { accomplishment, id, name, profession } = person;
		return (
			<li key={id}>
				<img src={getImageUrl(person)} alt={name} />
				<p>
					<b>{name}</b>
					{` ${profession} `}
					known for {accomplishment}
				</p>
			</li>
		);
	});
	return <ul>{listItems}</ul>;
}
import { Person } from "./data";

export function getImageUrl(person: Person) {
	return `https://i.imgur.com/${person.imageId}s.jpg`;
}

我已在下面的示例007中提供了整个示例代码。

React + TypeScript: 描述UI 07的例子

 

Array.prototype.filter()方法返回一个由符合条件的元素组成的新数组,其数据格式不变。通过将前面示例007中的List组件进行如下更改,可以显示化学家列表。

配列元素的数据格式保持不变,Array.prototype.filter()方法从条件匹配的元素返回新的元素数组。通过对前面示例007中List组件的下列更改,可以显示化学家列表。

export default function List() {
	const chemists = people.filter((person) => person.profession === 'chemist');
	// const listItems = people.map((person: Person) => {
	const listItems = chemists.map((person: Person) => {

	});
}

如果使用<></>语法来包装JSX节点的根节点为片段,则无法添加key。最好使用诸如

之类的元素。如果必须使用片段,可以使用语法并提供key(参见“为每个列表项显示多个DOM节点”)。

key的值必须是唯一的。也许你会认为可以使用数据数组的索引来代替。实际上,如果没有提供key,React会无奈地使用内部的数组索引。但是,一旦有数据的添加、删除或排序,索引就会被重新分配。这样一来,之前给定的key值就被违反第二个规定而改变了(请参考“为什么React需要key?”)。

保持组件的纯洁性

純粹的功能只对给定的数据进行操作。React组件通过使用纯粹的函数编写,可以减少代码的扩张,并减少错误和意外行为的困扰。函数式编程的函数具有以下两个特点(参见“纯度:组件作为公式”)。

関数内のデータだけを加工する: 関数が呼び出される前に外にあったオブジェクトや変数は変えません。

入力が同じなら出力も同じ: 同じ入力を与えられたら、純粋な関数の出力する結果はつねに同じだということです。

换句话说,React的纯组件在被两次调用时必须返回相同的JSX。StrictMode是用来验证这一点的。在开发过程中,默认情况下会对组件的初始渲染函数进行两次调用。StrictMode会测试组件函数是否是纯函数(请参阅「Detecting impure calculations with StrictMode」)。另外,详细信息请阅读「React + TypeScript: リアクティブなエフェクト(useEffect)のライフサイクル」。

纯粹的函数不会修改超出其作用域范围的变量或对象(这种修改被称为“突变”)。然而,在函数渲染时,在作用域内创建并修改对象是可以的。下面的代码是一个例子,称为“局部突变”(参考“局部突变:您组件的小秘密”)。

import { FC } from 'react';

type Props = {
	guest: number;
};
const Cup: FC<Props> = ({ guest }) => {
	return <h2>Tea cup for guest #{guest}</h2>;
};
export default function TeaGathering() {
	const cups = [];
	for (let i = 1; i <= 12; i++) {
		cups.push(<Cup key={i} guest={i} />);
	}
	return cups;
}

函数必须保持纯净,然而有时需要进行屏幕更新、动画或数据变更等操作。这些就是副作用(请参考“可以引起副作用的地方”)。

在React中,通常副作用会由事件处理程序处理。但是,组件中定义的事件处理程序在渲染期间不会被执行。因此,处理程序函数不需要是纯函数。对于那些很难在事件处理程序中处理的副作用,可以使用useEffect。处理将在允许副作用的渲染之后进行(请参考“React + TypeScript:响应式效果(useEffect)的生命周期”)。但是,请尽量避免使用(请参考“React:不需要使用效果(useEffect)的情况”)。

关于React的基础知识,我主要讲解了函数组件和JSX。由于并没有详细讨论TypeScript的类型定义,建议您参考CodeSandbox上的示例代码。

bannerAds