Understanding the Problem
In this final phase, we'll refactor the Autocomplete component from a class component to a function component. This is the most complex component in our application and presents several new challenges:
- It uses
componentDidUpdateto add and remove event listeners based on state changes - It uses
componentWillUnmountto clean up event listeners when the component is removed - It uses a ref to access the input element directly
- It uses the React Transition Group library for animations
This refactoring will require us to understand how to use useEffect with dependencies to replicate componentDidUpdate behavior, how to use useRef instead of createRef, and how to integrate all of these concepts while maintaining the component's functionality.
Experimenting with Lifecycle Methods
Before diving into the refactoring, let's examine the behavior of componentDidUpdate and componentWillUnmount in the original component to better understand what we need to replicate.
The Autocomplete component uses these lifecycle methods to manage event listeners:
componentDidUpdateadds an event listener to the document when the dropdown is shown (showListbecomestrue) and removes it when the dropdown is hidden (showListbecomesfalse)componentWillUnmountremoves the event listener when the component is unmounted
Both methods include console.log statements that make it easy to observe their behavior:
componentDidUpdate() {
if (this.state.showList) {
document.addEventListener('click', this.handleOutsideClick);
} else {
console.log("Removing Autocomplete listener on update!");
document.removeEventListener('click', this.handleOutsideClick);
}
}
componentWillUnmount () {
console.log("Cleaning up event listener from Autocomplete!");
document.removeEventListener('click', this.handleOutsideClick);
}
By observing these console messages, we can see when each method runs:
- When the dropdown is closed, the "Removing Autocomplete listener on update!" message appears, indicating that
componentDidUpdateis removing the listener - When the component is unmounted, the "Cleaning up event listener from Autocomplete!" message appears, indicating that
componentWillUnmountis running
This understanding will help us correctly implement the equivalent behavior with useEffect.
George Polya's Problem-Solving Method
Step 1: Understand the Problem
Let's examine the original Autocomplete class component to understand its functionality:
// Autocomplete.jsx
import React from 'react';
import { CSSTransition, TransitionGroup } from 'react-transition-group';
class Autocomplete extends React.Component {
constructor(props) {
super(props);
this.state = {
inputVal: '',
showList: false
};
this.inputRef = React.createRef();
}
componentDidUpdate() {
if (this.state.showList) {
document.addEventListener('click', this.handleOutsideClick);
} else {
console.log("Removing Autocomplete listener on update!");
document.removeEventListener('click', this.handleOutsideClick);
}
}
componentWillUnmount () {
console.log("Cleaning up event listener from Autocomplete!");
document.removeEventListener('click', this.handleOutsideClick);
}
handleInput = (e) => {
this.setState({ inputVal: e.target.value });
}
selectName = e => {
e.stopPropagation();
this.setState({ inputVal: e.target.innerHTML, showList: false });
}
handleOutsideClick = () => {
// Leave dropdown visible as long as input is focused
if (document.activeElement === this.inputRef.current) return;
else this.setState({ showList: false });
}
matches = () => {
const { inputVal } = this.state;
const { names } = this.props;
const inputLength = inputVal.length;
const matches = [];
if (inputLength === 0) return names;
names.forEach(name => {
const nameSegment = name.slice(0, inputLength);
if (nameSegment.toLowerCase() === inputVal.toLowerCase()) {
matches.push(name);
}
});
if (matches.length === 0) matches.push('No matches');
return matches;
}
render() {
const results = this.matches().map((result) => {
const nodeRef = React.createRef();
return (
<CSSTransition
nodeRef={nodeRef}
key={result}
classNames="result"
timeout={{ enter: 500, exit: 300 }}
>
<li ref={nodeRef} className="nameLi" onClick={this.selectName}>
{result}
</li>
</CSSTransition>
)
});
return (
<section className="autocomplete-section">
<h1>Autocomplete</h1>
<div className="auto">
<input
placeholder="Search..."
ref={this.inputRef}
onChange={this.handleInput}
value={this.state.inputVal}
onFocus={() => this.setState({ showList: true })}
/>
{this.state.showList && (
<ul className="auto-dropdown">
<TransitionGroup>
{results}
</TransitionGroup>
</ul>
)}
</div>
</section>
);
}
}
export default Autocomplete;
Analyzing this code, we can see that:
- State: The component has two state variables:
inputVal: The value entered in the input fieldshowList: Whether the dropdown list is visible
- Ref: It uses
this.inputRefto reference the input element - Lifecycle Methods:
componentDidUpdate: Adds or removes a document click listener based onshowListcomponentWillUnmount: Removes the document click listener when the component unmounts
- Event Handlers:
handleInput: UpdatesinputValwhen the input changesselectName: UpdatesinputValand hides the dropdown when an option is selectedhandleOutsideClick: Closes the dropdown when the user clicks outside (unless the input is focused)
- Utility Method:
matches: Filters the names array based on the input value
- Render:
- Creates a list of
CSSTransitioncomponents for each matching name - Each transition item has its own
nodeRefcreated withReact.createRef() - Renders an input field with the
inputRefand event handlers - Conditionally renders the dropdown list based on
showList
- Creates a list of
Step 2: Devise a Plan
- Convert the class declaration to a function declaration
- Use props destructuring in the function parameters
- Replace
this.statewith multipleuseStatehooks - Replace
this.inputRefwithuseRef - Use
useEffectwithshowListas a dependency to replicatecomponentDidUpdatebehavior - Return a cleanup function from
useEffectto replicatecomponentWillUnmountbehavior - Convert instance methods to regular functions
- Update references to
this.state,this.props, and class methods - Update the render logic, removing the
rendermethod and returning JSX directly
Step 3: Carry Out the Plan
1. Component Declaration, Props, and State
Let's start by converting the component declaration, setting up props and state:
// From
class Autocomplete extends React.Component {
constructor(props) {
super(props);
this.state = {
inputVal: '',
showList: false
};
this.inputRef = React.createRef();
}
// ...
}
// To
function Autocomplete({ names }) {
// Replace this.state with useState hooks
const [inputVal, setInputVal] = useState('');
const [showList, setShowList] = useState(false);
// Replace this.inputRef with useRef
const inputRef = useRef(null);
// ...
}
Don't forget to import the necessary hooks:
import React, { useState, useEffect, useRef } from 'react';
2. Converting Event Handlers
Next, let's convert the event handlers to regular functions:
// From
handleInput = (e) => {
this.setState({ inputVal: e.target.value });
}
selectName = e => {
e.stopPropagation();
this.setState({ inputVal: e.target.innerHTML, showList: false });
}
handleOutsideClick = () => {
// Leave dropdown visible as long as input is focused
if (document.activeElement === this.inputRef.current) return;
else this.setState({ showList: false });
}
// To
const handleInput = (e) => {
setInputVal(e.target.value);
};
const selectName = (e) => {
e.stopPropagation();
setInputVal(e.target.innerHTML);
setShowList(false);
};
const handleOutsideClick = () => {
// Leave dropdown visible as long as input is focused
if (document.activeElement === inputRef.current) return;
else setShowList(false);
};
3. Converting the Matches Method
Now let's convert the matches method:
// From
matches = () => {
const { inputVal } = this.state;
const { names } = this.props;
const inputLength = inputVal.length;
const matches = [];
if (inputLength === 0) return names;
names.forEach(name => {
const nameSegment = name.slice(0, inputLength);
if (nameSegment.toLowerCase() === inputVal.toLowerCase()) {
matches.push(name);
}
});
if (matches.length === 0) matches.push('No matches');
return matches;
}
// To
const matches = () => {
const inputLength = inputVal.length;
const matchesArray = [];
if (inputLength === 0) return names;
names.forEach(name => {
const nameSegment = name.slice(0, inputLength);
if (nameSegment.toLowerCase() === inputVal.toLowerCase()) {
matchesArray.push(name);
}
});
if (matchesArray.length === 0) matchesArray.push('No matches');
return matchesArray;
};
4. Converting Lifecycle Methods with useEffect
Now for the most complex part: converting componentDidUpdate and componentWillUnmount to a single useEffect hook:
// From
componentDidUpdate() {
if (this.state.showList) {
document.addEventListener('click', this.handleOutsideClick);
} else {
console.log("Removing Autocomplete listener on update!");
document.removeEventListener('click', this.handleOutsideClick);
}
}
componentWillUnmount () {
console.log("Cleaning up event listener from Autocomplete!");
document.removeEventListener('click', this.handleOutsideClick);
}
// To
useEffect(() => {
// This effect runs after the initial render and after any render
// where showList changes
if (showList) {
document.addEventListener('click', handleOutsideClick);
}
// Return a cleanup function that runs before the effect runs again
// or when the component unmounts
return () => {
console.log("Cleaning up event listener from Autocomplete!");
document.removeEventListener('click', handleOutsideClick);
};
}, [showList]); // Dependency array includes showList
Notice several important differences:
- We include
showListin the dependency array, so the effect runs whenevershowListchanges - We've simplified the logic by only adding the event listener when
showLististrue - We're not explicitly removing the listener when
showListbecomesfalsebecause the cleanup function will do that automatically before the effect runs again - The cleanup function also handles the
componentWillUnmountcase, as React will run it when the component unmounts
5. Updating the Render Logic
Finally, let's update the render logic to use our state variables and functions directly:
// Map over matches to create result items
const results = matches().map((result) => {
const nodeRef = React.createRef();
return (
<CSSTransition
nodeRef={nodeRef}
key={result}
classNames="result"
timeout={{ enter: 500, exit: 300 }}
>
<li ref={nodeRef} className="nameLi" onClick={selectName}>
{result}
</li>
</CSSTransition>
)
});
// Return what was previously in the render method
return (
<section className="autocomplete-section">
<h1>Autocomplete</h1>
<div className="auto">
<input
placeholder="Search..."
ref={inputRef}
onChange={handleInput}
value={inputVal}
onFocus={() => setShowList(true)}
/>
{showList && (
<ul className="auto-dropdown">
<TransitionGroup>
{results}
</TransitionGroup>
</ul>
)}
</div>
</section>
);
The main changes are:
- We changed
this.state.inputValtoinputVal - We changed
this.state.showListtoshowList - We changed
this.inputReftoinputRef - We changed
this.handleInputtohandleInput - We changed
this.selectNametoselectName - We changed
this.setState({ showList: true })tosetShowList(true)
6. Putting It All Together
Here's the complete refactored component:
// Autocomplete.jsx (Refactored)
import React, { useState, useEffect, useRef } from 'react';
import { CSSTransition, TransitionGroup } from 'react-transition-group';
function Autocomplete({ names }) {
// Replace this.state with useState hooks
const [inputVal, setInputVal] = useState('');
const [showList, setShowList] = useState(false);
// Replace this.inputRef with useRef
const inputRef = useRef(null);
// Replace componentDidUpdate and componentWillUnmount with useEffect
useEffect(() => {
// This function runs when showList changes
if (showList) {
document.addEventListener('click', handleOutsideClick);
}
// Return a cleanup function that runs before the next effect
// or when component unmounts
return () => {
console.log("Cleaning up event listener from Autocomplete!");
document.removeEventListener('click', handleOutsideClick);
};
}, [showList]); // Run this effect when showList changes
// Convert class methods to regular functions
const handleInput = (e) => {
setInputVal(e.target.value);
};
const selectName = (e) => {
e.stopPropagation();
setInputVal(e.target.innerHTML);
setShowList(false);
};
const handleOutsideClick = () => {
// Leave dropdown visible as long as input is focused
if (document.activeElement === inputRef.current) return;
else setShowList(false);
};
const matches = () => {
const inputLength = inputVal.length;
const matchesArray = [];
if (inputLength === 0) return names;
names.forEach(name => {
const nameSegment = name.slice(0, inputLength);
if (nameSegment.toLowerCase() === inputVal.toLowerCase()) {
matchesArray.push(name);
}
});
if (matchesArray.length === 0) matchesArray.push('No matches');
return matchesArray;
};
// Map over matches to create result items
const results = matches().map((result) => {
const nodeRef = React.createRef();
return (
<CSSTransition
nodeRef={nodeRef}
key={result}
classNames="result"
timeout={{ enter: 500, exit: 300 }}
>
<li ref={nodeRef} className="nameLi" onClick={selectName}>
{result}
</li>
</CSSTransition>
)
});
// Return what was previously in the render method
return (
<section className="autocomplete-section">
<h1>Autocomplete</h1>
<div className="auto">
<input
placeholder="Search..."
ref={inputRef}
onChange={handleInput}
value={inputVal}
onFocus={() => setShowList(true)}
/>
{showList && (
<ul className="auto-dropdown">
<TransitionGroup>
{results}
</TransitionGroup>
</ul>
)}
</div>
</section>
);
}
export default Autocomplete;
Step 4: Look Back and Review
Let's verify that our refactored component maintains the same functionality as the original:
- State is properly initialized (
inputValto''andshowListtofalse) - The input ref is created with
useRefinstead ofcreateRef - Event listeners are added when the dropdown is shown and removed when it's hidden or when the component unmounts
- The input field updates
inputValas the user types - The dropdown is shown when the input is focused
- Clicking on a name in the dropdown updates the input value and hides the dropdown
- Clicking outside the component hides the dropdown (unless the input is focused)
- The
matchesfunction correctly filters the names based on the input value
Let's run the tests to confirm that everything works as expected:
npm test
The tests for the Autocomplete component should pass, confirming that our refactoring was successful.
Understanding useRef
What is useRef?
The useRef hook is a function that returns a mutable ref object whose .current property is initialized to the passed argument. The returned object will persist for the full lifetime of the component.
const refContainer = useRef(initialValue);
There are two main uses for useRef:
- Accessing DOM elements: When you need to interact with a DOM element directly, you can use a ref to reference it
- Storing mutable values: When you need to keep track of a value that shouldn't trigger a re-render when it changes
useRef vs createRef
In React, there are two ways to create refs: useRef and createRef. The key difference is that useRef returns the same ref object on every render, while createRef creates a new ref on every render.
| createRef | useRef |
|---|---|
| Returns a new ref object on every render | Returns the same ref object on every render |
| Used in class components | Used in function components |
| Created once in constructor or as instance property | Created with the useRef hook |
In our Autocomplete component, we used useRef for the input element because we need the same ref to persist across renders. However, we continued to use createRef inside the map function because each list item needs its own ref, and these refs are created dynamically.
Using useRef for DOM Elements
To use useRef to access a DOM element, you assign the ref to the ref attribute of a JSX element:
function TextInputWithFocusButton() {
const inputRef = useRef(null);
const focusInput = () => {
// Accessing the DOM element via .current
inputRef.current.focus();
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Focus the input</button>
</div>
);
}
In this example, inputRef.current points to the actual DOM element, allowing us to call methods like focus() on it.
Using useRef for Mutable Values
useRef can also be used to hold any mutable value. Unlike state, updating a ref's .current property doesn't cause a re-render:
function Timer() {
const [count, setCount] = useState(0);
const intervalIdRef = useRef(null);
const startTimer = () => {
if (intervalIdRef.current) return;
intervalIdRef.current = setInterval(() => {
setCount(c => c + 1);
}, 1000);
};
const stopTimer = () => {
clearInterval(intervalIdRef.current);
intervalIdRef.current = null;
};
// Clean up on unmount
useEffect(() => {
return () => {
if (intervalIdRef.current) {
clearInterval(intervalIdRef.current);
}
};
}, []);
return (
<div>
Count: {count}
<button onClick={startTimer}>Start</button>
<button onClick={stopTimer}>Stop</button>
</div>
);
}
In this example, intervalIdRef is used to store the interval ID without triggering re-renders when it changes.
useEffect with Dependencies
Understanding the Dependency Array
In our refactored Autocomplete component, we used useEffect with [showList] as the dependency array. This tells React to run the effect after the initial render and after any render where showList has changed.
The dependency array is a powerful feature of useEffect that allows you to control when the effect runs:
- Empty array ([]): Run the effect only after the initial render (like
componentDidMount) - With dependencies ([dep1, dep2]): Run the effect after the initial render and after any render where any dependency has changed
- No array: Run the effect after every render
The Cleanup Function
When useEffect runs again due to a dependency change, React first runs the cleanup function from the previous effect run, then runs the new effect. This ensures that resources are properly managed and prevents memory leaks.
In our component, this means:
- When
showListchanges fromfalsetotrue:- The cleanup function runs (removing the event listener, although none was added yet)
- The effect runs, adding the event listener
- When
showListchanges fromtruetofalse:- The cleanup function runs (removing the event listener)
- The effect runs, but doesn't add an event listener since
showListis nowfalse
- When the component unmounts:
- The cleanup function runs (removing any event listener that was added)
Dependency Array Gotchas
When working with dependency arrays, be aware of these common issues:
Missing Dependencies
If you use a variable inside your effect but don't include it in the dependency array, you risk working with stale values. React's linter will usually warn you about this:
function Counter() {
const [count, setCount] = useState(0);
// ⚠️ Missing dependency: count
useEffect(() => {
const id = setInterval(() => {
console.log(count);
// This will always log the initial value of count (0)
// because the effect closure captures the initial value
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []); // Missing count in dependency array
return <div>Count: {count}</div>;
}
Function Dependencies
When using functions inside useEffect, you need to include them in the dependency array if they use any state or props:
function SearchResults({ query }) {
const [results, setResults] = useState([]);
// This function depends on the query prop
const fetchResults = async () => {
const response = await fetch(`https://api.example.com/search?q=${query}`);
const data = await response.json();
setResults(data);
};
// ⚠️ fetchResults should be included in the dependency array
useEffect(() => {
fetchResults();
}, [query]); // Should include fetchResults
// ...
}
To solve this issue, you can either:
- Move the function inside the effect
- Use the
useCallbackhook to memoize the function
Differences from Lifecycle Methods
Our refactoring reveals important differences between how class component lifecycle methods and function component hooks work:
| Class Lifecycle Methods | Function Component with useEffect |
|---|---|
componentDidMount runs once after the initial render |
useEffect(() => {}, []) runs once after the initial render |
componentDidUpdate runs after every render except the first |
useEffect(() => {}, [deps]) runs after the initial render and whenever dependencies change |
componentWillUnmount runs once before unmount |
Cleanup function returned from useEffect runs before unmount and before every re-run of the effect |
| Need explicit conditions to control when code runs | Control when code runs with dependency arrays |
| Setup and cleanup in separate methods | Setup and cleanup together in one effect |
In our Autocomplete component, we can see these differences clearly:
- In the class version, we needed to explicitly check
if (this.state.showList)incomponentDidUpdate - In the function version, we control when the effect runs with the
[showList]dependency array - In the class version, we needed to duplicate the
removeEventListenercode in bothcomponentDidUpdateandcomponentWillUnmount - In the function version, a single cleanup function handles both cases
Bonus: TransitionItem Component
The createRef Problem
In our refactored Autocomplete component, we're still using React.createRef() inside the map function:
const results = matches().map((result) => {
const nodeRef = React.createRef();
return (
<CSSTransition
nodeRef={nodeRef}
key={result}
classNames="result"
timeout={{ enter: 500, exit: 300 }}
>
<li ref={nodeRef} className="nameLi" onClick={selectName}>
{result}
</li>
</CSSTransition>
)
});
This is problematic because createRef creates a new ref object on every render, which means that the refs will be different each time the component renders. While this may work in simple cases, it can lead to subtle bugs and performance issues.
The README bonus phase suggests creating a separate TransitionItem component that uses useRef instead of createRef. Let's implement this improvement:
Creating a TransitionItem Component
We'll create a separate component for each list item that uses useRef at the top level:
// Create a separate component for each list item
function TransitionItem({ result, onClick, ...props }) {
// Use useRef at the top level of the component
const nodeRef = useRef(null);
return (
<CSSTransition
nodeRef={nodeRef}
classNames="result"
timeout={{ enter: 500, exit: 300 }}
{...props} // Pass through any props from TransitionGroup
>
<li ref={nodeRef} className="nameLi" onClick={onClick}>
{result}
</li>
</CSSTransition>
);
}
This component:
- Uses
useRefat the top level, which ensures the same ref object is used for each render of this specific item - Takes
resultandonClickas props, along with any additional props using the rest operator (...props) - Passes those additional props to
CSSTransitionusing the spread operator
Using the TransitionItem Component
Now we can update our results mapping to use the new component:
// Use our TransitionItem component instead of inline CSSTransition
const results = matches().map((result) => (
<TransitionItem
key={result}
result={result}
onClick={selectName}
/>
));
This approach is much cleaner and more efficient:
- Each
TransitionItemhas its own persistent ref created withuseRef - The mapping function is simpler and more readable
- The component structure is cleaner, with better separation of concerns
Complete Refactored Component with TransitionItem
Here's the complete refactored Autocomplete component with the TransitionItem improvement:
// Autocomplete.jsx with TransitionItem
import React, { useState, useEffect, useRef } from 'react';
import { CSSTransition, TransitionGroup } from 'react-transition-group';
// Create a separate component for each list item
function TransitionItem({ result, onClick, ...props }) {
// Use useRef at the top level of the component
const nodeRef = useRef(null);
return (
<CSSTransition
nodeRef={nodeRef}
classNames="result"
timeout={{ enter: 500, exit: 300 }}
{...props} // Pass through any props from TransitionGroup
>
<li ref={nodeRef} className="nameLi" onClick={onClick}>
{result}
</li>
</CSSTransition>
);
}
function Autocomplete({ names }) {
const [inputVal, setInputVal] = useState('');
const [showList, setShowList] = useState(false);
const inputRef = useRef(null);
useEffect(() => {
if (showList) {
document.addEventListener('click', handleOutsideClick);
}
return () => {
console.log("Cleaning up event listener from Autocomplete!");
document.removeEventListener('click', handleOutsideClick);
};
}, [showList]);
const handleInput = (e) => {
setInputVal(e.target.value);
};
const selectName = (e) => {
e.stopPropagation();
setInputVal(e.target.innerHTML);
setShowList(false);
};
const handleOutsideClick = () => {
if (document.activeElement === inputRef.current) return;
else setShowList(false);
};
const matches = () => {
const inputLength = inputVal.length;
const matchesArray = [];
if (inputLength === 0) return names;
names.forEach(name => {
const nameSegment = name.slice(0, inputLength);
if (nameSegment.toLowerCase() === inputVal.toLowerCase()) {
matchesArray.push(name);
}
});
if (matchesArray.length === 0) matchesArray.push('No matches');
return matchesArray;
};
// Use our TransitionItem component instead of inline CSSTransition
const results = matches().map((result) => (
<TransitionItem
key={result}
result={result}
onClick={selectName}
/>
));
return (
<section className="autocomplete-section">
<h1>Autocomplete</h1>
<div className="auto">
<input
placeholder="Search..."
ref={inputRef}
onChange={handleInput}
value={inputVal}
onFocus={() => setShowList(true)}
/>
{showList && (
<ul className="auto-dropdown">
<TransitionGroup>
{results}
</TransitionGroup>
</ul>
)}
</div>
</section>
);
}
export default Autocomplete;
Key Insights from This Refactoring
Understanding the Mental Model
One of the most important insights from refactoring the Autocomplete component is how the mental model for function components differs from class components:
- Class Components: Think in terms of object-oriented programming with lifecycle methods that run at specific points in the component's life cycle
- Function Components: Think in terms of state and effects that synchronize with that state as it changes
In the function component world:
- Effects (
useEffect) run after renders - Cleanup functions run before the next effect run or unmount
- Dependencies determine when effects run
Benefits of Function Components
This refactoring demonstrates several benefits of function components with hooks:
- Concise code: Function components are generally shorter and more focused
- Colocated logic: Related code (like effect setup and cleanup) stays together
- Flexible state management: State can be split into multiple variables that update independently
- Reusable logic: Logic can be extracted into custom hooks
- Avoiding this binding issues: No more confusion about
thiscontext - Better composition: It's easier to compose components and reuse logic
React Hooks Best Practices
From this refactoring, we can extract several best practices for working with React hooks:
- Split related state: Use multiple
useStatecalls for unrelated state - Be specific with dependencies: Include all values used in the effect in the dependency array
- Clean up resources: Always return a cleanup function when setting up resources that need to be released
- Prefer useRef over createRef in function components:
useRefpersists across renders - Create smaller, focused components: Extract reusable pieces like our
TransitionItem - Separate concerns: Use multiple
useEffecthooks for unrelated functionality - Think about synchronization:
useEffectis about synchronizing with external systems or state
Real-World Applications
When to Use This Pattern
The patterns we've seen in this Autocomplete component are common in many real-world applications:
- Search interfaces: Autocomplete, typeahead, and search suggestions
- Dropdown menus: Navigation menus, select components, and context menus
- Click-outside patterns: Modals, popovers, and tooltips that close when clicking outside
- Form validation: Validating input as the user types
- Animation and transitions: Adding and removing elements with animations
Extensions and Improvements
In a real-world application, you might extend this component in several ways:
- Keyboard navigation: Allow users to navigate the dropdown with arrow keys and select with Enter
- Async data fetching: Fetch suggestions from an API instead of filtering a local array
- Debouncing: Add a delay before filtering to avoid excessive computation during typing
- Highlighting matches: Highlight the matching portion of each suggestion
- Custom rendering: Allow customizing how each suggestion is rendered
- Accessibility: Implement ARIA attributes and keyboard interactions for better accessibility
Conclusion
Congratulations on completing the final phase of the refactoring project! You've successfully converted all the class components in the Widgets app to function components using React Hooks.
Let's recap what you've learned throughout this process:
- Phase 1 (App): Basic state management with
useState - Phase 2 (Folder): Working with props and component composition
- Phase 3 (Weather): Using
useEffectfor data fetching - Phase 4 (Clock): Setting up intervals and cleaning up resources
- Phase 5 (Autocomplete): Using
useRef, dependency arrays, and component extraction
You've gained a solid understanding of React Hooks and how they compare to lifecycle methods in class components. You've also learned important patterns and best practices for working with function components.
This knowledge will serve you well as you continue your journey with React. Function components with hooks are now the recommended approach for building React applications, and the patterns you've learned here are widely used in the React ecosystem.
Keep exploring and building with these tools!