React forwardRef相关总结

React forwardRef相关总结

一句话概括:React 使用 forwardRef 完成 ref 的转发。

一、React forwardRef 简介

没有使用forwardRef时,父组件传入子组件ref属性,此时ref指向的是子组件本身。如下所示:

class Child extends React.Component {
  render() {
    return (
      <div>
        <button>点我</button>
      </div>
    );
  }
}

function App() {
  const child = useRef<any>();

  useEffect(() => {
    setTimeout(() => {
      console.log(child);
    }, 2000);
  }, []);

  return (
    <div styleName="container">
      <Child ref={child} />
    </div>
  );
}

此时控制台的截图如下所示:

在这里插入图片描述

可是如果我想让child指向的是Childbutton呢?此时只能拓展Childprops,新增一个字段如buttonRef,如下所示:

interface IProps {
  buttonRef: any;
}

class Child extends React.Component<IProps> {
  render() {
    return (
      <div>
        <button ref={this.props.buttonRef}>点我</button>
      </div>
    );
  }
}

function App() {
  const child = useRef<any>();

  useEffect(() => {
    setTimeout(() => {
      console.log(child);
    }, 2000);
  }, []);

  return (
    <div styleName="container">
      <Child buttonRef={child} />
    </div>
  );
}

此时child指向的是button这一dom对象。

可以看到在该场景中存在的问题:子组件需要增加 buttonRef 字段,父组件需要承担相应的心智负担。

所以 React 提供了 forwardRef,用于将 ref 转发。这样子组件在提供内部的 dom 时,不用扩充额外的 ref 字段

修改后代码如下所示:

const Child = forwardRef((props: any, ref: any) => {
  return (
    <div>
      <button ref={ref}>点我</button>
    </div>
  );
});
function App() {
  const child = useRef<any>();

  useEffect(() => {
    setTimeout(() => {
      console.log(child);
    }, 2000);
  }, []);

  return (
    <div styleName="container">
      <Child ref={child} />
    </div>
  );
}

修改后 App 组件不用承担额外的心智负担,还是像之前那样使用 ref 即可。

二、高阶组件中使用 forwardRef 转发组件 Ref

该例子为官方文档中的例子,阅读过的读者可直接跳到三。

现有一 Hoc 为打印组件 props:

function logProps(WrappedComponent) {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('old props:', prevProps);
      console.log('new props:', this.props);
    }

    render() {
      return <WrappedComponent {...this.props} />;
    }
  }

  return LogProps;
}

当其作用于子组件时,因为中间隔了一层,ref 就会传递失败,最终导致父组件调用ref.current.xxx()时失败:

import Button from './Button';
const LoggedButton = logProps(Button);

const ref = React.createRef();

// LoggedButton 组件是高阶组件(HOC)LogProps。
// 尽管渲染结果将是一样的,
// 但我们的 ref 将指向 LogProps 而不是内部的 Button 组件!
// 这意味着我们不能调用例如 ref.current.xxx() 这样的方法
<LoggedButton label="Click Me" handleClick={handleClick} ref={ref} />;

此时我们就需要在 hoc 中用 forwardRef 再包一层,转发 ref:

function logProps(WrappedComponent) {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('old props:', prevProps);
      console.log('new props:', this.props);
    }

    render() {
      const { componentRef, ...rest } = this.props;
      return <WrappedComponent {...rest} ref={this.props.componentRef} />;
    }
  }

  return forwardRef((props, ref) => {
    return <LogProps {...props} componentRef={ref} />;
  });
}

三、拓展:使用forwardRef和useImperativeHandle限制父组件调用子组件的Api

假设现有一开发人员开发了一个控制某状态切换的按钮组件Button.tsx,组件代码如下所示:

import React from 'react';

export enum Status {
  On = 'on',
  Off = 'off',
}

interface IProps {
  onChange: (status: Status) => void;
}

interface IState {
  status: Status;
}

class Button extends React.Component<IProps, IState> {
  state = {
    status: Status.Off,
  };

  private logStatus = () => {
    console.log(this.state.status);
  };

  onToggleStatus = () => {
    this.setState(
      (prevState) => ({
        status: prevState.status === Status.Off ? Status.On : Status.Off,
      }),
      () => {
        this.logStatus();
        this.props.onChange(this.state.status);
      }
    );
  };

  render() {
    return (
      <div>
        <button onClick={this.onToggleStatus}>status: {this.state.status}</button>
      </div>
    );
  }
}

export default Button;

演示效果如下所示,每次点击切换内部状态:

在这里插入图片描述

Button组件提供了onChange回调,外部组件可以传入onChange方法获取实时的status,Button内部则通过onToggleStatus控制状态

如果现在另一个开发人员开发外部组件时,想要实现在外部实现第二个按钮实时控制和同步显示Button的状态。此时他已经可以通过onChange实时同步状态,而从外部修改Button状态则一般有两种方式:

  1. 修改Button组件为纯函数组件,将其状态和修改状态的方法提升至父组件或者状态管理工具中。
  2. 通过ref拿到该组件,通过ref.current.onToggleStatus()的方式修改子组件状态。

这两种方式各有利弊,使用者需要根据实际场景自行选择。下面我将针对第二种方案,讨论如何更好的使用。

首先,先列出父组件使用ref的代码:

function App() {
  const [buttonStatus, setButtonStatus] = useState<Status>();
  const ref = useRef<any>();

  const onClick = useCallback(() => {
    // 更新内部状态
    ref.current.onToggleStatus();
    // 更新外部状态
    setButtonStatus(() => ref.current.state.status);
  }, []);

  // 首次渲染时同步状态
  useEffect(() => {
    setButtonStatus(() => ref.current.state.status);
  }, []);
  return (
    <div styleName="container">
      <Button ref={ref} onChange={setButtonStatus} />
      <button onClick={onClick}>控制和显示👆的按钮状态:{buttonStatus}</button>
    </div>
  );
}

父组件通过ref.current.state.status读取Button状态,通过onChange钩子实时同步状态,最终实现了下图的效果:

在这里插入图片描述

到这里,实际上功能已经开发完毕了。但是,本着探索的原则,我们回顾一下父组件的代码和调用方式,会发现这种方式存在以下几个问题:

  1. 子组件Button的接口完全被暴露在外,如果子组件是一个多组件复用的、状态集成到状态管理工具中的组件,父组件随意调用是很有可能造成风险的。
  2. 子组件Button的开发者在实际的开发中,并没有设计供外部主动调用的接口的开发意识。将Button组件当做糕点师的话,onChange是响应式的api风格,是我做多少糕点你吃多少,我什么时候做,你什么时候吃;而供外部主动调用的接口则是命令式的api风格,更像是外部主动点餐,你点多少我做多少,你什么时候点,我什么时候做给你吃。

面对以上问题,我们可以采用如下的优化方式:使用forwardRef加上useImperativeHandle封装一个HOC,动态地补充供外部主动调用的接口,让Button组件无感知。

代码如下:

import React, { useImperativeHandle, useRef } from 'react';

function buttonDecorator(Component: any) {
  const WrappedComponent: React.FC<any> = (props) => {
    const childRef = useRef<any>();
    const { parentRef, ...rest } = props;

    // 封装供外部主动调用的接口
    useImperativeHandle(parentRef, () => ({
      toggleStatus: () => {
        childRef.current.onToggleStatus();
      },
      getStatus: () => {
        return childRef?.current?.state?.status;
      },
    }));

    return <Component {...rest} ref={childRef} />;
  };

  return React.forwardRef<any, any>((props, ref) => {
    return <WrappedComponent {...props} parentRef={ref} />;
  });
}

export default buttonDecorator;

可以看到hoc包裹Button后仅提供toggleStatusgetStatus方法。该hoc通过useImperativeHandle给被转发的parentRef注入公共api,api中通过childRef实现对子组件的调用。所以父组件此时调用ref.current.state访问状态,就会报错:

在这里插入图片描述

对于上述的问题一(多组件复用可能造成副作用),开发者可以自行使用useImperativeHandle定义api,控制对状态管理工具的副作用范围,对于问题二(组件内部的api更偏向于onXX的响应式风格,外部调用则更偏向于命令式的风格),开发者仅仅二次命名api,就可以让api从外部看起来很命令式,如用toggleStatus替代onToggleStatus。父组件对应需要修改:

  const onClick = useCallback(() => {
    // 更新内部状态
    ref.current.toggleStatus();
    // 更新外部状态
    setButtonStatus(() => ref.current.getStatus());
  }, []);

  // 首次渲染时同步状态
  useEffect(() => {
    setButtonStatus(() => ref.current.getStatus());
  }, []);

四、总结

本文主要是介绍了forwardRef的意义、forwardRef在hoc中是如何转发Ref的、forwardRef的一种实际业务场景。关于forwardRef的更多使用场景需要读者自行去探索,希望本文能给读者一些启发。

  • 8
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值