对于React,我也是刚刚开始学习,所以总结中难免会出现错误,欢迎指出问题,大家共同交流讨论!
父组件通过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}/>
)
}
原理是:父组件通过使用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}}/>
)
}
}
子组件通过调用父组件传来的回调函数,从而将数据传给父组件。
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}/>
)
}
}
利用事件冒泡机制,可以在父组件的元素上接收到子组件元素的点击事件。
function Parent() {
const handleClick = useCallback(() => console.log('click'),[])
return (
<div onClick={handleClick}>
<Child />
</div>
)
}
function Child() {
return <button>Click</button>
}
将父组件作为中间层来实现数据互通。
function Parent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => setCount(count + 1) ,[count]);
return (
<div>
<SiblingA count={count}/>
<SiblingB onClick={handleClick}/>
</div>
)
}
通常一个项目中,会存在一些全局性质的数据,很多组件都会用到,比如当前登陆的用户信息、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>
)
}
class ThemeButton extends React.Component {
const contextType = ThemeContext;
render() {
return (
<Button theme={this.context}/>
)
}
}
总结:context对于解决react组件层级很深的props传递很有效,但也不应该被滥用。只有像theme、language等这种全局属性,才考虑context。因为这样会使得组件的复用性变差。
如果只是单纯为了解决层级很深的props传递,可以直接用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的。
有时候可能需要传递多个子组件,甚至会为这些子组件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}
/>
);
}
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节点下的中去了。
使用场景一般有:Tooltip、Modal、Popup、Dropdown等
。
两个不相干组件需要通信时,可以使用观察者模式,其中一个组件负责订阅某个消息,而另一个组件负责发送消息。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