React: 自定义组件及组件通讯

19782
2022/11/16 01:56:05

本章我们将正式开启 React 组件之路。虽然我们之前章节的都叫组件,但整个应用只有一个组件,略显单薄。同时,我们还将讨论组件之间如何进行通讯。

自定义组件

何谓自定义组件?我们之前章节都在自定义组件——App就是。在实际项目中,可能有多个功能,不可能把它们全堆在 App里,而是需要把不同的功能封装成不同的组件,然后在 App 里调用。

以上一章最后的那个带“正在载入”的用户列表为例,它至少可以拆解成两个组件:

  • Loading:显示“正在载入”
  • UserList:显示用户列表

更进一步,你会发现,用户列表中有多个用户,所以可以继续拆解。最后,将其拆解成以下组件:

组件说明
App主组件
Loading“正在载入”组件
UserList用户列表组件
UserItem单个用户组件

明确了这些,我们开始拆解。动手之前,我想说一下 React 项目的目录结构:

  • 通常组件会放在单独的 compontents 目录
  • 每个组件是一个单独的*.jsx 文件
  • 如果组件包含子组件,则创建和组件同名目录
    • index.jsx 提供入口组件的定义
    • Xxxx.jsx 提供子组件的定义

由此,我们的目录将变为:

- src
	- App.jsx  -- 主组件
	- compontents -- 组件目录
		- Loading.jsx -- “正在载入”组件
		- Users -- 用户相关的组件
			- index.jsx -- 用户列表组件
			- UserItem.jsx -- 单个用户组件

Loading 组件

import React from 'react';

export default function Loading() {
  return <div>正在载入</div>;
}

Loding 组件很简单,就一个提示文本。

UserItem 组件

import React from 'react';

export default function UserItem(props) {
  const { user } = props;
  return (
    <li>
      姓名:{user.name}, 年龄:{user.age}
    </li>
  );
}
  • 第3行:我们给这个函数式组件声明了一个形参,这样它就可以通过 props 参数,从父组件那里得到需要的数据
  • 第4行:这是 ES6 的解构写法,rust 也有类似的写法。它和 const user = props.user; 等价
  • 第6~8行:显示用户信息。细心的你可能发现了(当然,我知道你没这么细心),在第 6 行没有 key 属性。这是因为,在父组件的循环中才有必要给 key 属性(当然,你同时在这里设置 key 也行,但不要和父组件设置的重复)

UserList 组件

import React, { useEffect, useState } from 'react';
import UserItem from '../Users/UserItem';

export default function UserList() {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    // 模拟从后端获取数据
    setTimeout(() => {
      setUsers([
        { id: 123, name: 'axum.rs-1', age: 18 },
        { id: 456, name: 'axum.rs-2', age: 14 },
        { id: 789, name: 'axum.rs-3', age: 21 },
        { id: 1011, name: 'axum.rs-4', age: 28 },
        { id: 1213, name: 'axum.rs-5', age: 38 },
        { id: 1415, name: 'axum.rs-6', age: 58 },
        { id: 1617, name: 'axum.rs-7', age: 82 },
        { id: 1819, name: 'axum.rs-8', age: 12 },
        { id: 2021, name: 'axum.rs-9', age: 13 },
        { id: 2223, name: 'axum.rs-10', age: 33 },
      ]);
    }, 3000);
  }, []);

  return (
    <ul>
      {users.map((user) => (
        <UserItem key={user.id} user={user} />
      ))}
    </ul>
  );
}
  • 第2行:从当前目录导入 UserItem 组件
  • 第28行:由之前的直接 li 改成了我们自定义的 UserItem 属性
    • 并且使用了 user.id 来设置每个 UserItem 的key
    • 同时,传递了名为user的属性给子组件,属性的值是 map 里面的 user

App 组件

import React from 'react';
import Loading from './compontents/Loading';
import UserList from './compontents/Users';

function App() {
  return (
    <div>
      <h1>用户列表</h1>
      <Loading />
      <UserList />
    </div>
  );
}

export default App;

现在的 App 组件是相当清爽了。

  • 第2、3行:分别导入 Loading 组件和 UserList 组件
  • 第9、10行:分别使用 LoadingUserList 组件

运行之后发现,无论是否完成加载,Loading 组件一直都在。下面将逐步解决这个问题。

本节代码:axum-rs-react-compontents

自定义组件如何传递标签体的内容

// App.jsx
<Loading msg="正在载入" />


// Loading.jsx
import React from 'react';

