Uncontrolled SearchInput: Solving the Focus Loss Problem in React

Uncontrolled SearchInput: Solving the Focus Loss Problem in React

Ihor (Harry) Chyshkala
Ihor (Harry) ChyshkalaAuthor
|5 min read

Audio Narration

Uncontrolled SearchInput: Solving the Focus Loss Problem in React

0:000:00
AI-generated audio narration

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

javascript(17 lines)
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 text

What 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.

jsx
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:

text
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.

jsx
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:

text
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 STAYS

Comparison 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)

jsx(26 lines)
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 request

Step-by-step breakdown of what happened:

text(20 lines)
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 again

Main 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.

jsx(63 lines)
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?

text(25 lines)
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 timer

The Three "Magic" Ingredients

1. React.memo() - Component Memoization

jsx
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!

text
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 preserved

2. useRef() - Stable References

jsx
1const inputRef = useRef(null);        // Reference to DOM element
2const timeoutRef = useRef(null);      // Store timer
3const onChangeRef = useRef(null);     // Store callback function

What it does: useRef creates an object that DOESN'T change between re-renders.

Key property: Changing .current does NOT trigger a re-render!

jsx
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

jsx
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:

jsx
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 lost

Custom 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

typescript(72 lines)
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 parent

Usage in Parent Component

typescript(46 lines)
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

About the Author

Ihor (Harry) Chyshkala

Ihor (Harry) Chyshkala

Code Alchemist: Transmuting Ideas into Reality with JS & PHP. DevOps Wizard: Transforming Infrastructure into Cloud Gold | Orchestrating CI/CD Magic | Crafting Automation Elixirs