
Uncontrolled SearchInput: Solving the Focus Loss Problem in React
Audio Narration
Uncontrolled SearchInput: Solving the Focus Loss Problem in React
Uncontrolled SearchInput: A Detailed Explanation of Solving the Focus Loss Problem
Have you ever experienced the frustration of typing in a search field, only to have the cursor disappear after each keystroke? This article explains why this happens and how to fix it using uncontrolled components in React.
Table of Contents
1. The Problem: Focus Loss When Typing
2. React Rendering Cycle: How It Works
3. Controlled vs Uncontrolled Components
4. Why the Problem Occurred
5. How Uncontrolled Input Solved the Problem
6. Real-World Analogies
7. Technical Implementation
The Problem: Focus Loss When Typing
Symptoms
When a user tried to type in the search field:
1. Types the first letter - everything works fine
2. Tries to type the second letter - cursor disappears from the field
3. Has to click on the field again
4. Types the next letter - focus lost again
5. And so on, constantly...
Why is this a critical problem?
- Impossible to use the search normally
- Poor UX (user experience)
- Users get frustrated and abandon the feature
React Rendering Cycle: How It Works
React operates on the principle of reactivity: when state changes, React automatically re-renders the component.
Component Lifecycle
1// 1. INITIAL RENDER
2// - React creates the component
3// - Calls the component function
4// - Creates Virtual DOM
5// - Renders to the real DOM
6
7// 2. USER INTERACTION
8// - User types textWhat happens during a re-render?
IMPORTANT: During a re-render, React:
- PRESERVES refs (useRef)
- PRESERVES memoized values (useMemo, useCallback)
- DOES NOT preserve DOM state (focus, cursor position) if the component is recreated
- MAY LOSE focus if the element was "replaced" in the DOM
Controlled vs Uncontrolled Components
Controlled Component
Definition: A component where React controls the input value through state.
1function ControlledInput() {
2 const [value, setValue] = useState('');
3
4 return (
5 <input
6 value={value} // React controls the value
7 onChange={(e) => setValue(e.target.value)}
8 />
9 );
10}The controlled input cycle:
1User types 'a'
2 |
3onChange is called with event
4 |
5setValue('a') updates state
6 |
7COMPONENT RE-RENDERS
8 |
9React renders <input value="a" />
10 |
11DOM INPUT IS RECREATED OR UPDATED
12 |
13FOCUS MAY BE LOST (if input is in a parent that also re-renders)Uncontrolled Component
Definition: A component where the DOM itself stores and controls its value.
1function UncontrolledInput() {
2 const inputRef = useRef(null);
3
4 return (
5 <input
6 ref={inputRef} // Access to DOM element
7 onChange={(e) => {
8 // Handle changes, but DO NOT control value
9 console.log(e.target.value);
10 }}
11 // NO value prop!
12 />
13 );
14}The uncontrolled input cycle:
1User types 'a'
2 |
3DOM updates its internal value (native browser behavior)
4 |
5onChange is called with event
6 |
7We process the event (e.g., debounce)
8 |
9setState is NOT called for input value
10 |
11NO RE-RENDER HAPPENS
12 |
13FOCUS STAYSComparison Table
Controlled: React controls value via state, value prop required, always re-renders on change, may lose focus
Uncontrolled: DOM controls value natively, no value prop, no re-render (unless other state changes), focus preserved
Why the Problem Occurred
Old Implementation (Problematic)
1function Contacts() {
2 const [search, setSearch] = useState(''); // State for search
3 const [contacts, setContacts] = useState([]); // State for contacts
4 const [page, setPage] = useState(1); // State for pagination
5
6 // Load contacts
7 const fetchContacts = useCallback(() => {
8 // ... API requestStep-by-step breakdown of what happened:
11. User types 'a' in the search field
2 |
32. onChange calls setSearch('a')
4 |
53. THE ENTIRE Contacts COMPONENT RE-RENDERS
6 (because state changed)
7 |
84. React calls the Contacts() function againMain reasons for focus loss:
1. React reconciliation algorithm: When a parent component re-renders, React may "replace" child elements, even if they're "similar"
2. Key prop issues: If list elements don't have stable keys, React may confuse them
3. Conditional rendering: If there's conditional rendering around the input ({condition && <input />}), the element may be recreated
4. Parent component updates: When a parent component updates (via other state), all child components also update
How Uncontrolled Input Solved the Problem
New Implementation
Key idea: Extract SearchInput into a separate component and make it uncontrolled.
1// 1. Separate memoized component
2const SearchInput = React.memo(() => {
3 const inputRef = useRef(null);
4 const timeoutRef = useRef(null);
5 const onChangeRef = useRef(null);
6
7 // One-time initialization of event listeners
8 useEffect(() => {Why does this work?
11. User types 'a' in the search field
2 |
32. DOM updates the input value itself (native browser behavior)
4 FOCUS STAYS - this is a native operation
5 |
63. onChange is called with event
7 |
84. handleChange processes event and starts debounce timerThe Three "Magic" Ingredients
1. React.memo() - Component Memoization
1const SearchInput = React.memo(() => {
2 // ...
3});What it does: Prevents re-rendering of a component if its props haven't changed.
In our case: SearchInput receives NO props, so it NEVER re-renders after the initial render!
1Parent component updates
2 |
3React checks: "Did SearchInput props change?"
4 |
5Answer: "No, props are empty {}"
6 |
7React SKIPS SearchInput re-render
8 |
9DOM element remains unchanged
10 |
11Focus is preserved2. useRef() - Stable References
1const inputRef = useRef(null); // Reference to DOM element
2const timeoutRef = useRef(null); // Store timer
3const onChangeRef = useRef(null); // Store callback functionWhat it does: useRef creates an object that DOESN'T change between re-renders.
Key property: Changing .current does NOT trigger a re-render!
1// This triggers a re-render:
2const [value, setValue] = useState('hello');
3setValue('world'); // Re-render
4
5// This does NOT trigger a re-render:
6const valueRef = useRef('hello');
7valueRef.current = 'world'; // No re-render!3. Custom Events - Communication Without Props
1// Parent component sends callback
2window.dispatchEvent(new CustomEvent('setSearchCallback', {
3 detail: { onChange: callback }
4}));
5
6// Child component receives callback
7window.addEventListener('setSearchCallback', (e) => {
8 onChangeRef.current = e.detail.onChange;
9});Why not props? If we passed callback through props:
1// BAD idea:
2<SearchInput onChange={handleSearch} />
3
4// Problem: every re-render of the parent component
5// creates a NEW handleSearch function
6// React.memo() sees "new" props
7// SearchInput re-renders
8// Focus is lostCustom events solve this: Callback is passed "outside" of React, props remain empty, React.memo() always returns false, SearchInput stays unchanged.
Real-World Analogies
Analogy 1: Phone Conversation
Controlled Input (bad): You tell a secretary what to write, they write a word, then ERASE everything, then REWRITE everything again including the new word. Too much work, constant interruptions.
Uncontrolled Input (good): The secretary just writes everything themselves. When needed, you ask "what's written there?" Fast, no interruptions, efficient.
Analogy 2: Ordering Food
Controlled Input: Waiter: "What will you have?" You: "Piz..." Waiter: "Wait! First I must write down 'Piz'..." Goes to write it down, comes back: "Continue!" You: "...za" Waiter: "Wait! Now I'll write 'Pizza'..." And so on for every letter!
Uncontrolled Input: You say everything at once: "Large Margherita pizza". The waiter writes it all themselves without interrupting. When you're done, they verify and go prepare. Natural and fast.
Technical Implementation
Complete SearchInput Component
1const SearchInput = React.memo(() => {
2 // Reference to the input DOM element
3 const inputRef = useRef<HTMLInputElement>(null);
4
5 // Reference to the debounce timer
6 const timeoutRef = useRef<NodeJS.Timeout | null>(null);
7
8 // Reference to callback function from parentUsage in Parent Component
1function Contacts() {
2 const [contacts, setContacts] = useState<Contact[]>([]);
3 const [search, setSearch] = useState('');
4 const [page, setPage] = useState(1);
5 const [filterParams, setFilterParams] = useState({...});
6
7 // Create stable reference to callback
8 const onChangeRef = useRef<((value: string) => void) | null>(null);Pros and Cons of Each Approach
Controlled Input
Advantages:
- Full control over the value
- Easy real-time validation
- Can format values on the fly
- Single source of truth
Disadvantages:
- Re-renders on every change
- May lose focus
- Slower for large forms
- Performance issues for complex components
When to use: Forms with validation, input masks (phone, credit card), input restrictions (numbers only), small forms
Uncontrolled Input
Advantages:
- Faster - fewer re-renders
- Focus always preserved
- Better performance for large lists
- Natural browser behavior
Disadvantages:
- Less control
- Harder to do real-time validation
- Need ref to access value
- Slightly more complex implementation
When to use: Search with debounce, large forms, inputs in frequently updating lists, performance-critical cases
Conclusion
Why did Uncontrolled Input solve the problem?
1. React.memo() prevents SearchInput component re-renders
2. Absence of value prop makes the input uncontrolled - DOM manages the value itself
3. useRef() stores stable references without triggering re-renders
4. Custom Events pass callbacks without props, preserving memoization
5. Debounce delays API calls, reducing request count
The Result
BEFORE: Type 'a' - Re-render - Focus lost
AFTER: Type 'a' - DOM updates - Focus preserved
Key Lesson
React is about trade-offs:
- Controlled = more control, but more re-renders
- Uncontrolled = less control, but better performance
Choose the right tool for the right job!
Additional Resources
- React Docs: Controlled Components - react.dev/learn/sharing-state-between-components
- React Docs: useRef - react.dev/reference/react/useRef
- React Docs: React.memo - react.dev/reference/react/memo
- React Docs: Preserving and Resetting State - react.dev/learn/preserving-and-resetting-state

