组件通信
一个 React 应用是由许多个组件像搭积木一样搭建而成的,只要应用不是完全由展示组件组成的,组件之间就难免需要进行通信。其实,前面一些内容已经涉及组件的通信,只是我们并没有刻意强调这个概念。下面系统地介绍组件是如何进行通信的。
父子组件通信
父子组件通信是最常见的通信形式,例如 4.2 节的 UserListContainer 组件获取到的用户数据需要通过 UserList 组件展示,这时 UserListContainer 和 UserList 就存在父子组件通信。UserListContainer 作为父组件,将获取到的用户信息通过子组件 UserList 的 props 传递给 UserList。所以父组件向子组件通信是通过父组件向子组件的 props 传递数据完成的。代码如下:
class UserList extends React.Component {
render() {
return (
<div>
<ul className="user-list">
{this.props.users.map(function (user) {
return (
<li key={user.id}>
<span>{user.name}</span>
</li>
);
})}
</ul>
</div>
)
}
}
import UserList from './UserList'
class UserListContainer extends React.Component {
constructor(props) {
super(props);
this.state = {
users: []
}
}
componentDidMount() {
var that = this;
fetch('/path/to/user-api').then(function (response) {
response.json().then(function (data) {
that.setState({users: data})
});
});
}
render() {
return (
{/* 通过 props 传递 users */}
<UserList users={this.state.users}/>
)
}
}
当子组件需要向父组件通信时,又该怎么做呢?答案依然是 props。父组件可以通过子组件的 props 传递给子组件一个回调函数,子组件在需要改变父组件数据时,调用这个回调函数即可。下面为 UserList 再增加一个添加新用户的功能:
class UserList extends React.Component {
constructor(props) {
super(props);
this.state = {
newUser: ''
};
this.handleChange = this.handleChange.bind(this);
this.handleClick = this.handleClick.bind(this);
}
handleChange(e) {
this.setState({newUser: e.target.value});
}
// 通过 props 调用父组件的方法新增用户
handleClick() {
if(this.state.newUser && this.state.newUser.length > 0) {
this.props.onAddUser(this.state.newUser);
}
}
render() {
return (
<div>
<ul className="user-list">
{this.props.users.map(function (user) {
return (
<li key={user.id}>
<span>{user.name}</span>
</li>
);
})}
</ul>
<input onChange={this.handleChange} value={this.state.newUser}/>
<button onClick={this.handleClick}>新增</button>
</div>
)
}
}
在 input 内输入新用户的名称,然后点击新增按钮,调用 handleClick 方法,在 handleClick 内部,调用通过 props 传递过来的 onAddUser 执行保存用户的逻辑。下面来看一下 onAddUser 在 UserListContainer 中的实现:
import UserList from './UserList'
class UserListContainer extends React.Component {
constructor(props) {
super(props);
this.state = {
users: []
}
this.handleAddUser = this.handleAddUser.bind(this);
}
componentDidMount() {
var that = this;
fetch('/path/to/user-api').then(function (response) {
response.json().then(function (data) {
that.setState({users: data})
});
});
}
// 新增用户
handleAddUser(user) {
var that = this;
fetch('/path/to/save-user-api', {
method: 'POST',
body: JSON.stringify({'username': user})
}).then(function (response) {
response.json().then(function (newUser) {
//将服务器端返回的新用户添加到 state 中
that.setState((preState) => ({users: preState.users.concat([newUser])}))
});
});
}
render() {
return (
{/* 通过 props 传递 users 和 handleAddUser 方法 */}
<UserList users={this.state.users} onAddUser={this.handleAddUser}
/>
)
}
}
子组件 UserList 通过调用 props.onAddUser 方法成功地将待新增的用户传递给父组件 UserListContainer 的 handleAddUser 方法执行保存操作,保存成功后,UserListContainer 会更新状态 users,从而又将最新的用户列表传递给 UserList。这一过程既包含子组件到父组件的通信,又包含父组件到子组件的通信,而通信的桥梁就是通过 props 传递的数据和回调方法。
兄弟组件通信
当两个组件不是父子关系但有相同的父组件时,称为兄弟组件。注意,这里的兄弟组件在整个组件树上并不一定处于同一层级,如图4-1所示的两种情况,B 和 C 都是兄弟组件,因为他们都有相同的父组件 A。

