MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

React useRef 钩子的使用场景与技巧

2022-01-106.6k 阅读

1. 理解 useRef 的基本概念

在 React 中,useRef 是一个 Hook,它允许我们在函数组件中创建可变的引用。与一般的状态(如通过 useState 创建的)不同,useRef 创建的引用在组件的整个生命周期内保持不变。这意味着,当组件重新渲染时,useRef 的值不会像 useState 那样触发重新渲染,除非显式地改变它。

从本质上讲,useRef 返回一个可变的 ref 对象,其 .current 属性初始化为传入的参数(初始值)。这个 ref 对象在组件的整个生命周期内都存在,即使组件重新渲染,ref 对象的引用也不会改变。

2. useRef 的基本使用

下面通过一个简单的示例来展示 useRef 的基本用法。

import React, { useRef } from'react';

const BasicUseRefExample = () => {
  const countRef = useRef(0);

  const incrementCount = () => {
    countRef.current++;
    console.log(countRef.current);
  };

  return (
    <div>
      <p>Ref value: {countRef.current}</p>
      <button onClick={incrementCount}>Increment</button>
    </div>
  );
};

export default BasicUseRefExample;

在上述代码中,我们使用 useRef 创建了一个 countRef,初始值为 0。当点击按钮时,我们直接修改 countRef.current 的值,并在控制台打印出来。注意,这里的修改不会触发组件的重新渲染,因为 useRef 并不用于触发 React 的更新机制。

3. 在 DOM 操作中的应用

3.1 获取 DOM 元素

useRef 最常见的用途之一是获取 DOM 元素的引用。在 React 中,直接操作 DOM 并不是推荐的做法,但在某些情况下,比如聚焦输入框、滚动到特定元素等,我们需要获取 DOM 元素。

import React, { useRef } from'react';

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

  const focusInput = () => {
    if (inputRef.current) {
      inputRef.current.focus();
    }
  };

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

export default DOMManipulationExample;

在这个例子中,我们通过 useRef 创建了一个 inputRef,并将其作为 ref 属性传递给 <input> 元素。当点击按钮时,我们调用 inputRef.current.focus() 来聚焦输入框。这里 inputRef.current 就是实际的 DOM 元素,前提是该元素已经挂载到 DOM 树中。

3.2 操作 DOM 元素的属性

除了聚焦元素,我们还可以使用 useRef 来操作 DOM 元素的其他属性。例如,我们可以改变一个 <div> 的背景颜色。

import React, { useRef } from'react';

const DOMPropertyManipulationExample = () => {
  const divRef = useRef(null);

  const changeBackgroundColor = () => {
    if (divRef.current) {
      divRef.current.style.backgroundColor = 'lightblue';
    }
  };

  return (
    <div>
      <div ref={divRef} style={{ width: '200px', height: '100px', backgroundColor: 'white' }}>
        Click the button to change my background color.
      </div>
      <button onClick={changeBackgroundColor}>Change Background Color</button>
    </div>
  );
};

export default DOMPropertyManipulationExample;

在这个示例中,当点击按钮时,我们通过 divRef.current 获取 <div> 元素,并修改其 style.backgroundColor 属性,从而改变背景颜色。

4. 跨渲染保存数据

4.1 保存函数的调用次数

有时候,我们希望在组件的多次渲染之间保存一些数据,而这些数据的变化不应该触发组件重新渲染。例如,我们可以使用 useRef 来记录一个函数被调用的次数。

import React, { useRef } from'react';

const FunctionCallCountExample = () => {
  const callCountRef = useRef(0);

  const incrementCallCount = () => {
    callCountRef.current++;
    console.log(`Function has been called ${callCountRef.current} times.`);
  };

  return (
    <div>
      <button onClick={incrementCallCount}>Call Function</button>
    </div>
  );
};

export default FunctionCallCountExample;

在这个例子中,每次点击按钮调用 incrementCallCount 函数时,callCountRef.current 的值会增加,并且这个变化不会导致组件重新渲染。我们只是在控制台打印调用次数,而不会因为调用次数的改变而重新渲染整个组件。

4.2 保存上一次的 props 或 state

我们还可以使用 useRef 来保存上一次的 propsstate。这在比较当前值和上一次的值时非常有用,例如,我们可以在 props 变化时执行一些副作用操作,但只在 props 实际发生变化时才执行,而不是每次渲染都执行。

import React, { useRef } from'react';

