Skip to main content
useImperativeHandle customizes the instance value that is exposed to parent components when using ref. This hook should be used with forwardRef.

Signature

function useImperativeHandle<T, R extends T>(
  ref: Ref<T>,
  create: () => R,
  inputs?: ReadonlyArray<unknown>
): void

Parameters

ref
Ref<T>
required
The ref that will be mutated. This is typically the ref forwarded from a parent component.
create
() => R
required
A function that returns the value to be attached to ref.current. This should return an object with the methods/properties you want to expose.
inputs
ReadonlyArray<unknown>
An array of dependencies. The effect will only activate if the values in this array change (compared using ===). If omitted, the handle is recreated on every render.

Returns

void

Basic Usage

import { forwardRef, useImperativeHandle, useRef } from 'preact/compat';
import { useRef as useHookRef } from 'preact/hooks';

const FancyInput = forwardRef((props, ref) => {
  const inputRef = useHookRef();

  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    },
    clear: () => {
      inputRef.current.value = '';
    }
  }), []);

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

function Parent() {
  const inputRef = useHookRef();

  return (
    <div>
      <FancyInput ref={inputRef} />
      <button onClick={() => inputRef.current.focus()}>Focus</button>
      <button onClick={() => inputRef.current.clear()}>Clear</button>
    </div>
  );
}

Video Player Control

const VideoPlayer = forwardRef(({ src }, ref) => {
  const videoRef = useRef();

  useImperativeHandle(ref, () => ({
    play: () => {
      videoRef.current.play();
    },
    pause: () => {
      videoRef.current.pause();
    },
    seek: (time) => {
      videoRef.current.currentTime = time;
    },
    setVolume: (volume) => {
      videoRef.current.volume = Math.max(0, Math.min(1, volume));
    },
    getCurrentTime: () => {
      return videoRef.current.currentTime;
    }
  }), []);

  return <video ref={videoRef} src={src} />;
});

function VideoController() {
  const playerRef = useRef();

  return (
    <div>
      <VideoPlayer ref={playerRef} src="video.mp4" />
      <button onClick={() => playerRef.current.play()}>Play</button>
      <button onClick={() => playerRef.current.pause()}>Pause</button>
      <button onClick={() => playerRef.current.seek(0)}>Restart</button>
      <button onClick={() => playerRef.current.setVolume(0.5)}>50% Volume</button>
    </div>
  );
}

Form Control

const Form = forwardRef(({ children }, ref) => {
  const formRef = useRef();
  const [errors, setErrors] = useState({});

  useImperativeHandle(ref, () => ({
    submit: () => {
      formRef.current.dispatchEvent(
        new Event('submit', { cancelable: true, bubbles: true })
      );
    },
    reset: () => {
      formRef.current.reset();
      setErrors({});
    },
    validate: () => {
      const isValid = formRef.current.checkValidity();
      if (!isValid) {
        // Collect validation errors
        const newErrors = {};
        const elements = formRef.current.elements;
        for (let i = 0; i < elements.length; i++) {
          const el = elements[i];
          if (!el.validity.valid) {
            newErrors[el.name] = el.validationMessage;
          }
        }
        setErrors(newErrors);
      }
      return isValid;
    },
    getValues: () => {
      const formData = new FormData(formRef.current);
      return Object.fromEntries(formData.entries());
    }
  }), []);

  return <form ref={formRef}>{children}</form>;
});

function App() {
  const formRef = useRef();

  const handleExternalSubmit = () => {
    if (formRef.current.validate()) {
      const values = formRef.current.getValues();
      console.log('Form values:', values);
      formRef.current.submit();
    }
  };

  return (
    <div>
      <Form ref={formRef}>
        <input name="email" type="email" required />
        <input name="password" type="password" required />
      </Form>
      <button onClick={handleExternalSubmit}>Submit</button>
      <button onClick={() => formRef.current.reset()}>Reset</button>
    </div>
  );
}
const Modal = forwardRef(({ title, children }, ref) => {
  const [isOpen, setIsOpen] = useState(false);

  useImperativeHandle(ref, () => ({
    open: () => setIsOpen(true),
    close: () => setIsOpen(false),
    toggle: () => setIsOpen(prev => !prev),
    isOpen: () => isOpen
  }), [isOpen]);

  if (!isOpen) return null;

  return (
    <div className="modal-overlay">
      <div className="modal">
        <h2>{title}</h2>
        <div>{children}</div>
        <button onClick={() => setIsOpen(false)}>Close</button>
      </div>
    </div>
  );
});

function App() {
  const modalRef = useRef();

  return (
    <div>
      <button onClick={() => modalRef.current.open()}>Open Modal</button>
      <Modal ref={modalRef} title="My Modal">
        <p>Modal content here</p>
      </Modal>
    </div>
  );
}

Canvas Drawing API

const Canvas = forwardRef(({ width, height }, ref) => {
  const canvasRef = useRef();

  useImperativeHandle(ref, () => {
    const ctx = canvasRef.current?.getContext('2d');
    return {
      clear: () => {
        ctx.clearRect(0, 0, width, height);
      },
      drawCircle: (x, y, radius, color = 'black') => {
        ctx.beginPath();
        ctx.arc(x, y, radius, 0, 2 * Math.PI);
        ctx.fillStyle = color;
        ctx.fill();
      },
      drawRect: (x, y, w, h, color = 'black') => {
        ctx.fillStyle = color;
        ctx.fillRect(x, y, w, h);
      },
      drawLine: (x1, y1, x2, y2, color = 'black', width = 1) => {
        ctx.strokeStyle = color;
        ctx.lineWidth = width;
        ctx.beginPath();
        ctx.moveTo(x1, y1);
        ctx.lineTo(x2, y2);
        ctx.stroke();
      },
      getImageData: () => {
        return canvasRef.current.toDataURL();
      }
    };
  }, [width, height]);

  return <canvas ref={canvasRef} width={width} height={height} />;
});

function DrawingApp() {
  const canvasRef = useRef();

  return (
    <div>
      <Canvas ref={canvasRef} width={800} height={600} />
      <button onClick={() => canvasRef.current.drawCircle(100, 100, 50, 'red')}>
        Draw Circle
      </button>
      <button onClick={() => canvasRef.current.clear()}>Clear</button>
    </div>
  );
}

With Dependencies

const Counter = forwardRef(({ step = 1 }, ref) => {
  const [count, setCount] = useState(0);

  useImperativeHandle(ref, () => ({
    increment: () => setCount(c => c + step),
    decrement: () => setCount(c => c - step),
    reset: () => setCount(0),
    getValue: () => count
  }), [step, count]); // Recreate when step or count changes

  return <div>Count: {count}</div>;
});
useImperativeHandle should be used with forwardRef. It allows child components to expose a custom API to parent components through refs.
This hook runs during the layout phase (like useLayoutEffect), so the ref value is updated synchronously before the browser paints.
Avoid overusing imperative APIs. Most interactions between components should be done declaratively through props. Use useImperativeHandle sparingly for cases where you need to expose imperative methods like focus(), play(), or reset().

Build docs developers (and LLMs) love