React组件通信方法

React组件间通信方式总结

  • 父组件 => 子组件:
    1、Props
    2、Instance Methods
  • 子组件 => 父组件:
    3、Callback Functions
    4、Event Bubbling
  • 兄弟组件:
    5、Parent Component
  • 不相关组件:
    6、Context
    7、Component Composition
    8、Portals
    9、Observer Pattern
    10、Redux等

对于React,我也是刚刚开始学习,所以总结中难免会出现错误,欢迎指出问题,大家共同交流讨论!


1、Props

父组件通过props将数据传给子组件:

const Child = ({ name }) => {
	<div>{ name }</div>
}

class Parent extends React.Component {
	constructor(props) {
	super(props);
	this.state = {
		name: 'zhangsan'
	}
}
render() {
	return (
		<Child name={this.state.name}/>
	)}
}


// 改为函数组件的写法
function Parent(props) {
	const [name, setName] = useState('zhangsan');
	return (
		<Child name={name}/>
	)
}

2、Instance Methods - 实例方法

原理是:父组件通过使用refs来直接调用子组件实例的方法
该方法最常见的一种使用场景:比如子组件是一个modal弹窗组件,子组件中有显示/隐藏这个modal弹窗的各种方法,我们可以通过使用这个方法,直接在父组件上调用子组件实例的这些方法来操控子组件的显示/隐藏。这种方法比传递一个控制modal显示/隐藏的props给子组件要美观多了。