const PreviousPropsExample = ({ value }) => {
  const previousValueRef = useRef();

  if (previousValueRef.current!== value) {
    console.log(`Value changed from ${previousValueRef.current} to ${value}`);
    previousValueRef.current = value;
  }

  return (
    <div>
      <p>Current value: {value}</p>
    </div>
  );
};

export default PreviousPropsExample;

在这个组件中,我们通过 previousValueRef 保存上一次的 value。当 value 发生变化时,我们在控制台打印出值的变化情况,并更新 previousValueRef.current。这样,我们可以在不依赖 useEffect 的复杂依赖数组设置的情况下,有效地捕获 props 的变化。

5. 在动画和过渡中的应用

5.1 实现简单动画

useRef 可以与 CSS 动画结合使用,来实现一些简单的动画效果。例如,我们可以通过改变 DOM 元素的 style 属性来实现一个元素的淡入淡出动画。

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

const FadeAnimationExample = () => {
  const fadeRef = useRef(null);

  useEffect(() => {
    if (fadeRef.current) {
      fadeRef.current.style.opacity = '0';
      fadeRef.current.style.transition = 'opacity 1s ease-in-out';
      setTimeout(() => {
        fadeRef.current.style.opacity = '1';
      }, 100);
    }
  }, []);

  return (
    <div>
      <div ref={fadeRef} style={{ width: '200px', height: '100px', backgroundColor: 'lightgreen' }}>
        Fading element
      </div>
    </div>
  );
};

export default FadeAnimationExample;

在这个例子中,我们在组件挂载时(通过 useEffect 且依赖数组为空),先将元素的透明度设置为 0,并添加一个过渡效果。然后通过 setTimeout 在 100 毫秒后将透明度设置为 1,从而实现淡入效果。这里 fadeRef 用于获取 DOM 元素并操作其 style 属性。

5.2 控制动画状态

我们还可以使用 useRef 来控制动画的状态,例如暂停和播放动画。

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

const AnimatedElement = () => {
  const elementRef = useRef(null);
  const [isPlaying, setIsPlaying] = useState(true);

  const toggleAnimation = () => {
    setIsPlaying(!isPlaying);
  };

  useEffect(() => {
    if (elementRef.current) {
      if (isPlaying) {
        elementRef.current.style.animationPlayState = 'running';
      } else {
        elementRef.current.style.animationPlayState = 'paused';
      }
    }
  }, [isPlaying]);

  return (
    <div>
      <div
        ref={elementRef}
        style={{
          width: '100px',
          height: '100px',
          backgroundColor: 'blue',
          animation: 'rotate 5s infinite linear',
          animationPlayState: isPlaying? 'running' : 'paused'
        }}
      />
      <button onClick={toggleAnimation}>
        {isPlaying? 'Pause Animation' : 'Play Animation'}
      </button>
    </div>
  );
};

export default AnimatedElement;

在这个示例中,我们通过 elementRef 获取动画元素,并根据 isPlaying 的状态来控制动画的播放和暂停。当点击按钮时,isPlaying 的状态改变,通过 useEffect 来更新动画的 animationPlayState 属性。

6. 在表单处理中的应用

6.1 访问表单值

在处理表单时,useRef 可以用来获取表单元素的值,而不需要通过 React 的状态管理来跟踪每个输入值。

import React, { useRef } from'react';

