React Ref揭秘:直接操作DOM的"秘密通道"

大家好,我是小杨。今天想和大家聊聊React中一个既强大又容易让人困惑的特性------Ref。就像它的名字一样,Ref确实是React世界中的一条"秘密通道",让我们能够直接与DOM元素对话。

初识Ref:为什么需要"走后门"?

还记得我刚学React时,被告知要"忘记DOM操作",一切通过状态驱动。但很快我就遇到了这样的需求:

javascript 复制代码
function SearchBox() {
  const [query, setQuery] = useState('');
  
  const handleFocus = () => {
    // 我想让输入框自动获得焦点,但怎么直接操作DOM呢?
    // document.getElementById('search-input').focus() ? 
    // 这违背了React的原则!
  };
  
  return (
    <input 
      id="search-input"
      value={query}
      onChange={(e) => setQuery(e.target.value)}
    />
  );
}

这时候,Ref就像一把钥匙,打开了直接操作DOM的大门。

Ref的三种武功秘籍

1. useRef Hook - 现代React的首选

javascript 复制代码
function AutoFocusInput() {
  const inputRef = useRef(null);
  
  useEffect(() => {
    // 组件挂载后自动聚焦
    inputRef.current.focus();
  }, []);
  
  return (
    <input 
      ref={inputRef}
      placeholder="我会自动获得焦点!"
    />
  );
}

2. createRef - 类组件中的老将

javascript 复制代码
class TraditionalInput extends Component {
  constructor(props) {
    super(props);
    this.inputRef = createRef();
  }
  
  componentDidMount() {
    this.inputRef.current.focus();
  }
  
  render() {
    return <input ref={this.inputRef} />;
  }
}

3. 回调Ref - 更灵活的选择

javascript 复制代码
function CallbackRefExample() {
  const [inputElement, setInputElement] = useState(null);
  
  useEffect(() => {
    if (inputElement) {
      inputElement.focus();
    }
  }, [inputElement]);
  
  return (
    <input ref={setInputElement} />
  );
}

Ref的实际战斗场景

场景1:管理焦点、文本选择

javascript 复制代码
function LoginForm() {
  const emailRef = useRef(null);
  const passwordRef = useRef(null);
  
  const handleEmailSubmit = () => {
    // 邮箱输入完成后,自动跳到密码框
    passwordRef.current.focus();
  };
  
  return (
    <form>
      <input 
        ref={emailRef}
        placeholder="邮箱"
        onBlur={handleEmailSubmit}
      />
      <input 
        ref={passwordRef}
        placeholder="密码"
        type="password"
      />
    </form>
  );
}

场景2:集成第三方DOM库

javascript 复制代码
function ChartComponent() {
  const chartContainerRef = useRef(null);
  
  useEffect(() => {
    // 使用D3.js等第三方库时,需要直接操作DOM
    const chart = d3.select(chartContainerRef.current)
      .append('svg')
      // ... 更多D3操作
    
    return () => {
      // 清理工作
      chart.remove();
    };
  }, []);
  
  return <div ref={chartContainerRef} />;
}

场景3:获取组件尺寸

javascript 复制代码
function ResponsiveComponent() {
  const containerRef = useRef(null);
  const [dimensions, setDimensions] = useState({});
  
  useEffect(() => {
    const updateSize = () => {
      if (containerRef.current) {
        setDimensions({
          width: containerRef.current.offsetWidth,
          height: containerRef.current.offsetHeight
        });
      }
    };
    
    updateSize();
    window.addEventListener('resize', updateSize);
    
    return () => window.removeEventListener('resize', updateSize);
  }, []);
  
  return (
    <div ref={containerRef}>
      当前尺寸: {dimensions.width} x {dimensions.height}
    </div>
  );
}

场景4:实现动画效果

javascript 复制代码
function FadeInBox() {
  const boxRef = useRef(null);
  
  useEffect(() => {
    const observer = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting) {
        entry.target.style.opacity = '1';
        entry.target.style.transform = 'translateY(0)';
      }
    });
    
    if (boxRef.current) {
      observer.observe(boxRef.current);
    }
    
    return () => observer.disconnect();
  }, []);
  
  return (
    <div 
      ref={boxRef}
      style={{
        opacity: 0,
        transform: 'translateY(20px)',
        transition: 'all 0.5s ease'
      }}
    >
      我会在进入视口时淡入!
    </div>
  );
}

进阶技巧:forwardRef - 让Ref穿透组件

