React refs实例:正确使用子组件的功能
最近在抽离一个组件时,需要暴露一些方法给父组件使用,google 百度了一番无果,最后想到了 refs。可不太熟悉,于是又去查了遍官方文档,发现以前工作的时候写的一个全局弹窗的组件很傻,遂记录一下。
当时的业务需求:一个全局的支付弹窗,在不同的页面点击支付后打开弹窗。
写过的“笨”方法
最理想的使用方式就是 PayModal.show()
后直接展示。但是基于 react 的特性,一般做法都是传一个 visible
props 上去,可组件挂载在全局 Layout 下面,没法儿在当前页面下控制。
于是乎:
function PayModal(props) {
const [visible, setVisible] = useState(false);
/**
* 直接在组件上赋值。反正只挂载一次。。。
*/
PayModal.show = () => {
setVisible(true);
};
return (
<Modal visible={visible}>
// ...
</Modal>
);
}
这样的写法显然有缺陷:
- 组件未挂载之前调用
.show()
将会报错 - 仅限挂载一次
所以当时召集小伙伴们进行 code review 的时候就被“教怎么做了”,xxxxx。还有人提出通过 props 来暴露,大意是这样:
function PayModal(props) {
useEffect(() => {
props.fns = {
show: () => {
setVisible(true);
},
};
}, []);
}
// 使用时
class Page1 extends React.Component {
constructor() {
this.fns = {};
}
render() {
return (
<div>
<PayModal fns={this.fns} />
</div>
);
}
}
这样的做法也可,但违背了 react 中自上而下数据的传递。你不知道哪个地方子组件修改你的数据,容易踩西瓜皮。
当时新同学推荐的做法是 ref + useImperativeHandle
👍
关于 Refs
官方文档上介绍地很清楚,也翻到过几遍,但是有时还是不太会用它。没有业务场景,就有时很难理解某些概念,为什么要这样?当理解学会后,就会想当然地使用,感觉还是业务驱动技术啊。
所以 ref 是个什么东西?以前看文档关注的重心,就知道到它能获取 dom,基本上替代了旧版 findDOMNode()
的用法:
class Foo extends React.Component {
constructor() {
this.inputRef = React.createRef();
}
componentDidMount() {
this.inputRef.current; // dom 节点
this.inputRef.current.focus();
}
render() {
return (
<input ref={this.inputRef} />
);
}
}
事实上它有 3 种情况(文档):
- ref 写在 HTML 元素上,
.current
指向了该元素 dom 节点 - ref 写在 class 组件上,
.current
指向了该组件实例 - 无法在函数式组件上使用 ref,因为没有实例
此外 ref 其实也很简单,仅仅是一个纯对象,createRef()
也只是个工厂函数:
// packages/react/src/ReactCreateRef.js
// an immutable object with a single mutable value
export function createRef() {
const refObject = {
current: null,
};
if (__DEV__) {
Object.seal(refObject);
}
return refObject;
}
所以你甚至能这样写(不推荐):
class Foo extends React.Component {
constructor() {
this.inputRef = { current: null };
}
}
可能会有这样的疑问:为什么要用 this.inputRef.current
访问,直接 this.inputRef
访问不是更方便么? 这与 react 的性能优化有关,使用同一个对象(引用地址)能减少 react 的重复渲染。之后会写一篇优化相关的文章,具体介绍。
场景
回到场景,利用 ref 公开子组件上的方法,暴露给父组件使用。基于组件的写法分为 class 组件 与函数式。
class 组件暴露方法
基于 ref 的特性,无需额外操作即可访问子组件上的方法。例:
class Parent {
constructor() {
this.ref = React.createRef();
}
componentDidMount() {
this.ref.current.sayHello();
}
render() {
return <Child ref={this.ref} />;
}
}
// 子组件,实例化后上的 sayHello 可供调用
class Child {
sayHello = () => {
// ...
}
render() {}
}
函数式组件暴露方法
得益于 hooks 的推行,函数式组件可复用能力大大增强。以前给函数式组件写 ref 时还得定义一个额外的 props 属性 wrapComponentRef
等等,现在官方提供了 API 直接操作。
此外便是通过 useImperativeHandle
重写 ref 以暴露方法。例:
function Child(props, ref) {
useImperativeHandle(ref, () => {
// 重写。参考 antd 表单的做法
return {
sayHello: () => {
// ...
},
};
});
return <div />;
}
// 转发 ref
export default React.forwardRef(Child);
回顾:全局弹窗的解决方案
全局弹窗由于只挂载在整个应用的 Layout 下面,因此也没有办法通过 ref 来打开弹窗。。。
结合整个技术方案,数据流用的 redux,可以直接把 visible
放到全局的 store 里面,通过 action 触发:
class Page1 extends React.Component {
constructor(props){
super(props);
this.state = {};
}
handleClick = () => {
this.props.dispatch({
type: OPEN_PAYMODAL,
});
};
render() {}
}
export default connect()(Page1);
甚至还能用事件发布/订阅模式~~~有时的思维还是被什么最佳规范给局限住了。
结尾。被强行塞了一波知识 😫,zz...
原文链接(博客小站引流):ningtaostudy.cn/articles/bD…
转载自:https://juejin.cn/post/7043368793296338980