const FormExample = () => {
  const nameRef = useRef(null);
  const emailRef = useRef(null);

  const handleSubmit = (e) => {
    e.preventDefault();
    if (nameRef.current && emailRef.current) {
      const name = nameRef.current.value;
      const email = emailRef.current.value;
      console.log(`Name: ${name}, Email: ${email}`);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Name:
        <input type="text" ref={nameRef} />
      </label>
      <br />
      <label>
        Email:
        <input type="email" ref={emailRef} />
      </label>
      <br />
      <button type="submit">Submit</button>
    </form>
  );
};

export default FormExample;

在这个表单示例中,当用户提交表单时,我们通过 nameRef.current.valueemailRef.current.value 获取输入框的值,并在控制台打印出来。这种方式在处理简单表单时非常便捷,尤其是当我们不需要在输入变化时实时更新 React 状态的情况下。

6.2 表单验证

useRef 还可以用于表单验证。我们可以通过获取输入框的值,并根据一定的规则进行验证。

import React, { useRef } from'react';

const ValidatedFormExample = () => {
  const passwordRef = useRef(null);
  const confirmPasswordRef = useRef(null);
  const [isValid, setIsValid] = useState(true);

  const handleSubmit = (e) => {
    e.preventDefault();
    if (passwordRef.current && confirmPasswordRef.current) {
      const password = passwordRef.current.value;
      const confirmPassword = confirmPasswordRef.current.value;
      if (password === confirmPassword) {
        setIsValid(true);
        console.log('Passwords match.');
      } else {
        setIsValid(false);
        console.log('Passwords do not match.');
      }
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Password:
        <input type="password" ref={passwordRef} />
      </label>
      <br />
      <label>
        Confirm Password:
        <input type="password" ref={confirmPasswordRef} />
      </label>
      <br />
      {!isValid && <p style={{ color:'red' }}>Passwords do not match.</p>}
      <button type="submit">Submit</button>
    </form>
  );
};

export default ValidatedFormExample;

在这个表单验证示例中,当用户提交表单时,我们通过 passwordRef.current.valueconfirmPasswordRef.current.value 获取两个密码输入框的值,并进行比较。如果密码不匹配,我们通过 setIsValid(false) 设置验证不通过,并在页面上显示错误信息。

7. 与 useEffect 结合使用

7.1 避免无限循环

在使用 useEffect 时,有时会因为依赖数组的设置不当而导致无限循环。useRef 可以帮助我们解决这个问题。例如,我们有一个组件,它根据 props 的变化来执行一些副作用操作,但我们不想在组件初始化时执行这些操作。

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

const InfiniteLoopAvoidanceExample = ({ value }) => {
  const isInitialMountRef = useRef(true);

  useEffect(() => {
    if (!isInitialMountRef.current) {
      console.log(`Value has changed to: ${value}`);
    } else {
      isInitialMountRef.current = false;
    }
  }, [value]);

  return (
    <div>
      <p>Current value: {value}</p>
    </div>
  );
};

export default InfiniteLoopAvoidanceExample;

在这个例子中,我们使用 isInitialMountRef 来标记组件是否是首次挂载。在 useEffect 中,只有当不是首次挂载时(即 !isInitialMountRef.currenttrue),才会打印 value 的变化信息。这样可以避免在组件初始化时就执行我们期望在 props 变化时才执行的操作,从而防止潜在的无限循环。

7.2 执行延迟操作

useRefuseEffect 结合还可以用于执行延迟操作。例如,我们希望在 props 变化后的一段时间后再执行某个操作。

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

const DelayedActionExample = ({ value }) => {
  const timeoutRef = useRef(null);

  useEffect(() => {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current);
    }
    timeoutRef.current = setTimeout(() => {
      console.log(`Delayed action: Value is now ${value}`);
    }, 1000);

    return () => {
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
      }
    };
  }, [value]);

  return (
    <div>
      <p>Current value: {value}</p>
    </div>
  );
};

export default DelayedActionExample;

在这个示例中,每当 value 变化时,我们先清除之前设置的定时器(如果存在),然后设置一个新的定时器,在 1000 毫秒后执行延迟操作(在控制台打印信息)。timeoutRef 用于保存定时器的引用,以便在组件卸载或 value 再次变化时清除定时器,防止内存泄漏。

8. 性能优化方面的应用

8.1 避免不必要的渲染

在某些情况下,组件内部的一些数据变化并不需要触发整个组件的重新渲染。使用 useRef 可以将这些数据存储在 ref 中,从而避免不必要的重新渲染,提高性能。

import React, { useRef } from'react';

const PerformanceOptimizationExample = () => {
  const dataRef = useRef({ count: 0 });

  const incrementData = () => {
    dataRef.current.count++;
    console.log(dataRef.current.count);
  };

  return (
    <div>
      <p>Ref data count: {dataRef.current.count}</p>
      <button onClick={incrementData}>Increment Data</button>
    </div>
  );
};

export default PerformanceOptimizationExample;

在这个例子中,dataRef.current.count 的变化不会导致组件重新渲染,只有当我们在组件外部依赖于这个数据时,才可能需要考虑将其提升到状态中。这种方式可以减少不必要的重新渲染,特别是在组件包含复杂的子组件树时,对性能提升尤为明显。

8.2 缓存计算结果

我们可以使用 useRef 来缓存一些计算结果,避免在每次渲染时重复计算。例如,我们有一个复杂的计算函数,并且希望在 props 没有变化时不再重新计算。

import React, { useRef } from'react';