兄弟组件不能直接相互传送数据,需要通过状态提升的方式实现兄弟组件的通信,即把组件之间需要共享的状态保存到距离它们最近的共同父组件内,任意一个兄弟组件都可以通过父组件传递的回调函数来修改共享状态,父组件中共享状态的变化也会通过 props 向下传递给所有兄弟组件,从而完成兄弟组件之间的通信。
我们在 UserListContainer 中新增一个子组件 UserDetail,用于显示当前选中用户的详细信息,比如用户的年龄、联系方式、家庭地址等。这时,UserList 和 UserDetail 就成了兄弟组件,UserListContainer 是它们的共同父组件。当用户在 UserList 中点击一条用户信息时,UserDetail 需要同步显示该用户的详细信息,因此,可以把当前选中的用户 currentUser 保存到 UserListContainer 的状态中。
先来修改 UserList 组件:
class UserList extends React.Component {
constructor(props) {
super(props);
this.state = {
newUser: ''
};
this.handleChange = this.handleChange.bind(this);
this.handleClick = this.handleClick.bind(this);
}
handleChange(e) {
this.setState({newUser: e.target.value});
}
// 通过 props 调用父组件的方法新增用户
handleClick() {
if(this.state.newUser && this.state.newUser.length > 0) {
this.props.onAddUser(this.state.newUser);
}
}
// 通过 props 调用父组件的方法,设置当前选中的用户
handleUserClick(userId) {
this.props.onSetCurrentUser(userId);
}
render() {
return (
<div>
<ul className="user-list">
{this.props.users.map((user) => {
return (
<li key={user.id}
{/* 使用不同样式显示当前用户 */}
className={(this.props.currentUserId === user.id) ? 'current' : ''}
onClick={this.handleUserClick.bind(this, user.id)}
>
<span>{user.name}</span>
</li>
);
})}
</ul>
<input onChange={this.handleChange} value={this.state.newUser}/>
<button onClick={this.handleClick}>新增</button>
</div>
)
}
}
我们为 UserList 添加了处理点击用户项的回调函数 handleUserClick,还为当前处于选中状态的用户项添加了名为 current 的样式。
再来创建 UserDetail 组件:
function UserDetail(props) {
return (
<div>
{props.currentUser ? (
<div>用户姓名:{props.currentUser.name}</div>
<div>用户年龄:{props.currentUser.age}</div>
<div>用户联系方式:{props.currentUser.phone}</div>
<div>家庭地址:{props.currentUser.address}</div>
): ''}
</div>
)
}
UserDetail 不需要维护自己的状态,因此最适合用函数组件来实现。
最后修改 UserListContainer 组件:
import UserList from './UserList'
import UserDetail from './UserDetail'
class UserListContainer extends React.Component {
constructor(props) {
super(props);
this.state = {
users: [],
currentUserId: null
}
this.handleAddUser = this.handleAddUser.bind(this);
this.handleSetCurrentUser = this.handleSetCurrentUser.bind(this);
}
componentDidMount() {
var that = this;
fetch('/path/to/user-api').then(function (response) {
response.json().then(function (data) {
that.setState({users: data})
});
});
}
// 新增用户
handleAddUser(user) {
var that = this;
fetch('/path/to/save-user-api', {
method: 'POST',
body: JOSN.stringify({'username': user}).then(function (response) {
response.json().then(function (newUser) {
that.setState((preState) => ({users: preState.users.concat([newUser])}))
});
})
});
}
// 设置当前选中的用户
handleSetCurrentUser(userId) {
this.setState({
currentUserId: userId
});
}
render() {
//根据currentUserId,刷选出当前用户对象
const filterUsers = this.state.users.filter((user) => {user.id === this.state.currentUserId});
const currentUser = filterUsers.length > 0 ? filterUsers[0] : null;
return (
<UserList users={this.state.users}
currentUserId = {this.state.currentUserId}
onAddUser = {this.handleAddUser}
onSetCurrentUser = {this.handleSetCurrentUser}
/>
<UserDetail currentUser = {currentUser} />
)
}
}
UserListContainer 新增状态 currentUserId 用来标识当前选中的用户,这个状态正是 UserList 和 UserDetail 两个组件都要用到的状态,通过状态提升保存到它们共同的父组件 UserListContainer 中。同时,UserListContainer 通过 UserList 的 props 将修改 currentUserId 的回调函数传递给 UserList,使 UserList 可以在自身内部修改 currentUserId。
Context
当组件所处层级太深时,往往需要经过很多层的 props 传递才能将所需的数据或者回调函数传递给使用组件。这时,以 props 作为桥梁的组件通信方式便会显得很烦琐。例如,我们把 UserList 中新增用户的工作单独拆分到一个新的组件 UserAdd 中:
class UserAdd extends React.Component {
constructor(props) {
super(props);
this.state = {
newUser: ''
};
this.handleChange = this.handleChange.bind(this);
this.handleClick = this.handleClick.bind(this);
}
handleChange(e) {
this.setState({newUser: e.target.value});
}
// 通过 props 调用父组件的方法新增用户
handleClick() {
if(this.state.newUser && this.state.newUser.length > 0) {
this.props.onAddUser(this.state.newUser);
}
}
render() {
return (
<div>
<input onChange={this.handleChange} value={this.state.newUser} />
<button onClick={this.handleClick}>新增</button>
</div>
)
}
}
同时,修改原有的 UserList 组件:
import UserAdd from './UserAdd'
class UserList extends React.Component {
// 通过 props 调用父组件的方法,设置当前用户
handleUserClick(userId) {
this.props.onSetCurrentUser(userId);
}
render() {
return (
<div>
<ul className="user-list">
{this.props.users.map((user) => {
return (
<li key={user.id}
className={(this.props.currentUserId === user.id) ? 'current': ''}
onClick = {this.handleUserClick.bind(this, user.id)}
>
<span>{user.name}</span>
</li>
)
})}
</ul>
{/* 传递 UserListContainer 的 handleAddUser 方法 */}
<UserAdd onAddUser={this.props.onAddUser} />
</div>
)
}
}
可以发现,UserListContainer 中处理添加用户的函数 handleAddUser 经过 UserList 和 UserAdd 两个层级的 props 传递才到达 UserAdd 组件中。当应用更加复杂时,组件的层级会更多,组件通信就需要经过更多层级的传递,组件通信会变得非常麻烦。幸好,React 提供了一个 context 上下文,让任意层级的子组件都可以获取父组件中的状态和方法。创建 context 的方式是:在提供 context 的组件内新增一个 getChildContext 方法,返回 context 对象,然后在组件的 childContextTypes 属性上定义 context 对象的属性的类型信息。UserListContainer 是提供 context 的组件,改写如下:
class UserListContainer extends React.Component {
/** 省略其余代码 **/
// 创建 context 对象,包含 onAddUser 方法
getChildContext() {
return {onAddUser: this.handleAddUser};
}
// 新增用户
handleAddUser(user) {
this.setState((preState) => ({users: preState.users.concat([{'id': 'c', 'name': 'cc'}])}))
}
render() {
const filterUsers = this.state.users.filter((user) => {user.id = this.state.currentUserId});
const currentUser = filterUsers.length > 0 ? filterUsers[0] : null;
return (
<UserList users={this.state.users}
currentUserId = {this.state.currentUserId}
onSetCurrentUser = {this.handleSetCurrentUser}
/>
<UserDetail currentUser={currentUser} />
)
}
}
// 声明 context 的属性的类型信息
UserListContainer.childContextTypes = {
onAddUser: PropTypes.func
};
UserListContainer 通过增加 getChildContext 和 childContextTypes 将 onAddUser 在组件树中自动向下传递,当任意层级的子组件需要使用时,只需要在该组件的 contextTypes 中声明使用的 context 属性即可。例如,UserAdd 需要使用 context 中的 onAddUser,代码如下:
class UserAdd extends React.Component {
/** 省略其余代码 **/
handleChange(e) {
this.setState({newUser: e.target.value});
}
handleClick() {
if(this.state.newUser && this.state.newUser.length > 0) {
this.context.onAddUser(this.state.newUser);
}
}
render() {
return (
<div>
<input onChange={this.handleChange} value={this.state.newUser}/>
<button onClick={this.handleClick}>Add</button>
</div>
)
}
}
// 声明要使用的 context 对象的属性
UserAdd.contextType = {
onAddUser: propTypes.func
};
增加 contextTypes 后,在 UserAdd 内部就可以通过 this.context.onAddUser 的方式访问 context 中的 onAddUser 方法。注意,这里的示例传递的是组件的方法,组件中的任意数据也可以通过 context 自动向下传递。另外,当 context 中包含数据时,如果要修改 context 中的数据,一定不能直接修改,而是要通过 setState 修改,组件 state 的变化会创建一个新的 context,然后重新传递给子组件。
虽然 context 给组件通信带来了便利,但过多使用 context 会让应用中的数据流变得混乱,而且 context 是一个实验性的 API,在未来的 React 版本中是可能被修改或者废弃的。所以,使用 context 一定要慎重。