React.forwardRef 实战代码示例

98 阅读5分钟

React.forwardRef 完整指南

📌 快速概览

React.forwardRef 是一个 React API,用于将 ref 从父组件转发到子组件的 DOM 元素

核心问题

在 React 中,props 不能直接传递 ref。下面的代码会 失效

// ❌ 不能这样用
const MyButton = (props) => {
  return <button ref={props.ref}>Click</button>; // 不工作
};

export default MyButton;

解决方案

使用 React.forwardRef 解决这个问题:

// ✅ 正确用法
const MyButton = React.forwardRef((props, ref) => {
  return <button ref={ref}>Click</button>; // 正常工作
});

export default MyButton;

🎯 使用场景

1. 管理焦点(最常见)

场景:需要从父组件控制子组件 input 的焦点

// ❌ 问题代码
const TextInput = (props) => {
  return <input />;
};

const Parent = () => {
  const inputRef = useRef(null);

  const handleFocus = () => {
    // ❌ 这不会工作,inputRef.current 是组件实例,不是 DOM
    inputRef.current.focus();
  };

  return (
    <>
      <TextInput ref={inputRef} />
      <button onClick={handleFocus}>Focus Input</button>
    </>
  );
};

✅ 解决方案

const TextInput = React.forwardRef((props, ref) => {
  return <input ref={ref} />;
});

const Parent = () => {
  const inputRef = useRef(null);

  const handleFocus = () => {
    // ✅ 现在可以正常工作
    inputRef.current.focus();
  };

  return (
    <>
      <TextInput ref={inputRef} />
      <button onClick={handleFocus}>Focus Input</button>
    </>
  );
};

2. 触发组件方法

场景:需要从父组件调用子组件的方法

// 可伸缩面板组件
const Collapse = React.forwardRef((props, ref) => {
  const contentRef = useRef(null);

  useImperativeHandle(ref, () => ({
    // 暴露给父组件的方法
    expand: () => {
      contentRef.current.style.height = "auto";
    },
    collapse: () => {
      contentRef.current.style.height = "0";
    },
  }));

  return (
    <div>
      <div ref={contentRef} style={{ height: 0, overflow: "hidden" }}>
        {props.children}
      </div>
    </div>
  );
});

const Parent = () => {
  const collapseRef = useRef(null);

  return (
    <div>
      <Collapse ref={collapseRef}>
        <p>This is collapsible content</p>
      </Collapse>
      <button onClick={() => collapseRef.current?.expand()}>Expand</button>
      <button onClick={() => collapseRef.current?.collapse()}>Collapse</button>
    </div>
  );
};

3. 获取 DOM 属性或调用 DOM 方法

场景:获取 input 的值、video 的当前时间等

const VideoPlayer = React.forwardRef((props, ref) => {
  return (
    <video ref={ref} controls>
      <source src={props.src} />
    </video>
  );
});

const PlayerController = () => {
  const videoRef = useRef(null);

  const handlePlay = () => videoRef.current?.play();
  const handlePause = () => videoRef.current?.pause();
  const handleSeek = (time) => {
    videoRef.current.currentTime = time;
  };

  return (
    <div>
      <VideoPlayer ref={videoRef} src="video.mp4" />
      <button onClick={handlePlay}>Play</button>
      <button onClick={handlePause}>Pause</button>
      <button onClick={() => handleSeek(30)}>Jump to 30s</button>
    </div>
  );
};

4. 组件库设计(最重要)

场景:创建可复用的 UI 组件库,允许用户访问底层 DOM

这正是 Button 组件的使用场景:

// Button 组件
interface ButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
  type?: 'primary' | 'secondary';
  loading?: boolean;
  disabled?: boolean;
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  (props, ref) => {
    const { type = 'primary', loading, disabled, ...rest } = props;

    return (
      <button
        ref={ref}
        className={`btn btn-${type}`}
        disabled={disabled || loading}
        {...rest}
      >
        {loading && <Spinner />}
        {props.children}
      </button>
    );
  }
);