const ComplexCalculationExample = ({ value }) => {
  const cachedResultRef = useRef();
  const previousValueRef = useRef();

  const complexCalculation = () => {
    // 模拟复杂计算
    let result = 0;
    for (let i = 0; i < 1000000; i++) {
      result += i * value;
    }
    return result;
  };

  if (previousValueRef.current!== value) {
    cachedResultRef.current = complexCalculation();
    previousValueRef.current = value;
  }

  return (
    <div>
      <p>Cached result: {cachedResultRef.current}</p>
    </div>
  );
};

export default ComplexCalculationExample;

在这个示例中,我们通过 cachedResultRef 缓存复杂计算的结果。只有当 value 发生变化时(即 previousValueRef.current!== value),才会重新执行 complexCalculation 并更新缓存结果。这样可以避免在每次渲染时都进行复杂的计算,提高组件的性能。

9. 注意事项与常见问题

9.1 ref 变化不会触发重新渲染

需要明确的是,useRef 创建的 ref 值变化不会触发组件重新渲染。这与 useState 有很大的区别。如果希望某个值的变化触发重新渲染,应该使用 useState 或其他状态管理机制。例如,在下面的代码中:

import React, { useRef } from'react';

const IncorrectUsageExample = () => {
  const countRef = useRef(0);

  const incrementCount = () => {
    countRef.current++;
    console.log(countRef.current);
  };

  return (
    <div>
      <p>Ref value: {countRef.current}</p>
      <button onClick={incrementCount}>Increment</button>
    </div>
  );
};

export default IncorrectUsageExample;

虽然 countRef.current 的值会随着按钮点击而增加,但页面上显示的 Ref value: {countRef.current} 并不会更新,因为没有触发重新渲染。如果希望页面上的值更新,应该使用 useState

9.2 初始值的设置

在使用 useRef 时,要注意初始值的设置。初始值只会在组件首次渲染时被设置,之后即使组件重新渲染,初始值也不会再次被设置。例如:

import React, { useRef } from'react';

const InitialValueExample = () => {
  const initialValue = Math.random();
  const refValue = useRef(initialValue);

  return (
    <div>
      <p>Ref value: {refValue.current}</p>
    </div>
  );
};

export default InitialValueExample;

在这个例子中,initialValue 是一个随机数,但是 refValue 的初始值只会在组件首次渲染时被设置为这个随机数。之后组件重新渲染,refValue.current 的值不会因为 initialValue 的重新计算而改变。

9.3 在类组件中的使用限制

useRef 是 React Hook,只能在函数组件中使用。如果在类组件中需要类似的功能,可以使用 React.createRef() 或在类实例上定义属性来保存可变数据。例如:

import React from'react';

class ClassComponentExample extends React.Component {
  constructor(props) {
    super(props);
    this.count = 0;
  }

  incrementCount = () => {
    this.count++;
    console.log(this.count);
  };

  render() {
    return (
      <div>
        <p>Count: {this.count}</p>
        <button onClick={this.incrementCount}>Increment</button>
      </div>
    );
  }
}

export default ClassComponentExample;

在这个类组件中,我们在 constructor 中初始化 this.count,并在 incrementCount 方法中修改它的值。虽然这与 useRef 的功能类似,但在使用方式和原理上有所不同。

10. 总结 useRef 的优势与适用场景

useRef 在 React 开发中具有独特的优势和广泛的适用场景。

从优势方面来看,首先,它允许我们在函数组件中拥有可变的状态,而不会触发不必要的重新渲染,这对于性能优化非常重要。其次,它为我们提供了一种直接访问 DOM 元素的便捷方式,在处理 DOM 相关操作时更加灵活。再者,useRef 可以用于跨渲染保存数据,方便我们记录和操作一些在组件多次渲染过程中需要保持不变的数据。

在适用场景上,当我们需要进行 DOM 操作,如聚焦输入框、操作元素属性等,useRef 是首选。在表单处理中,获取表单值和进行表单验证时,useRef 可以简化代码逻辑。在动画和过渡效果实现方面,useRef 能够与 CSS 动画结合,实现各种动画控制。同时,在性能优化领域,避免不必要的渲染和缓存计算结果都可以借助 useRef 来完成。

然而,我们也要清楚 useRef 的局限性,比如它不能像 useState 那样触发重新渲染,在使用时需要根据具体需求合理选择。总之,熟练掌握 useRef 的使用场景与技巧,能够让我们在 React 开发中更加高效地实现各种功能,提升应用的性能和用户体验。