Skip to content

1.forwardRef 两种用法

1.forwardRef+useImperativeHandle 定义 ref 上的方法

1.useImperativeHandle 介绍

useImperativeHandle 是 React 中的一个 Hook,通常与 forwardRef 一起使用,用来 暴露组件内部实例的特定值或方法 给父组件。它允许你控制从子组件传递给父组件的 ref 的内容,而不是直接暴露整个组件实例。

主要用途:

useImperativeHandle 允许你 定制通过 **ref** 传递给父组件的接口。通常情况下,父组件可以通过 ref 获取子组件的完整实例,但有时你可能只希望父组件能访问到某些特定的方法或属性,而不是整个实例。这时,useImperativeHandle 就派上用场了。

语法:

javascript
useImperativeHandle(ref, createHandle, [deps]);
  • ref:来自父组件的 ref,通常是通过 forwardRef 转发的。
  • createHandle:一个函数,返回你希望暴露给父组件的值或方法。
  • [deps]:依赖项数组,类似于 useEffect 的依赖项数组,表示只有当这些值发生变化时,才会重新生成暴露的接口。

使用场景:

  • 控制暴露的接口:当父组件需要调用子组件的方法,但你不希望直接暴露整个子组件实例时,可以通过 useImperativeHandle 来实现。
  • 封装逻辑:你可以只暴露对子组件某些行为的控制,而隐藏其他不需要暴露的实现细节。

示例:

假设有一个自定义的 Input 组件,父组件可以通过 ref 控制它的焦点或清除它的内容,但不暴露其他方法。

子组件(Input.js):
javascript
import React, { useImperativeHandle, useRef, forwardRef } from "react";

// 使用 forwardRef 转发 ref 给子组件
const Input = forwardRef((props, ref) => {
  const inputRef = useRef();

  // 使用 useImperativeHandle 来暴露给父组件的接口
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    },
    clear: () => {
      inputRef.current.value = "";
    },
  }));

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

export default Input;
父组件:
javascript
import React, { useRef } from "react";
import Input from "./Input";