有时候,我们需要在自定义组件上使用Ref:

javascript 复制代码
// 错误示范:这样Ref不会传递到input上
function MyInput(props) {
  return <input {...props} />;
}

// 正确做法:使用forwardRef
const MyInput = forwardRef((props, ref) => {
  return <input {...props} ref={ref} />;
});

// 使用方式
function ParentComponent() {
  const inputRef = useRef(null);
  
  useEffect(() => {
    inputRef.current.focus();
  }, []);
  
  return <MyInput ref={inputRef} placeholder="我可以获得焦点" />;
}

useImperativeHandle:控制Ref的暴露内容

javascript 复制代码
const FancyInput = forwardRef((props, ref) => {
  const inputRef = useRef(null);
  
  useImperativeHandle(ref, () => ({
    // 只暴露focus方法,隐藏其他DOM API
    focus: () => {
      inputRef.current.focus();
    },
    // 自定义方法
    shake: () => {
      // 实现抖动动画
    }
  }));
  
  return <input ref={inputRef} {...props} />;
});

// 使用时,只能调用暴露的方法
function App() {
  const fancyInputRef = useRef(null);
  
  const handleClick = () => {
    fancyInputRef.current.focus(); // 可以
    // fancyInputRef.current.value = 'hello'; // 错误!没有暴露value
  };
  
  return (
    <>
      <FancyInput ref={fancyInputRef} />
      <button onClick={handleClick}>聚焦</button>
    </>
  );
}

我踩过的坑:Ref使用注意事项

1. Ref不是状态!

javascript 复制代码
function BadExample() {
  const countRef = useRef(0);
  
  const increment = () => {
    countRef.current += 1;
    // 不会触发重新渲染!
    console.log(countRef.current); // 值变了,但UI不会更新
  };
  
  return (
    <div>
      <button onClick={increment}>增加</button>
      {/* 这里永远显示0 */}
      <span>计数: {countRef.current}</span>
    </div>
  );
}

2. 不要在渲染期间操作Ref

javascript 复制代码
function DangerousComponent() {
  const ref = useRef(null);
  
  // 错误!不要在渲染期间操作DOM
  if (ref.current) {
    ref.current.style.color = 'red';
  }
  
  return <div ref={ref}>Hello</div>;
}

我的实战经验总结

该用Ref的时候:

  • 管理焦点、文本选择、媒体播放
  • 触发强制动画
  • 集成第三方DOM库
  • 需要直接测量DOM元素

不该滥用Ref的时候:

  • 能用状态驱动解决的问题
  • 组件间的数据传递(应该用props)
  • 派生数据计算

记住Ref是React的"逃生舱",在大多数情况下,我们应该优先考虑React的数据流和状态管理。只有在真正需要直接操作DOM时,才请出这个强大的工具。

⭐ 写在最后

请大家不吝赐教,在下方评论或者私信我,十分感谢🙏🙏🙏.

✅ 认为我某个部分的设计过于繁琐,有更加简单或者更高逼格的封装方式

✅ 认为我部分代码过于老旧,可以提供新的API或最新语法

✅ 对于文章中部分内容不理解

✅ 解答我文章中一些疑问

✅ 认为某些交互,功能需要优化,发现BUG

✅ 想要添加新功能,对于整体的设计,外观有更好的建议

✅ 一起探讨技术加qq交流群:906392632

最后感谢各位的耐心观看,既然都到这了,点个 👍赞再走吧!

相关推荐
江城开朗的豌豆3 小时前
何时该请出Redux?前端状态管理的正确打开方式
前端·javascript·react.js
玲小珑3 小时前
LangChain.js 完全开发手册(十二)高性能 AI 应用优化技术
前端·langchain·ai编程
小岛前端3 小时前
Vue3 生态再一次加强,网站开发无敌!
前端·vue.js·前端框架
答案answer3 小时前
历时180多天,浅谈我对自由职业的初次探索
前端·程序员·three.js
江城开朗的豌豆3 小时前
Redux的双面人生:天使还是恶魔?
前端·javascript·react.js
JarvanMo3 小时前
为什么 Google 同时投资 Kotlin Multiplatform 和 Flutter
前端
Hello.Reader3 小时前
Flink 容错从状态后端到 Exactly-Once
前端·javascript·flink
小菜全3 小时前
《前端开发中常用的快捷键大全》
前端
努力往上爬de蜗牛3 小时前
安装npm install vuedraggable@next报错
前端·npm·node.js