组件、属性与状态

组件、属性与状态

组件 & render 函数

React 的一个核心概念就是组件

组件更像是将 UI 拆分得到的独立可复用的小块,其能够接受属性的传入,并且返回出描述屏幕上的展示内容的 React 元素。

我们将会从类组件入手介绍 React 组件的编写方式。一个经典的类组件的结构如下(即下发框架之中的 App.tsx 文件内容):

class App extends React.Component {
    render(): ReactElement {
        return <p> Hello, React! </p>;
    }
}

每一个类组件都是 React.Component 类的子类,其中我们需要重载 render 函数以定义这个组件在屏幕上的展示方式(用户界面)。render 函数的返回值为使用标签语法编写的 ReactElement。比如说上述 App 组件就返回了一句问候语,这就代表 App 组件在实例化的时候会渲染出这一句问候语。

<App />;
<App title="SAST-React" />; // 'props' will be introduced later
<App><div /></App>;

编写第一个自定义组件

我们说过组件是独立可复用的,也就是说我们可以编写自己的组件并使用。比如说我们可以使用类组件写一个渲染一个红色正方形的组件 Square(直接在 App.tsx 文件内插入下述代码即可):

class Square extends React.Component {
    render(): ReactElement {
        return <div style={{width: 100, height: 100, backgroundColor: "red"}} />;
    }
}

这个时候我们就可以在 App 组件的 render 函数里面使用这个组件了:

// Component 'App'
render(): ReactElement {
    return (
        <div>
            <p>This is a square!</p>
            <Square />
        </div>
    );
}

编译之后就能看到屏幕上的一个红色正方形。

自定义组件的状态(传入 props

刚刚编写 Square 的时候应该注意到了,正方形的尺寸和颜色都是写死的,实际上我们可以通过传入属性的方式完成对组件形态的控制,正如 HTML 标签的属性。

我们在实例化 Square 的时候可以如下面代码一样传入若干的属性:

// Part of 'render' function in Component 'App'
<Square size={100} color="red" />

那么这些属性我们怎么在组件内部捕获呢?答案是通过 this.props 变量:

// Component 'Square'
render(): ReactElement {
    return <div
        style={{
            width: this.props.size,
            height: this.props.size,
            backgroundColor: this.props.color
        }}
    />;
}

但你会发现报错,基本的意思就是 props 上没有成员 size, color。这实际上就是 TypeScript 的要求,你需要为组件指定这个组件需要哪些属性,哪些属性是可选的等等,这样可以有效避免出现难以探查的 undefined 的问题。

指定的方式是 interface,你需要在这个接口之中定义这个组件接受的所有属性。比如说 Square 只需要接受尺寸和颜色:

interface SquareProps {
    size: number;
    color: string;
}

这里也可以指定部分属性是可选的,这样的话实例化组件就没必要传入这一部分属性,并且通过 this.props 访问这些属性会得到 undefined 。但是这样的行为显然更为可控。

我们在 TypeScript 文档 中提到过接口的属性除了可以规定为可选属性,还可以规定为只读属性。实际上 React 严格要求属性是不可更改的,这种要求类似于纯函数,纯函数的参数传入之后不可修改,至少不会影响到原变量。

所以如果严格要求的话,接口定义应该写为:

interface SquareProps {
    readonly size: number;
    readonly color: string;
}

如果不写的话可能会在部分较为严格的条件下编译失败。

之后将其挂载到组件的定义上,挂载的语法类似于 C++ 的模板语法:

class Square extends React.Component<SquareProps> { // Register 'props' here
    // ...
}

这样你就基本了解了如何给组件传入属性。不过现在你应该已经注意到了属性是从父组件向子组件传递的,这也是后续会提到的瀑布数据流,也就是说这些控制组件形态的属性数据的流向永远是向下的。

实现动态的 UI(添加组件的 state

我们已经提到了使用 props 来自定义组件的形态,但根据 props 的不可变性就可以知道只使用 props 控制的组件等价于纯函数,也就是说其渲染的 UI 是完全静态的,只是单纯地简化了一些代码写法而已。

为了实现动态的效果,比如最简单的点击变色,就需要让组件具有一定的记忆功能,让组件管理自己的状态,这就是 state

就和类成员一样,组件的 state 是每一个实例化的对象所管理的一些变量,这些变量可能需要通过父组件初始化,但是这之后这些 state 就完全受控于管理它们的组件本身。

比如说我们想让正方形的颜色不受控于父组件 App,那么我们可以设立 state 来管理。给组件创建状态的方式是简单的,我们需要编写一个自定义的构造函数:

// Component 'Square'
constructor(props: SquareProps) {
    super(props); // Never forget this when writing constructor!
    this.state = { color: "red" };
}

之后在渲染函数之中就可以将合适的 this.props 替换为 this.state,这就意味着一部分渲染结果由组件本身状态接管,不受控于父组件传入的属性。比如说这里,正方形尺寸本身还是受控于父组件,但是颜色由自身控制:

// Component 'Square'
render(): ReactElement {
    return <div
        style={{
            width: this.props.size,
            height: this.props.size,
            backgroundColor: this.state.color
        }}
    />;
}

变更当前组件的状态使用的函数为 this.setState,这个函数由 React.Component 实现,可以直接继承使用。

一定注意,不要使用 this.state = ... 这样的赋值句改变状态,因为 this.setState 内部除了修改状态还包括触发再次渲染等等过程,具体流程见下面的讲解。

修改状态的方式为:

this.setState({ ...this.state, color: "blue" });

这个语句就会把状态之中的 color 属性修改为 blue

// Component 'Square'
render(): ReactElement {
    return <div
        style={{
            width: this.props.size,
            height: this.props.size,
            backgroundColor: this.state.color
        }}
        onClick={() => this.setState({ ...this.state, color: "blue" })}
    />;
}

现在我们希望让用户点击正方形的时候,正方形颜色在红色和蓝色之间相互切换。那么我们可以首先编写一个切换颜色的函数,内部调用 this.setState() 方法:

// Component 'Square'
switchColor(): void {
    this.setState((prev) => ({
        ...prev,
        color: prev.color === "red" ? "blue" : "red",
    }));
}

这里注意,我们在 this.setState 函数中传入了一个回调函数,原因见下述解释。

interface SquareStates {
    readonly color: string;
}

class Square extends React.Component<SquareProps, SquareStates> { /* ... */ }