const ParentComponent = () => {
  const inputRef = useRef();

  const handleFocus = () => {
    inputRef.current.focus(); // 调用子组件暴露的 focus 方法
  };

  const handleClear = () => {
    inputRef.current.clear(); // 调用子组件暴露的 clear 方法
  };

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

export default ParentComponent;

关键点:

  1. **forwardRef**:用来将父组件传递的 ref 转发到子组件内部。
  2. **useImperativeHandle**:在子组件中定制 ref 暴露给父组件的内容。

总结:

  • useImperativeHandle 主要用于与 forwardRef 一起使用,以控制子组件通过 ref 暴露给父组件的内容。
  • 它使得子组件可以选择性地暴露某些方法或属性,而不是暴露整个组件实例,这有助于提高代码的封装性和可维护性。

2.例子:使用 forwardRef 获取组件实例,调用内部函数

内部

暴露方法:使用 useImperativeHandle

外部

3.效果:外部点击算子的时候,通过 ref 调用子组件侧边栏弹出的方法

如果你想在外部调用 ResizableRightDiv 组件中的 handleShrink 函数,你需要通过 Ref 来引用 ResizableRightDiv 组件,并使用引用来调用 handleShrink 函数。

首先,在 ResizableRightDiv 组件中,需要使用 useImperativeHandle 钩子函数将 handleShrink 函数暴露给父组件。以下是示例代码:

jsx
import React, { forwardRef, useImperativeHandle } from 'react';

// ...

const ResizableRightDiv = forwardRef(({ onShrink }, ref) => {
  // ...

  const handleShrink = () => {
    // 执行收缩操作
    // ...

    // 调用传递进来的 onShrink 回调函数
    onShrink();
  };

  // 将 handleShrink 函数暴露给父组件
  useImperativeHandle(ref, () => ({
    handleShrink,
  }));

  // ...

  return (
    // ...
  );
});

export default ResizableRightDiv;

在上述代码中,我们使用 forwardRef 函数来创建一个引用转发的组件。然后,在 useImperativeHandle 钩子函数中,将 handleShrink 函数暴露给父组件。

接下来,在父组件中,你需要创建一个引用,并将其传递给 ResizableRightDiv 组件。然后,你就可以通过引用来调用 handleShrink 函数。以下是示例代码:

jsx
import React, { useRef } from "react";
import ResizableRightDiv from "./ResizableRightDiv";

export default function ParentComponent() {
  const resizableRef = useRef(null);

  const handleShrinkButtonClick = () => {
    resizableRef.current.handleShrink();
  };

  return (
    <div>
      <button onClick={handleShrinkButtonClick}>收缩</button>
      <ResizableRightDiv ref={resizableRef} />
    </div>
  );
}

在上述代码中,我们创建了一个引用 resizableRef,并将其传递给 ResizableRightDiv 组件的 ref 属性。然后,在点击收缩按钮的事件处理程序中,我们通过引用调用 handleShrink 函数。

2.forwardRef 直接绑定到子组件的 dom 上

直接使用 forwardRef 转发 ref

javascript
const Input = forwardRef((props, ref) => {
  return <input ref={ref} />;
});
  • 在这个例子中,你只是简单地将 ref 转发给了子组件中的 input 元素。这意味着父组件传递给 ref将直接指向 **input** 元素,而不是通过一个自定义的接口暴露给父组件。
  • 如果你仅仅想让父组件控制 input 元素(例如,聚焦、获取值等),那么这种写法就足够了。

如果改成使用 useImperativeHandle 暴露方法

javascript
import React, { useRef, forwardRef, useImperativeHandle } from "react";

const Input = forwardRef((props, ref) => {
  const inputRef = useRef();

  // 使用 useImperativeHandle 来暴露接口给父组件
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus(); // 聚焦到 input 元素
    },
    clear: () => {
      inputRef.current.value = ""; // 清空输入框内容
    },
  }));

  return <input ref={inputRef} />;
});
  • **useImperativeHandle** 允许你定制父组件通过 ref 能访问到的接口。这种方式适用于当你需要暴露自定义方法(如 focus, clear 等)时。
  • 通过 useImperativeHandle,你能够控制父组件能访问到哪些特定的属性或方法,而不是直接暴露整个 input 元素的 ref

何时使用哪种方式?两者的对比

  • **简单转发 ****ref**:如果父组件只需要直接访问 DOM 元素的 ref,比如操作一个 <input /> 的值或调用它的 focus() 方法,你不需要 useImperativeHandle。直接转发 ref 就足够了。
  • 暴露特定方法或接口:如果你希望父组件通过 ref 控制子组件的某些特定行为(例如调用自定义方法),那么就需要使用 useImperativeHandle 来控制暴露给父组件的接口。

3.React 组件通讯方式的总结

forwardRefuseImperativeHandle 是 React 中用来实现 父子组件之间的通信 的一种特定方式,尤其是在 管理子组件的实例方法 时。这种组合方式的确可以看作是一种特殊的父到子通讯方式,允许父组件通过 ref 访问子组件暴露的特定接口。

但 React 提供了多种组件间通信的方式,下面是几种常见的父子、子父之间的通信方式以及它们的使用场景。

1. 通过 **props** 传递数据(父到子)

这是最常见的通信方式。父组件通过 props 向子组件传递数据或方法。子组件通过 props 接收并使用这些数据或方法。

  • 父到子:通过 props 传递数据或方法给子组件。
  • 子到父:通过调用父组件传递的函数,父组件可以接收来自子组件的数据。

示例:

javascript
// 父组件
function Parent() {
  const [count, setCount] = useState(0);

  const increment = () => setCount(count + 1);

  return <Child count={count} increment={increment} />;
}

