React 组件通信
create by db on 2020-9-2 13:30:46
Recently revised in 2020-9-20 13:30:51闲时要有吃紧的心思,忙时要有悠闲的趣味
前言
在项目开发中,组件通信总是不可避免的,下面我们就看下 React 中组件之间通信的方式都有哪些,主要分为三部分
-
父子组件通信:
-
跨级组件通信:
-
无关系组件通信:
正文
一、父子组件通信
React 数据流动是单向的, 父组件向子组件通信也是最常见的;
props
官方文档——组件 & Props | react 官网
父组件通过 props 向子组件传递数据及方法,子组件通过调用父组件方法向父组件传值。
其数据流如图所示:
拿我们之前写的ToDoList项目为例
我们将项目分为三个组件,分别为
-
父组件
ToDoList- 新增代办表单子组件
ItemForm- - 待办事项展示子组件
ItemList
- 新增代办表单子组件
我们看下其中ToDoList组件与子组件ItemList的数据交互。
ToDoList.js
// 引入React及useState
import React, { useState } from "react";
// 引入子组件
import ItemList from "../components/ItemList";
function ToDoList() {
// 初始化待办事项数组
const [items, setItems] = useState([
{
id: 1,
title: "吃饭",
},
{
id: 2,
title: "睡觉",
},
{
id: 3,
title: "打豆豆",
},
]);
// 完成待办
function deleteItem(index) {
items.splice(index, 1);
setItems([...items]);
}
return (
<div className="App">
<h1>To Do List</h1>
<ul>
{/* 将待办事项数组传入组件,并绑定删除方法 */}
<ItemList deleteItem={deleteItem} items={items}></ItemList>
</ul>
</div>
);
}
export default ToDoList;
ItemList.js
// 待办事项列表组件
function ItemList(props) {
// 父组件传入的方法及数据
const { deleteItem, items } = props;
// 点击完成,触发父组件方法
const clickDelete = index => {
deleteItem(index);
};
// 使用map遍历传入数组,返回渲染后的列表
const ItemLists = items.map((item, index) => (
// 在 map() 方法中的元素需要设置 key 属性。
<li key={item.id}>
{item.title}
<button onClick={() => clickDelete(index)}>完成</button>
</li>
));
return ItemLists;
}
export default ItemList;
二、跨级组件通信
跨级组件包括兄弟组件通信及爷孙组件
1. 组件层层传递
例如 A 组件和 B 组件是兄弟组件,他们之间要进行通信,
- 先找到
A和B公共的父组件C, A先向C组件通信C组件通过 props 和B组件通信
此时 父组件C 组件起的就是中间件的作用,但这样的缺点是层层传递比较麻烦。
2. context
官方文档——Context | react 官网
context 可以理解为一种全局的上下文变量。通过 context 我们可以跨级组件通信时不用在每一个组件使用 props 来传递值,直接在组件树之间来传递值和方法。只要是提供 context 组件的子组件都可以访问到 context 上的值。
一句话概括就是:跨级传值,状态共享。
但是 React 官方不建议使用大量 context, 尽管他可以减少逐层传递, 但是当组件结构复杂的时候, 我们并不知道 context 是从哪里传过来的; 由于在组件使用了 context,代码耦合度较高,不利于组件复用; 而且 context 是一个全局变量, 全局变量正是导致应用走向混乱的罪魁祸首. 其数据流如图所示:
简单使用 context
// 引入React及useState
import React, { useState, useContext } from "react";
// 声明Context
const Context = React.createContext();
// 祖先组件
// 使用 <Context.Provider></Context.Provider> 包裹起来并绑定数据
function Ancestor() {
const [state, setState] = useState("state");
return (
<Context.Provider value={{ state, setState }}>
<div>{state}</div>
<Son />
</Context.Provider>
);
}
// 儿子组件
// 仅作层级
function Son() {
return (
<div>
<Grandson />
</div>
);
}
// 孙子组件
// 使用 setState 更新数据
function Grandson() {
const { setState } = useContext(Context);
const onButtonClick = () => {
setState("孙子知道了");
};
return (
<div>
<button onClick={onButtonClick}>孙子请回答</button>
</div>
);
}
export default Ancestor;
当然为了方便,我们也可以封装一下: context.js
/*
组件状态context
state: 全局状态
dispatch: 事件触发器
getValue: 根据key获取state的函数
setValue: 根据key设置state的函数
*/
import React, { useReducer } from "react";
export const Context = React.createContext({});
export function ConfigContext({ children, initState }) {
const [state, dispatch] = useReducer(reducer, initState);
const getValue = key => state[key];
const setValue = (key, value) => {
return dispatch({ type: "SET_VALUE", key, value });
};
let ctx = {
state,
dispatch,
getValue,
setValue,
};
return <Context.Provider value={ctx}>{children}</Context.Provider>;
}
// 初始状态
function reducer(state, { type, ...payload }) {
switch (type) {
case "SET_VALUE": {
return {
...state,
[payload.key]: payload.value,
};
}
default: {
return state;
}
}
}
使用的時候在父组件直接引入就好
ToDoList.js
// 引入ConfigContext
import { ConfigContext } from "../components/contextAndreducer/context";
import ItemList from "../components/ToDoList2/ItemList";
import ItemForm from "../components/ToDoList2/ItemForm";
const items = [
{
id: 1,
title: "吃饭",
},
{
id: 2,
title: "睡觉",
},
{
id: 3,
title: "打豆豆",
},
];
function ToDoList() {
return (
// 使用ConfigContext 组件包裹需数据传递的子组件及初始化数据
<ConfigContext initState={{ items }}>
<ItemForm></ItemForm>
<ItemList></ItemList>
</ConfigContext>
);
}
export default ToDoList;
这样我们就不需要关心数据流向,只需要在子组件中更新相关数据就好了。
ItemForm.js
import React, { useState, useContext } from "react";
import { Context } from "../contextAndreducer/context";
// 待办事项列表组件
function ItemForm(props) {
// 声明组件中的数据
const {
state: { items = [], itemId = 4 },
setValue,
} = useContext(Context);
const [textContent, setTextContent] = useState("");
// 输入框内容更改,更新state
const handleChange = event => {
setTextContent(event.target.value);
};
// 点击提交按钮,保存选项,清空输入内容
const handleSubmit = event => {
event.preventDefault();
if (!textContent) {
return;
}
addNewItem(textContent);
setTextContent("");
};
// 新增待办
function addNewItem(data) {
let obj = {
id: itemId,
title: data,
};
items.push(obj);
// 更新组件数据
setValue("itemId", itemId + 1);
setValue("items", [...items]);
}
return (
<form onSubmit={handleSubmit}>
<label>待办事项:</label>
<input type="text" value={textContent} onChange={handleChange} />
<input type="submit" value="提交" />
</form>
);
}
export default ItemForm;
ItemList.js
// 待办事项列表组件
import { Context } from "../contextAndreducer/context";
import React, { useContext } from "react";
function ItemList() {
const {
state: { items = [] },
setValue,
} = useContext(Context);
// 点击完成,触发父组件方法
const clickDelete = index => {
items.splice(index, 1);
setValue("items", [...items]);
};
// 使用map遍历传入数组,返回渲染后的列表
const ItemLists = items.map((item, index) => (
// 在 map() 方法中的元素需要设置 key 属性。
<li key={item.id}>
{item.title}
<button onClick={() => clickDelete(index)}>完成</button>
</li>
));
return ItemLists;
}
export default ItemList;
三、无关系组件通信
当两个组件没有关系,他们互不嵌套,处在同个层级或者不同层级上。如果他们之间要进行通信,应该怎么办呢?
有以下几种常用方法
1. 浏览器缓存
包括缓存 sessionStorage、localStorage 等
(1) sessionStorage
传递参数:
sessionStorage.setItem("parameter_value", "我是参数");
获取参数:
let parameter = sessionStorage.getItem("parameter_value");
(2) localStorage
传递参数:
localStorage.setItem("parameter_value", "我是参数");
获取参数:
let parameter = localStorage.getItem("parameter_value");
ps
localStorage 与 sessionStorage的区别大家应该都清楚
-
localStorage 的生命周期是永久的,关闭页面或浏览器之后 localStorage 中的数据也不会消失。除非主动删除数据,否则数据永远不会消失。
-
sessionStorage 的生命周期是仅在当前会话下有效。sessionStorage 引入了一个“浏览器窗口”的概念,sessionStorage 是在同源的窗口中始终存在的数据。只要这个浏览器窗口没有关闭,即使刷新页面或者进入同源另一个页面,数据依然存在。但是 sessionStorage 在关闭了浏览器窗口后就会被销毁。同时独立的打开同一个窗口同一个页面,sessionStorage 也是不一样的。
2. 路由传值
如果两个组件之间存在跳转,可以使用路由跳转传值。在切换路由时,传参方式主要有 3 种:
- params
- query
- state
#### (1)params 参数
使用:
//路由链接(携带参数):
<Link to={{ pathname:`/b/child1/${"01"}/${"消息1"}` }}>Child1</Link>
//注册路由(声明接收):
<Route path="/b/child1/:id/:title" component={Test}/>
//接收参数:
import { useParams } from "react-router-dom";
const params = useParams();
//params参数 => {id: "01", title: "消息1"}
优势 : 刷新地址栏,参数依然存在
缺点:
- 只能传字符串
- 传值过多 url 会变得很长
- 参数必须在路由上配置
#### (2)search 参数
传递参数:
//路由链接(携带参数):
<Link className="nav" to={`/b/child2?age=20&name=zhangsan`}>Child2</Link>
//注册路由(无需声明,正常注册即可):
<Route path="/b/child2" component={Test}/>
//接收参数方法1:
import { useLocation } from "react-router-dom";
import qs from "query-string";
const { search } = useLocation();
//search参数 => {age: "20", name: "zhangsan"}
//接收参数方法2:
import { useSearchParams } from "react-router-dom";
const [searchParams, setSearchParams] = useSearchParams();
// console.log( searchParams.get("id")); // 12
优势 : 刷新地址栏,参数依然存在
缺点: 获取到的 search 是 urlencoded 编码字符串(例如: ?age=20&name=zhangsan),需要借助 query-string 解析参数成对象
#### (3)state 传参
使用:
//通过Link的state属性传递参数
<Link className="link" to={{ pathname: "/list2" }} state={{ id: "02", title: "消息2" }}>
列表2
</Link>
//注册路由(无需声明,正常注册即可):
<Route path="/b/child2" component={Test}/>
//接收参数:
import { useLocation } from "react-router-dom";
const { state } = useLocation();
//state参数 => {id: "02", name: "消息2"}
优势: 传参优雅,传递参数可传对象;刷新也可以保留住参数,因此推荐 state 方式。
3、event(发布--订阅)自定义事件
使用自定义事件机制,类似 vue 的eventBus事件
- 首先需要项目中安装 events 包:
npm install events --save - 在 src 下新建一个 util 目录里面建一个
events.js
import { EventEmitter } from "events";
export default new EventEmitter();
- 在 ToDoList1 中引入 emitter 并注册事件
import emitter from "../util/events";
// 声明一个自定义事件
emitter.addListener("changeMessage", message => {
console.log("自定义事件", message);
});
- 在 ToDoList2 中引入 emitter 并触发事件
import emitter from "../util/events";
// 触发一个自定义事件
emitter.emit("changeMessage", "触发自定义事件");
总结
- 父子组件通信:
- props
- 跨级组件通信:
- 层层组件传递 props
- context
- 无关系组件通信:
- 浏览器缓存
- 路由传值
- event(发布--订阅)自定义事件
在进行组件通信的时候,主要看业务的具体需求,选择最合适的; 当业务逻辑复杂到一定程度,就可以考虑引入Mobx, Redux 等状态管理工具
参考文档
- Context | react 官网
- React 中组件使用 context 通信的方式 | 掘金-cAuth
- React 中组件通信的几种方式 | 掘金-IOneStar
- React 中使用 react-router-dom 路由传参的三种方式详解【含 V5.x、V6.x】!!! | 掘金-JasonMa 丶
后记:Hello 小伙伴们,如果觉得本文还不错,记得点个赞或者给个 star,你们的赞和 star 是我编写更多更丰富文章的动力!GitHub 地址
文档协议
db 的文档库 由 db 采用 知识共享 署名-非商业性使用-相同方式共享 4.0 国际 许可协议进行许可。
基于github.com/danygitgit上的作品创作。
本许可协议授权之外的使用权限可以从 creativecommons.org/licenses/by… 处获得。