javascript 46 lines · 8 steps

Exposing an imperative handle in React

A forwardRef TextField hides its DOM node but exposes a controlled focus/clear API to parents.

Explained by highlit
1import { forwardRef, useImperativeHandle, useRef, useState } from 'react';
2 
3const TextField = forwardRef(function TextField(
4 { label, error, onChange, ...props },
5 ref
6) {
7 const inputRef = useRef(null);
8 const [touched, setTouched] = useState(false);
9 
10 useImperativeHandle(
11 ref,
12 () => ({
13 focus: () => inputRef.current?.focus(),
14 select: () => inputRef.current?.select(),
15 clear: () => {
16 if (inputRef.current) inputRef.current.value = '';
17 setTouched(false);
18 },
19 get value() {
20 return inputRef.current?.value ?? '';
21 },
22 }),
23 []
24 );
25 
26 return (
27 <label className="text-field">
28 {label && <span className="text-field__label">{label}</span>}
29 <input
30 ref={inputRef}
31 className={error && touched ? 'input input--error' : 'input'}
32 aria-invalid={Boolean(error && touched)}
33 onBlur={() => setTouched(true)}
34 onChange={onChange}
35 {...props}
36 />
37 {error && touched && (
38 <span role="alert" className="text-field__error">
39 {error}
40 </span>
41 )}
42 </label>
43 );
44});
45 
46export default TextField;
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1useImperativeHandle lets a component expose a curated method surface instead of leaking its raw DOM node.
  2. 2Tracking a touched flag defers error styling until after the user leaves the field.
  3. 3Spreading remaining props onto the input keeps a wrapper component flexible without re-declaring every attribute.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Exposing an imperative handle in React — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code