React: useImperativeHandle with Typescript

kaichikaichi • 2022-04-11

2 分钟0 阅读

封装组件时,有时组件需要暴露命令式的 API 供外部调用,useImperativeHandle Hook 可以自定义要暴露的实例。比如:

function Counter({ defaultValue = 0 }) {
  const [count, setCount] = useState(defaultValue);

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

  return (
    <>
      {count}
      <button onClick={increment}>click me</button>
    </>
  );
}

function App() {
  return <Counter />;
}

现在 Counter 组件内部完成对状态的控制,如果 App 要通过命令式的方式调用 Counterincrement 方法,可以使用 useImperativeHandle Hook 将该方法暴露出去。

Counter.jsx
const Counter = forwardRef(({ defaultValue = 0 }, ref) => {
  const [count, setCount] = useState(defaultValue);
  const increment = () => setCount((prev) => prev + 1);

  useImperativeHandle(
    ref,
    () => ({
      increment
    }),
    []
  );

  return (
    <>
      {count}
      <button onClick={increment}>click me</button>
    </>
  );
});

export default Counter;
App.jsx
export default function App() {
  const counterRef = useRef(null);
  return (
    <>
      <Counter ref={counterRef} />
      <button onClick={() => counterRef.current?.increment()}>click</button>
    </>
  );
}

点击 App 中的按钮时,也能改变 Counter 内部的状态,但是如果用 typescript 改写组件时,则会发现暴露出来的 API increment 方法的类型无法推断出来。

error

改写一下 Counter 组件。

Counter.tsx
type CounterHandle = {
  increment: VoidFunction;
};

type CounterProps = {
  defaultValue?: number;
};

const Counter: ForwardRefRenderFunction<CounterHandle, CounterProps> = (
  { defaultValue = 0 },
  ref
) => {
  const [count, setCount] = useState(defaultValue);
  const increment = () => setCount((prev) => prev + 1);

  useImperativeHandle(
    ref,
    () => ({
      increment
    }),
    []
  );

  return (
    <>
      {count}
      <button onClick={increment}>click me</button>
    </>
  );
};

export default forwardRef(Counter);

App 中使用。

App.tsx
function App() {
  const counterRef = useRef<ElementRef<typeof Counter>>(null);
  return (
    <>
      <Counter ref={counterRef} />
      <button onClick={() => counterRef.current?.increment()}>click</button>
    </>
  );
}

类型可以正常推导。以上,下面是全部代码。

Counter.tsx
import {
  forwardRef,
  useState,
  useImperativeHandle,
  ForwardRefRenderFunction
} from "react";

type CounterHandle = {
  increment: VoidFunction;
};

type CounterProps = {
  defaultValue?: number;
};

const Counter: ForwardRefRenderFunction<CounterHandle, CounterProps> = (
  { defaultValue = 0 },
  ref
) => {
  const [count, setCount] = useState(defaultValue);
  const increment = () => setCount((prev) => prev + 1);

  useImperativeHandle(
    ref,
    () => ({
      increment
    }),
    []
  );

  return (
    <>
      {count}
      <button onClick={increment}>click me</button>
    </>
  );
};

export default forwardRef(Counter);
App.tsx
import { ElementRef, useRef } from "react";
import Counter from "./Counter";

export default function App() {
  const counterRef = useRef<ElementRef<typeof Counter>>(null);

  return (
    <>
      <Counter defaultValue={1} ref={counterRef} />
      <button onClick={() => counterRef.current?.increment()}>click</button>
    </>
  );
}

在 Github 上编辑