class Modal extends React.Component {
	show = () => {//codes to show the modal};
	hide = () => {//codes to hide the modal};
	render() {
		return <div>I'm a modal.</div>
	}
}
class Parent extends React.Component {
	componentDidMount() {
		if(//some condition) {
			this.modal.show()
		}
	}
	render() {
		return(
			<Modal ref={el => {this.modal = el}}/>
		)
	}
}

3、Callback Functions

子组件通过调用父组件传来的回调函数,从而将数据传给父组件。

const Child = ({ onClick }) => {
	<div onClick={() => onClick('zhangsan')}>Click me</div>
}

class Parent extends React.Component {
	handleClick = (data) => {
		console.log("Parent received value from child:" + data)
	}
	render() {
		return (
			<Child onClick={this.handleClick}/>
		)
	}
}

4、Event Bubbling - 事件冒泡

利用事件冒泡机制,可以在父组件的元素上接收到子组件元素的点击事件。

function Parent() {
	const handleClick = useCallback(() => console.log('click'),[])
	return (
		<div onClick={handleClick}>
		<Child />
		</div>
	)
}
function Child() {
	return <button>Click</button>
}

5、Parent Component - 利用父组件通信

将父组件作为中间层来实现数据互通。

function Parent() {
	const [count, setCount] = useState(0);
	const handleClick = useCallback(() => setCount(count + 1) ,[count]);
	return (
		<div>
			<SiblingA count={count}/>
			<SiblingB onClick={handleClick}/>
		</div>
	)
}

6、Context

通常一个项目中,会存在一些全局性质的数据,很多组件都会用到,比如当前登陆的用户信息、ui主题、用户选择的语言等。当组件层级很深时,通过props传递就有些麻烦,此时可以使用context。
比如:下面的例子中,为了让Button元素拿到主题色,我们必须将theme作为props,从App传到Toolbar,再从Toolbar传到ThemeButton,最后Button从父组件ThemeButton的props里拿到theme,很麻烦。

function App() {
	return <Toolbar theme="dark"/>
}
function Toolbar(props) {
	return (
		<div>
			<ThemeButton theme={props.theme}/>
		</div>
	)
}
function ThemeButton(props) {
	return <Button theme={props.theme}/>
}

使用Context进行改写

const ThemeContext = React.createContext('light')
function App() {
	return (
		<ThemeContext.Provider value="dark">
			<Toolbar>
		</ThemeContext.Provider>
	)
}
function Toolbar() {
	return (
		<div>
			<ThemeButton/>
		</div>
	)
}
function ThemeButton() {
	return (
		<ThemeContext.Consumer>
			{value => <Button theme={value}}/>
		</ThemeContext.Consumer>
	)
}
  • 先用React.createContext创建一个Context对象,假如某个组件订阅了这个对象,当react渲染该组件时,会从离该组件最近的一个Provider组件中读取当前context值。
  • Context.Provider:每个Context对象都有一个Provider属性,该属性是一个React组件。在Provider组件内的所有组件都可以通过它订阅context值的变动。具体来说就是Provider组件有一个叫value的prop传递给所有内部组件,每当value值发生变化的时候,Provider内部的组件都会根据新value重新渲染。
  • 内部组件用Context.Consumer组件来使用context对象中的东西,该组件接受一个函数作为自己的child,函数的入参就是context的value,并返回一个React组件。
  • 如果内部组件是一个类组件,可以将Context对象赋值给这个类的属性contextType,如下:
class ThemeButton extends React.Component {
	const contextType = ThemeContext;
	render() {
		return (
			<Button theme={this.context}/>
		)
	}
}

总结:context对于解决react组件层级很深的props传递很有效,但也不应该被滥用。只有像theme、language等这种全局属性,才考虑context。因为这样会使得组件的复用性变差。
如果只是单纯为了解决层级很深的props传递,可以直接用component composition。

7、Component Composition - 组合组件

比如Page组件层层向下传递user和avatarSize属性,从而让深度嵌套的Link和Avatar组件读取到这些属性。

<Page user={user} avatarSize={avatarSize} />
// ... 渲染出 ...
<PageLayout user={user} avatarSize={avatarSize} />
// ... 渲染出 ...
<NavigationBar user={user} avatarSize={avatarSize} />
// ... 渲染出 ...
<Link href={user.permalink}>
  <Avatar user={user} size={avatarSize} />
</Link>

如果最后只有Avatar组件真的需要user和avatarSize,那么层层传递所使用到的两个props就显得非常冗余。而且一旦Avatar组件需要更多来自顶层组件的props,还需要在中间层一个一个加上去,很麻烦。
一种无需使用context的解决方法是将Avatar组件自身传递下去,因为中间组件无需知道user或者avatarSize等props:

function Page(props) {
	const user = props.user;
	const userLink = (
		<Link href={user.permalink}>
		<Avatar user={user} size={props.avatarSize}>
		</Link>
	);
	return <PageLayout userLink={userLink}/>;
}

// 现在,我们有这样的组件:
<Page user={user} avatarSize={avatarSize} />
// ... 渲染出 ...
<PageLayout userLink={...} />
// ... 渲染出 ...
<NavigationBar userLink={...} />
// ... 渲染出 ...
{props.userLink}

这样,只有最顶层的Page组件需要知道Link和Avatar组件是如何使用user和avatarSize的

  • 这种对组件的控制反转减少了应用中需要传递的props的数量,很多场景下会使得代码更干净,对根组件有更多的把控。
  • 但是,这种做法将逻辑提升到组件树到更高层级来处理,会使得这些高层组件更加复杂,且会强行将底层组件适应这样的形式。

有时候可能需要传递多个子组件,甚至会为这些子组件children封装多个单独的接口slots:

function Page(props) {
	const user = props.user;
	const content = <Feed user={user}/>;
	const topBar = (
		<NavigationBar>
	      <Link href={user.permalink}>
	        <Avatar user={user} size={props.avatarSize} />
	      </Link>
	    </NavigationBar>
	);
	return (
		<PageLayout
			content={content}
			topBar={topBar}
		/>
	);
}

8、Portals

Portals是react提供的新特性,虽然不是用来解决组件通信的,但也涉及到了组件通信的问题。
其主要应用场景为:两个组件在react项目中是父子组件的关系,但在HTML DOM中我们不想让它们成为父子元素的关系
比如,有一个父组件Parent,里面包含一个子组件Tooltip,虽然在react层级上它们是父子关系,但是我们希望子组件Tooltip渲染的元素在DOM中直接挂载到body节点里,而不是挂载到父组件的元素里。这样可以避免父组件的一些样式(如overflow: hidden、z-index、position等)导致子组件无法渲染成我们想要的样式。
首先,修改html文件,给portals增加一个节点:

<html>
	<body>
		<div id="react-root"></div>
		<div id="portal-root"></div>
	</body>
</html>

然后创建一个可以复用的portal容器:

import { useEffect } from 'react';
import { createPortal } from 'react-dom';

const Portal = ({children}) => {
const mount = document.getElementById("portal-root");
const el = document.getElementById("div");

useEffect(() => {
mount.appendChild(el);
return () => mount.removeChild(el);
}, [el, mount]);

return createPortal(children, el)
};
export default Portal;

最后在父组件中使用portal容器组件,并将Tooltip作为children传给portal容器组件:

const Parent = () => {
	const [coords, setCoords] = useState({});
	return (
		<div style={{overflow: "hidden"}}>
			<Button>Hover me</Button>
			<Portal>
				<Tooltip coords={coords}>
					Awesome content that is never cut off by its parent container!
				</Tooltip>
			</Portal>
		</div>
	)
}

这样,虽然父组件是overflow: hidden,但Tooltip不会被截断,它渲染到body节点下的<div id="portal-root"></div>中去了。
使用场景一般有:Tooltip、Modal、Popup、Dropdown等

9、Observer Pattern - 观察者模式

两个不相干组件需要通信时,可以使用观察者模式,其中一个组件负责订阅某个消息,而另一个组件负责发送消息。js提供了现成的API来发送自定义事件:CustomEvent,可以直接利用。
首先,在componentA中,接受这个自定义事件:

function ComponentA() {
	useEffect(() => {
		document.addEventListener('myEvent', handleEvent);
		return () => document.removeEventListener('myEvent', handleEvent);
	}, []);
	handleEvent = useCallback(e => console.log(e.detail.log), []);
}

然后,在componentB中,在合适的时候发送自定义事件:

function ComponentB() {
	sendEvent = useCallback(() => {
		document.dispatchEvent(new CustomEvent('myEvent', {
		detail: {
			log: "I'm zhangsan"
		}
	}))
}, []);
return <button onClick={sendEvent}>Send</button>
}

这样我们就用观察者模式实现了两个不相关组件之间的通信。当然现在的实现有个小问题,我们的事件都绑定在了document上,这样实现起来方便,但很容易导致一些冲突的出现,所以我们可以小小的改良下,独立一个小模块EventBus专门这件事:

class EventBus {
    constructor() {
        this.bus = document.createElement('fakeelement');
    }

    addEventListener(event, callback) {
        this.bus.addEventListener(event, callback);
    }

    removeEventListener(event, callback) {
        this.bus.removeEventListener(event, callback);
    }

    dispatchEvent(event, detail = {}){
        this.bus.dispatchEvent(new CustomEvent(event, { detail }));
    }
}

export default new EventBus

然后我们就可以愉快的使用它了,这样就避免了把所有事件都绑定在document上的问题:

import EventBus from './EventBus'
class ComponentA extends React.Component {
    componentDidMount() {
        EventBus.addEventListener('myEvent', this.handleEvent)
    }
    componentWillUnmount() {
        EventBus.removeEventListener('myEvent', this.handleEvent)
    }
    
    handleEvent = (e) => {
        console.log(e.detail.log)  //i'm zach
    }
}
class ComponentB extends React.Component {
    sendEvent = () => {
        EventBus.dispatchEvent('myEvent', {log: "i'm zach"}))
    }
    
    render() {
        return <button onClick={this.sendEvent}>Send</button>
    }
}

最后我们也可以不依赖浏览器提供的api,手动实现一个观察者模式,或者叫pub/sub,或者就叫EventBus。

function EventBus() {
  const subscriptions = {};
  this.subscribe = (eventType, callback) => {
    const id = Symbol('id');
    if (!subscriptions[eventType]) subscriptions[eventType] = {};
    subscriptions[eventType][id] = callback;
    return {
      unsubscribe: function unsubscribe() {
        delete subscriptions[eventType][id];
        if (Object.getOwnPropertySymbols(subscriptions[eventType]).length === 0) {
          delete subscriptions[eventType];
        }
      },
    };
  };

  this.publish = (eventType, arg) => {
    if (!subscriptions[eventType]) return;

    Object.getOwnPropertySymbols(subscriptions[eventType])
      .forEach(key => subscriptions[eventType][key](arg));
  };
}
export default EventBus;

10、Redux
一般是项目较大时,才考虑使用Redux这种状态管理库。


对于React,我也是刚刚开始学习,所以总结中难免会出现错误,欢迎指出问题,大家共同交流讨论!

参考原文档:https://segmentfault.com/a/1190000023585646

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
React组件通信可以通过props属性进行传递数据。子组件可以通过props接收父组件传递的数据,并进行相应的操作。在函数式组件中,可以直接通过props对象来获取父组件传递的数据。在类组件中,需要使用this.props来获取父组件传递的数据。\[1\] 另外,子组件也可以通过调用父组件传递过来的函数来实现子传父的通信。子组件可以将需要传递的数据作为函数的实参传入,并在子组件内部调用该函数。这样就可以将子组件的数据传递给父组件进行处理。\[3\] 总结起来,React组件通信可以通过props属性进行父传子和子传父的数据传递和操作。父组件通过props将数据传递给子组件,子组件通过props接收父组件传递的数据。子组件可以通过调用父组件传递的函数来将子组件的数据传递给父组件进行处理。\[1\]\[3\] #### 引用[.reference_title] - *1* *3* [React组件通信](https://blog.csdn.net/weixin_52148548/article/details/125287315)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [react组件通信](https://blog.csdn.net/weixin_39818813/article/details/125737028)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值