likes
comments
collection
share

React refs实例:正确使用子组件的功能

作者站长头像
站长
· 阅读数 22

最近在抽离一个组件时,需要暴露一些方法给父组件使用,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>
    );
}

这样的写法显然有缺陷:

  1. 组件未挂载之前调用 .show() 将会报错
  2. 仅限挂载一次

所以当时召集小伙伴们进行 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…