// 子组件
function Child({ count, increment }) {
  return (
    <div>
      <p>{count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

2. 通过 **ref****forwardRef**(父<->子,控制子组件实例)

ref 是用于访问组件实例或 DOM 元素的方式。通过 ref,父组件可以控制子组件的实例方法或 DOM 元素,尤其是在类组件中比较常见。

  • 父到子:父组件通过 ref 获取子组件实例或 DOM。
  • 子到父:子组件没有直接向父组件传递数据,而是通过 ref 暴露某些方法。

示例:

javascript
// 子组件
const Child = forwardRef((props, ref) => {
  useImperativeHandle(ref, () => ({
    focus: () => {
      // 子组件暴露给父组件的方法
      console.log("Focus method called");
    },
  }));

  return <input />;
});

// 父组件
function Parent() {
  const childRef = useRef();

  const handleFocus = () => {
    childRef.current.focus(); // 父组件调用子组件暴露的方法
  };

  return (
    <div>
      <Child ref={childRef} />
      <button onClick={handleFocus}>Focus Input</button>
    </div>
  );
}
  • forwardRef 用于将 ref 转发到子组件。
  • useImperativeHandle 用来定制通过 ref 暴露给父组件的内容(如暴露 focus 方法)。

3. **Context**** API(深层次的组件传递数据):父到子**

Context 是一种用于跨组件树传递数据的机制。它适合用于多个组件之间共享状态,避免通过层层传递 props

  • 父到子:通过 Context.Provider 将数据传递给子组件及其嵌套的后代组件。
  • 子到父:子组件可以通过 useContext 来访问父组件通过 Context 提供的值。

示例:

javascript
const MyContext = React.createContext();

function Parent() {
  const [value, setValue] = useState("Hello, World!");

  return (
    <MyContext.Provider value={value}>
      <Child />
    </MyContext.Provider>
  );
}

function Child() {
  const value = useContext(MyContext); // 从 context 中获取父组件提供的数据
  return <p>{value}</p>;
}

4. 传递操作函数:子到父

子组件通过调用父组件传递下来的更新函数,来实现将数据传递给父组件。通常通过 useStateuseEffect 来管理和传递状态。

示例:

javascript
// 父组件
function Parent() {
  const [count, setCount] = useState(0);

  const handleCountChange = (newCount) => {
    setCount(newCount); // 父组件更新状态
  };

  return <Child onCountChange={handleCountChange} />;
}

// 子组件
function Child({ onCountChange }) {
  const handleClick = () => {
    onCountChange(5); // 子组件通过回调函数传递数据给父组件
  };

  return <button onClick={handleClick}>Update Count</button>;
}

5. **useReducer**(共享状态的父子通信):props+传操作函数:父<->子

useReducer 提供了一种管理组件复杂状态的方法,适用于需要多个操作来处理数据变化的场景。父子组件都可以通过分发动作(dispatch)来更新状态。

示例:

javascript
function reducer(state, action) {
  switch (action.type) {
    case "increment":
      return { count: state.count + 1 };
    default:
      return state;
  }
}

function Parent() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });

  return <Child state={state} dispatch={dispatch} />;
}

function Child({ state, dispatch }) {
  return (
    <div>
      <p>{state.count}</p>
      <button onClick={() => dispatch({ type: "increment" })}>Increment</button>
    </div>
  );
}

总结:React 组件通信方式

React 提供了多种父子组件间的通信方式,每种方式都有其特定的使用场景。

  1. **props**:最常见的父子通信方式,父组件通过 props 向子组件传递数据,子组件可以通过回调函数将数据传递回父组件。
  2. **ref**** 和 ****forwardRef**:父组件通过 ref 获取子组件实例或 DOM 元素,用于访问和控制子组件的特定行为或方法。
  3. **Context**:用于跨层级的组件共享数据,避免逐层传递 props,适用于全局状态管理。
  4. **useState**** 和 ****useEffect**:子组件通过回调函数更新父组件的状态,父组件通过 useState 管理数据,子组件通过 props 与父组件交互。
  5. **useReducer**:适用于复杂状态管理,父子组件可以通过 dispatch 来更新状态。

**forwardRef**** 和 ****useImperativeHandle** 是一种父到子、控制子组件实例行为的方式,通常用于需要暴露子组件特定方法(而非整个实例)时,帮助父组件控制子组件的行为,而不是直接操作子组件的 DOM。