面试遇到了React ref陷阱,那就

188 阅读3分钟

ref / useRef / forwardRef / Callback ref

ref是什么

ref是一个引用,用于获取组件实例或DOM元素。(也可以用来保存一个可变对象在多次渲染的一致性数据)。 可以使用多种方式使用和创建ref,包括ref属性,useRef hook, forwardRef函数和callback ref.

ref属性

ref属性:ref属性可以用于获取组件实例或DOM元素。它可以直接在组件上使用,例如:


function MyComponent() {
  const myRef = useRef(null);

  function handleClick() {
    myRef.current.focus();
  }

  return (
    <div>
      <input type="text" ref={myRef} />
      <button onClick={handleClick}>Focus Input</button>
    </div>
  );
 }

在这个例子中,MyComponent组件使用useRef钩子创建了一个ref,并将其传递给input元素的ref属性。在handleClick函数中,可以通过myRef.current来获取input元素,并调用其focus方法。

useRef Hook

useRef钩子可以创建一个ref对象,并在组件的生命周期中保持引用不变。它可以用于保存组件的状态或其他信息。例如:


function MyComponent() {
  const inputRef = useRef(null);
  const [text, setText] = useState("");

  function handleChange(event) {
    setText(event.target.value);
  }

  useEffect(() => {
    inputRef.current.focus();
  }, []);

  return (
    <div>
      <input type="text" ref={inputRef} value={text} onChange={handleChange} />
    </div>
  );
}

在这个例子中,MyComponent组件使用useRef钩子创建了一个inputRef对象,并将其传递给input元素的ref属性。在useEffect钩子中,可以通过inputRef.current来获取input元素,并调用其focus方法。同时,MyComponent组件使用useState钩子来保存input元素的值,并将其传递给input元素的value属性。

forwardRef函数

forwardRef函数可以用于将ref属性向下传递给子组件。它可以用于包装其他组件,并将ref属性传递给包装组件的子组件。例如:


const WrappedComponent = forwardRef((props, ref) => {
  return <ChildComponent {...props} forwardedRef={ref} />;
});

function ParentComponent() {
  const childRef = useRef(null);

  function handleClick() {
    childRef.current.doSomething();
  }

  return (
    <div>
      <WrappedComponent ref={childRef} />
      <button onClick={handleClick}>Call Child Component Method</button>
    </div>
  );
}

在这个例子中,WrappedComponent是一个高阶组件,用于包装ChildComponent,并将ref属性传递给ChildComponent的forwardedRef属性。在ParentComponent中,可以使用useRef钩子创建一个childRef对象,并将其传递给WrappedComponent的ref属性。在handleClick函数中,可以通过childRef.current来获取ChildComponent实例,并调用其doSomething方法。

callback ref

callback ref是一种回调函数,用于在ref被挂载或卸载时执行一些自定义逻辑。它可以用于获取组件实例或DOM元素,并执行一些自定义操作。例如:


function MyComponent() {
  const myRef = useRef(null);

  useEffect(() => {
    if (myRef.current) {
      console.log("Ref has been mounted!");
    }
  }, [myRef]);

  function handleRef(ref) {
    myRef.current = ref;
  }

  return <div ref={handleRef}>My Component</div>;
}

在这个例子中,MyComponent组件使用useRef钩子创建了一个myRef对象,并定义了一个handleRef回调函数,用于将组件的ref赋值给myRef.current。在useEffect钩子中,可以判断myRef.current是否存在,并输出相应的信息。同时,MyComponent组件将handleRef函数作为回调函数传递给div元素的ref属性,用于在ref被挂载时执行自定义逻辑。

ref特点

  1. 多次渲染保存的是同一个对象的引用值,也就是说如果不手动更新其内部的属性值,ref的值一直不会变。
  2. ref对象只有一个属性是current,改变ref引用对象的current属性,不会引发组件的重新渲染,只有组件卸载(销毁)后才会没。
  3. 函数式组件是不能直接添加ref属性的,因为它本身没有实例。需要forwardRef去包装一下

使用场景

  1. 可以获取上一次的值
const usePrevious = (val) => {
    const ref = useRef();
    useEffect(() => {
        ref.current = val;
    });
    return res.current;
}
  1. 可以保存在组件整个生命周期都不会变化的值
// 保存定时器ID
const App = () => {
    let timer = useRef(null);
    useEffect(() => {
        timer.current = setInterval(() => {}, 1000);
    }, []);
    return <button onClick={() => {clearInterval(timer.current)}}>stop</button>
}
  1. 可以用useRef实现一个DeepCompare版本的useEffect
import { isEqual } from 'lodash';

const useDeepCompareEffect = (cb, deps) => {
    const emitEffect = useRef(0);
    const prevDeps = useRef(deps);
    if (!isEqual(prevDeps,current, deps) {
        emitEffect.current++;
    }
    prevDeps.current = deps;
    return useEffect(cb, [emitEffect.current])
}