// 使用
const App = () => {
  const buttonRef = useRef(null);

  const handleClick = () => {
    // 可以直接访问 button DOM 方法
    buttonRef.current?.blur();
    buttonRef.current?.focus();
  };

  return (
    <>
      <Button ref={buttonRef} type="primary">
        Submit
      </Button>
      <button onClick={handleClick}>Control Button</button>
    </>
  );
};

💡 forwardRef API 详解

基础语法

const MyComponent = React.forwardRef((props, ref) => {
  return <div ref={ref}>{props.children}</div>;
});

// 在 TypeScript 中
const MyComponent = React.forwardRef<HTMLDivElement, MyProps>(
  (props, ref) => {
    return <div ref={ref}>{props.children}</div>;
  }
);

参数说明

参数类型说明
propsobject组件的所有 props
refRef父组件传入的 ref

返回值

返回一个新的 React 组件,可以接收 ref prop。


📚 详细代码示例

示例 1:受控输入框

// CustomInput.tsx
import React, { forwardRef, useRef, useImperativeHandle } from 'react';

interface CustomInputProps {
  placeholder?: string;
  defaultValue?: string;
}

interface CustomInputHandle {
  focus: () => void;
  blur: () => void;
  clear: () => void;
  getValue: () => string;
  setValue: (value: string) => void;
}

const CustomInput = forwardRef<CustomInputHandle, CustomInputProps>(
  (props, ref) => {
    const inputRef = useRef<HTMLInputElement>(null);

    // 使用 useImperativeHandle 暴露自定义方法
    useImperativeHandle(ref, () => ({
      focus: () => inputRef.current?.focus(),
      blur: () => inputRef.current?.blur(),
      clear: () => {
        if (inputRef.current) {
          inputRef.current.value = '';
        }
      },
      getValue: () => inputRef.current?.value || '',
      setValue: (value: string) => {
        if (inputRef.current) {
          inputRef.current.value = value;
        }
      }
    }));

    return (
      <input
        ref={inputRef}
        placeholder={props.placeholder}
        defaultValue={props.defaultValue}
        style={{
          padding: '8px 12px',
          border: '1px solid #d9d9d9',
          borderRadius: '4px'
        }}
      />
    );
  }
);

CustomInput.displayName = 'CustomInput';
export default CustomInput;

// 使用方式
const App = () => {
  const inputRef = useRef<CustomInputHandle>(null);

  return (
    <div>
      <CustomInput ref={inputRef} placeholder="Enter text" />
      <button onClick={() => inputRef.current?.focus()}>Focus</button>
      <button onClick={() => inputRef.current?.clear()}>Clear</button>
      <button onClick={() => {
        const value = inputRef.current?.getValue();
        console.log('Value:', value);
      }}>Get Value</button>
      <button onClick={() => inputRef.current?.setValue('Hello')}>
        Set Value
      </button>
    </div>
  );
};

示例 2:时刻表格组件

// Table.tsx
import React, { forwardRef, useRef, useImperativeHandle } from 'react';

interface TableHandle {
  scrollToTop: () => void;
  scrollToBottom: () => void;
  scroll: (offset: number) => void;
  getScrollPosition: () => number;
}

interface TableProps {
  data: Array<any>;
  columns: Array<any>;
}