export default function Loading(props) {
  const {msg} = props;
  return <div>{msg}</div>;
}
// App.jsx
<Loading>正在载入</Loading>

你会发现,根本没用。如何让它正常工作呢?React 会自动往 props 插入一个名为 children 的属性,它就是你要的:

// App.jsx
<Loading>正在载入</Loading>

// Loading.jsx
export default function Loading(props) {
  const {children} = props;
  return <div>{children}</div>;
}

既然知道了 ES6 支持解构,那我们可以这样写:

// Loading.jsx
export default function Loading({children}) {
  return <div>{children}</div>;
}

父子组件的通讯

所谓父子组件的通讯,是指在父子组件之间共享数据及交互。

父组件到子组件的通讯

我们已经用到了这种通讯方式:

  • App 调用 Loading时:<Loading>正在载入</Loading>
  • UserList 调用 UserItem 时: <UserItem key={user.id} user={user} />

你会发现父组件到子组件的通讯非常简单,使用 props 传递属性即可。

子组件到父组件的通讯

那么子组件如何发送数据给父组件呢?好像没办法,其实不然。你要明白的是:

props既然可以传递字符串(<Loading msg="正在载入"/>),也可以传递对象(<UserItem key={user.id} user={user} />),为什么不能传递函数呢?——你要知道,Javascript可是所谓的万物皆对象,函数也是对象。

好的,明白了,props 可以传递函数,那么然后呢?然后就可以将父组件定义的某个函数传递给子组件,而这个传递过去的函数就可以对数据进行操作,实现子组件到父组件的通讯。

我们回到遗留的问题:如何在用户列表完成数据加载之后,让“正在载入”的提示文本消失。

根据上面的分析,我们可以知道:使用子组件到父组件通讯——当UserList 完成数据加载之后,通过 App 传递给它的回调函数通知 App

具体实现可以有很多种,请读者根据我们给出的示例进行发散思维

  • App 维护 isLoading 状态和设置该状态的函数 setLoading
  • 当调用 UserList 的时候,通过 propssetLoading 传递给它
  • UserList 加载完数据之后,调用 App 传递过来的 setLoading 函数,通知 App 组件数据加载完成
  • App 根据 isLoading 状态决定是否显示 Loading 组件

App 组件

import React, { useState } from 'react';
import Loading from './compontents/Loading';
import UserList from './compontents/Users';

function App() {
  const [isLoading, setLoading] = useState(false);

  return (
    <div>
      <h1>用户列表</h1>
      {isLoading && <Loading>载入中。。。</Loading>}
      <UserList setLoading={setLoading} />
    </div>
  );
}

export default App;

  • 第6行:创建用于维护是否正在载入的状态 isLoading 及其对应的设置函数 setLoding
  • 第11行:如果 isLoading == true,则显示 Loading 组件
  • 第12行:将 setLoading 函数传递给 UserList 组件

UserList 组件

  • 第4行:从 props 里接收父组件传递过来的 setLoading 函数
  • 第8行:获取数据之前,调用 setLoading(true),开启“正在载入”
  • 第23行:获取数据完成之后,调用 setLoading(false),关闭“正在载入”

本节代码:axum-rs-react-compontents-communicate-1

兄弟组件及深层嵌套组件的通讯

对于简单的兄弟组件(组件嵌套一两层),完全可以使用上面的父子组件的通讯方式进行通讯。

随着项目的开发,组件嵌套可能会越来越深,如果还使用父子组件通讯的方式进行通讯,势必会造成:

  • 重复的 props 传递链
  • 臃肿的主组件——其中很多都只是为了通讯

基于此,可以选择:

  • 基于订阅的通讯,比如 pubsub-js
  • 使用集中式的通讯——状态共享

由于本人更倾向使用状态共享,所以给出一个简单的 pubsub-js 示例。至于状态共享,将在后面的章节进行讨论。

// 一个 pubsub-js 的简单示例
import PubSub from "pubsub-js";

// 定义订阅处理

const subHandler = (msg, data) => {
  console.log(msg, data);
};

// 订阅
const token = PubSub.subscribe("FOOBAR", subHandler);

// 发布
PubSub.publish("FOOBAR", "你好,世界");

// 取消订阅
PubSub.unsuscribe(token);


// -- 原则
// 1. 先订阅,再发布
// 2. 在 compontentDidMount() / (useEffect 的函数体 )进行订阅
// 3. 在 componentWillUnmount() / (useEffect 的返回值 )取消订阅