How to Control Focus or Disabled State of a Child Input from a Parent
Advertisement
🧩 Scenario
Architecture Walkthrough
Two Patterns: Declarative and Imperative
Controlling a child component from a parent falls into two categories. Declarative control means the parent passes a prop (like disabled) and the child renders accordingly. This is the React-preferred approach because data flows down through props and the child's state is determined entirely by its props.
Imperative control means the parent calls a method on the child (like focus(), select(), or scrollIntoView()). These are actions, not state, and have no natural prop equivalent. React provides forwardRef and useImperativeHandle for this pattern.
forwardRef
By default, refs cannot be attached to custom function components. forwardRef wraps a component to accept a second ref argument alongside props, and forwards that ref to an internal DOM element. From the parent, ref.current becomes the DOM node and all its native methods are available.
useImperativeHandle
useImperativeHandle allows a child to expose a curated interface through a ref rather than the raw DOM element. Instead of giving the parent unrestricted access to the DOM node, the child defines exactly which methods it exposes. This is better practice for shared components: the contract is explicit and the child can change its implementation without breaking the parent.
Key Code Explained
// Pattern 1: Declarative — disabled is a prop
interface ControlledInputProps {
disabled?: boolean;
placeholder?: string;
}
function ControlledInput({ disabled = false, placeholder }: ControlledInputProps) {
return (
<input
type="text"
disabled={disabled} // prop drives the disabled state
placeholder={placeholder}
className="form-input"
/>
);
}
function ParentDeclarative() {
const [isDisabled, setIsDisabled] = useState(false);
return (
<div>
<ControlledInput disabled={isDisabled} placeholder="Enter text..." />
<button onClick={() => setIsDisabled((d) => !d)}>
{isDisabled ? 'Enable' : 'Disable'} Input
</button>
</div>
);
}
// Pattern 2: Imperative — focus via forwardRef
interface FocusableInputRef {
focus: () => void;
select: () => void;
}
interface FocusableInputProps {
placeholder?: string;
disabled?: boolean;
}
// forwardRef lets the parent attach a ref to this component
const FocusableInput = forwardRef<FocusableInputRef, FocusableInputProps>(
({ placeholder, disabled = false }, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
// Expose only the methods the parent should be able to call
useImperativeHandle(
ref,
() => ({
focus: () => inputRef.current?.focus(),
select: () => inputRef.current?.select(),
// blur() and setValue() are NOT exposed — the parent cannot call them
}),
[], // stable: these methods don't depend on any changing values
);
return (
<input
ref={inputRef}
type="text"
disabled={disabled}
placeholder={placeholder}
className="form-input"
/>
);
},
);
FocusableInput.displayName = 'FocusableInput';
function ParentImperative() {
const inputRef = useRef<FocusableInputRef>(null);
const [isDisabled, setIsDisabled] = useState(false);
const handleFocusClick = () => {
inputRef.current?.focus(); // calls the method exposed by useImperativeHandle
};
const handleSelectClick = () => {
inputRef.current?.select();
};
return (
<div>
<FocusableInput
ref={inputRef} // attaches via forwardRef
disabled={isDisabled} // disabled: still a prop, not imperative
placeholder="Type to search..."
/>
<button onClick={handleFocusClick}>Focus Input</button>
<button onClick={handleSelectClick}>Select All</button>
<button onClick={() => setIsDisabled((d) => !d)}>
{isDisabled ? 'Enable' : 'Disable'}
</button>
</div>
);
}
The disabled state in the imperative example is still passed as a prop, not through the ref. This is intentional: disabled is state (it should cause a re-render when it changes) and follows the declarative pattern. Only focus and select use the ref because they are one-time actions with no state change.
Tradeoffs
| Approach | Mechanism | Best for | React style |
|---|---|---|---|
| Prop (disabled) | Declarative | State-driven behavior (disabled, hidden, value) | Idiomatic |
| forwardRef | Imperative (raw DOM) | When parent needs full DOM access | Use carefully |
| useImperativeHandle | Imperative (curated) | Exposing a specific API from a child component | Preferred for custom components |
What Interviewers Actually Check
- Whether you know the declarative prop approach for state-driven behavior like disabled
- Whether you know when to use forwardRef and useImperativeHandle for imperative actions like focus
- Whether you know that refs cannot be attached to custom components without forwardRef
- Whether you know useImperativeHandle lets the child control what the parent can call
- Whether you set
displayNameon forwardRef components for DevTools readability
Follow-Up Questions
- React 19 introduces
refas a regular prop (noforwardRefwrapper needed). How does this change the pattern? - How would you use
useImperativeHandleto expose avalidate()method that returns a boolean from a child form? - When might you use a callback ref (a function as the ref prop) instead of
useRef? - How does
useImperativeHandleinteract withReact.memo: if the parent re-renders, does the child's handle change? - How would you animate a child element with GSAP from a parent, and which approach (forwardRef or a prop) would you choose?
Common Candidate Mistakes
- Using imperative refs for
disabledstate when a prop is simpler and more idiomatic - Trying to attach a ref to a custom component without
forwardRefand gettingnullwith no clear error in development - Exposing the entire DOM node via
forwardRefwhenuseImperativeHandlewould provide a more controlled interface - Not checking
ref.current?.before calling methods, throwing on null when the component is not yet mounted - Forgetting to add
displayNameto forwardRef components, making debugging in React DevTools harder
Interview Readiness Checklist
Before you leave this question, make sure you can answer:
- Can you use
forwardRefto forward a ref from a parent to a child DOM element? - Can you use
useImperativeHandleto expose onlyfocus()andselect()from a child? - Can you use a controlled prop for
disabledstate from the parent? - Can you explain the declarative vs imperative tradeoff and when to use each?
- Can you call child methods from a parent button click handler using the ref?
Summary
Controlling a child input from a parent uses two different patterns depending on what is being controlled. State-driven behavior like disabled, value, or placeholder is declarative: the parent passes a prop and the child renders according to that prop. This is idiomatic React and requires no special APIs.
Imperative actions like focus() and select() have no prop equivalent and require refs. forwardRef wraps the child component to accept a ref argument alongside props, forwarding it to an internal DOM element. useImperativeHandle is the better approach for shared components: instead of exposing the raw DOM node, the child defines exactly which methods the parent can call, creating a stable and intentional contract.
In React 19, ref becomes a regular prop and forwardRef is no longer needed. Until then, the forwardRef + useImperativeHandle combination is the production pattern for any component that exposes imperative DOM operations to its parent.
When should I use forwardRef vs passing a prop?
Use a prop for disabled and other state-driven behavior (declarative). Use forwardRef and useImperativeHandle for imperative DOM methods like focus(), scroll(), or select() that have no prop equivalent.
Advertisement
Stay Updated
Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.
Advertisement