const Table = forwardRef<TableHandle, TableProps>(({ data, columns }, ref) => {
  const tableRef = useRef<HTMLDivElement>(null);

  useImperativeHandle(ref, () => ({
    scrollToTop: () => {
      if (tableRef.current) {
        tableRef.current.scrollTop = 0;
      }
    },
    scrollToBottom: () => {
      if (tableRef.current) {
        tableRef.current.scrollTop = tableRef.current.scrollHeight;
      }
    },
    scroll: (offset: number) => {
      if (tableRef.current) {
        tableRef.current.scrollTop += offset;
      }
    },
    getScrollPosition: () => {
      return tableRef.current?.scrollTop || 0;
    }
  }));

  return (
    <div
      ref={tableRef}
      style={{
        height: '300px',
        overflow: 'auto',
        border: '1px solid #ddd'
      }}
    >
      <table style={{ width: '100%' }}>
        <thead>
          <tr>
            {columns.map(col => (
              <th key={col.key}>{col.title}</th>
            ))}
          </tr>
        </thead>
        <tbody>
          {data.map((row, idx) => (
            <tr key={idx}>
              {columns.map(col => (
                <td key={col.key}>{row[col.key]}</td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
});

Table.displayName = 'Table';
export default Table;

// 使用
const App = () => {
  const tableRef = useRef<TableHandle>(null);

  return (
    <div>
      <Table
        ref={tableRef}
        columns={[
          { key: 'name', title: 'Name' },
          { key: 'age', title: 'Age' }
        ]}
        data={[
          { name: 'Alice', age: 25 },
          { name: 'Bob', age: 30 }
        ]}
      />
      <button onClick={() => tableRef.current?.scrollToTop()}>
        Scroll to Top
      </button>
      <button onClick={() => tableRef.current?.scrollToBottom()}>
        Scroll to Bottom
      </button>
    </div>
  );
};

示例 3:Modal 对话框

// Modal.tsx
import React, { forwardRef, useRef, useImperativeHandle, useState } from 'react';

interface ModalHandle {
  open: () => void;
  close: () => void;
  isOpen: () => boolean;
}

interface ModalProps {
  title: string;
  children: React.ReactNode;
}

const Modal = forwardRef<ModalHandle, ModalProps>(
  ({ title, children }, ref) => {
    const [isVisible, setIsVisible] = useState(false);

    useImperativeHandle(ref, () => ({
      open: () => setIsVisible(true),
      close: () => setIsVisible(false),
      isOpen: () => isVisible
    }));

    if (!isVisible) return null;

    return (
      <div style={{
        position: 'fixed',
        top: 0,
        left: 0,
        right: 0,
        bottom: 0,
        backgroundColor: 'rgba(0, 0, 0, 0.5)',
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center'
      }}>
        <div style={{
          backgroundColor: 'white',
          padding: '20px',
          borderRadius: '8px',
          minWidth: '400px'
        }}>
          <h2>{title}</h2>
          {children}
          <button onClick={() => setIsVisible(false)}>Close</button>
        </div>
      </div>
    );
  }
);

Modal.displayName = 'Modal';
export default Modal;

// 使用
const App = () => {
  const modalRef = useRef<ModalHandle>(null);

  return (
    <div>
      <button onClick={() => modalRef.current?.open()}>Open Modal</button>
      <Modal ref={modalRef} title="Confirm Action">
        <p>Are you sure?</p>
      </Modal>
    </div>
  );
};

🔑 关键要点

1. 必须搭配 useImperativeHandle

当需要暴露自定义方法时,使用 useImperativeHandle

useImperativeHandle(ref, () => ({
  // 暴露的方法
  method1: () => { ... },
  method2: () => { ... }
}));

2. TypeScript 类型声明

// 定义 Ref 的类型
type InputHandle = {
  focus: () => void;
  blur: () => void;
};

// 定义 Props 的类型
type InputProps = {
  placeholder?: string;
};

// 声明组件
const Input = forwardRef<InputHandle, InputProps>((props, ref) => {
  const inputRef = useRef<HTMLInputElement>(null);

  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current?.focus(),
    blur: () => inputRef.current?.blur()
  }));

  return <input ref={inputRef} {...props} />;
});

3. displayName 属性

添加 displayName 便于调试和 React DevTools 识别:

CustomComponent.displayName = "CustomComponent";

4. 不要过度使用

不好的做法:过度暴露 DOM API

// 不推荐:暴露所有 DOM 方法
useImperativeHandle(ref, () => inputRef.current);

好的做法:只暴露必要的接口

// 推荐:只暴露需要的方法
useImperativeHandle(ref, () => ({
  focus: () => inputRef.current?.focus(),
  setValue: (value) => { ... }
}));

🏆 最佳实践

1. 规范化命名

// ✅ 好
const Button = forwardRef<HTMLButtonElement, ButtonProps>(...);

// ❌ 不好
const Btn = forwardRef((props, ref) => ...);

2. 完整的 TypeScript 支持

// ✅ 推荐
interface ComponentHandle {
  method1: () => void;
  method2: (param: string) => void;
}

interface ComponentProps {
  prop1: string;
}

const Component = forwardRef<ComponentHandle, ComponentProps>(
  (props, ref) => { ... }
);

// ❌ 不推荐
const Component = forwardRef((props, ref) => { ... });

3. 添加 displayName

const Button = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
  return <button ref={ref}>{props.children}</button>;
});

Button.displayName = 'Button'; // ✅ 添加这一行

4. 优雅降级

// 使用可选链操作符
useImperativeHandle(ref, () => ({
  focus: () => inputRef.current?.focus(),
  blur: () => inputRef.current?.blur(),
  clear: () => {
    if (inputRef.current) {
      inputRef.current.value = "";
    }
  },
}));

⚠️ 常见问题

Q1: 为什么 forwardRef 中不能直接使用 ref?

:因为 ref 不是 prop,它是特殊的 API。React 会将其单独处理。

Q2: forwardRef 性能影响大吗?

:性能影响微乎其微,可以忽略。

Q3: 函数组件可以直接接收 ref 吗?

:不能。必须使用 forwardRef 包装。

// ❌ 不能
const MyComponent = (props, ref) => <div ref={ref} />;

// ✅ 必须这样
const MyComponent = forwardRef((props, ref) => <div ref={ref} />);

Q4: 什么时候应该使用 forwardRef?

:以下情况使用:

  • ✅ 需要访问 DOM 元素(焦点、滚动等)
  • ✅ 需要触发 DOM 方法(play、pause 等)
  • ✅ 创建 UI 组件库
  • ✅ 需要集成第三方 DOM 库

不应该使用

  • ❌ 仅为了传递 props(使用 children 或 props)
  • ❌ 管理数据状态(使用 state)

🎓 实战案例(Button 组件)

我们的 Button 组件为什么要使用 forwardRef:

// src/button/index.tsx
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  (props: ButtonProps, ref) => {
    const {
      type = "normal",
      size = "medium",
      // ... 其他 props
    } = props;

    return (
      <button
        {...others}
        ref={ref}  // ✅ 转发 ref  DOM button
        className={cls}
        // ... 其他属性
      >
        {children}
      </button>
    );
  }
);

使用示例

// 父组件
const App = () => {
  const buttonRef = useRef < HTMLButtonElement > null;

  const handleClick = () => {
    // 现在可以直接控制按钮元素
    buttonRef.current?.focus();
    buttonRef.current?.blur();

    // 获取 button 属性
    console.log(buttonRef.current?.disabled);
    console.log(buttonRef.current?.innerText);
  };

  return (
    <>
      <Button ref={buttonRef} type="primary">
        Click me
      </Button>
      <button onClick={handleClick}>Control Button</button>
    </>
  );
};

📖 参考资源


✨ 总结

特性说明
用途将 ref 从父组件转发到子组件 DOM 元素
何时使用访问 DOM、调用 DOM 方法、创建组件库
搭配工具useImperativeHandle(暴露自定义方法)
性能无明显影响
可维护性高(清晰的公开接口)
复杂性低(API 简单易用)

forwardRef 是创建专业级 React 组件库的必需工具! 🚀