React 组件通信

420 阅读8分钟

React 组件通信

create by db on 2020-9-2 13:30:46
Recently revised in 2020-9-20 13:30:51

闲时要有吃紧的心思,忙时要有悠闲的趣味

目录

前言

返回目录

  在项目开发中,组件通信总是不可避免的,下面我们就看下 React 中组件之间通信的方式都有哪些,主要分为三部分

  1. 父子组件通信:

  2. 跨级组件通信:

  3. 无关系组件通信:

正文

一、父子组件通信

返回目录

  React 数据流动是单向的, 父组件向子组件通信也是最常见的;

props

官方文档——组件 & Props | react 官网

 父组件通过 props 向子组件传递数据及方法,子组件通过调用父组件方法向父组件传值。

其数据流如图所示: props.webp

 拿我们之前写的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 组件是兄弟组件,他们之间要进行通信,

  1. 先找到 AB 公共的父组件 C,
  2. A 先向 C 组件通信
  3. C 组件通过 props 和 B 组件通信

 此时 父组件C 组件起的就是中间件的作用,但这样的缺点是层层传递比较麻烦。

2. context

官方文档——Context | react 官网

 context 可以理解为一种全局的上下文变量。通过 context 我们可以跨级组件通信时不用在每一个组件使用 props 来传递值,直接在组件树之间来传递值和方法。只要是提供 context 组件的子组件都可以访问到 context 上的值。

 一句话概括就是:跨级传值,状态共享。

 但是 React 官方不建议使用大量 context, 尽管他可以减少逐层传递, 但是当组件结构复杂的时候, 我们并不知道 context 是从哪里传过来的; 由于在组件使用了 context,代码耦合度较高,不利于组件复用; 而且 context 是一个全局变量, 全局变量正是导致应用走向混乱的罪魁祸首. 其数据流如图所示:

context.webp

简单使用 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

localStoragesessionStorage的区别大家应该都清楚

  • 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事件

  1. 首先需要项目中安装 events 包: npm install events --save
  2. 在 src 下新建一个 util 目录里面建一个 events.js
import { EventEmitter } from "events";

export default new EventEmitter();
  1. 在 ToDoList1 中引入 emitter 并注册事件
import emitter from "../util/events";
// 声明一个自定义事件
emitter.addListener("changeMessage", message => {
  console.log("自定义事件", message);
});
  1. 在 ToDoList2 中引入 emitter 并触发事件
import emitter from "../util/events";
// 触发一个自定义事件
emitter.emit("changeMessage", "触发自定义事件");

总结

返回目录

  1. 父子组件通信:
  • props
  1. 跨级组件通信:
  • 层层组件传递 props
  • context
  1. 无关系组件通信:
  • 浏览器缓存
  • 路由传值
  • event(发布--订阅)自定义事件

 在进行组件通信的时候,主要看业务的具体需求,选择最合适的; 当业务逻辑复杂到一定程度,就可以考虑引入Mobx, Redux 等状态管理工具

参考文档

后记:Hello 小伙伴们,如果觉得本文还不错,记得点个赞或者给个 star,你们的赞和 star 是我编写更多更丰富文章的动力!GitHub 地址

文档协议


db 的文档库db 采用 知识共享 署名-非商业性使用-相同方式共享 4.0 国际 许可协议进行许可。
基于github.com/danygitgit上的作品创作。
本许可协议授权之外的使用权限可以从 creativecommons.org/licenses/by… 处获得。