From 573fabb95d6ae3a68900334310b1b26c8a982435 Mon Sep 17 00:00:00 2001 From: Stuti Gupta Date: Mon, 1 Jun 2026 08:16:39 +0530 Subject: [PATCH] Refactor IdleSearchField to functional component This PR converts `IdleSearchField` from a class component to a function component using React hooks. Changes: - Replaced lifecycle methods with `useEffect` - Migrated state handling to `useState` - Simplified event handlers - Preserved existing behavior No functional changes intended. --- src/components/shared/IdleSearchField.tsx | 217 ++++++++++++---------- 1 file changed, 121 insertions(+), 96 deletions(-) diff --git a/src/components/shared/IdleSearchField.tsx b/src/components/shared/IdleSearchField.tsx index f5207c3488..ac5a6de9dd 100644 --- a/src/components/shared/IdleSearchField.tsx +++ b/src/components/shared/IdleSearchField.tsx @@ -1,7 +1,14 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { PureComponent } from 'react'; + +import React, { + forwardRef, + useEffect, + useRef, + useState, +} from 'react'; + import classNames from 'classnames'; import { Localized } from '@fluent/react'; @@ -17,133 +24,151 @@ type Props = { readonly title: string | null; }; -type State = { - value: string; - previousDefaultValue: string; -}; +export const IdleSearchField = forwardRef( + ( + { + onIdleAfterChange, + onFocus, + onBlur, + idlePeriod, + defaultValue, + className, + title, + }, + forwardedRef + ) => { + const [value, setValue] = useState(defaultValue || ''); + + const timeoutRef = useRef(null); + + const previouslyNotifiedValue = useRef(value); + + const internalInputRef = useRef(null); + + // Sync forwarded ref with internal input ref + const setRefs = (input: HTMLInputElement | null) => { + internalInputRef.current = input; + + if (typeof forwardedRef === 'function') { + forwardedRef(input); + } else if (forwardedRef) { + forwardedRef.current = input; + } + }; -export class IdleSearchField extends PureComponent { - _timeout: NodeJS.Timeout | null = null; - _previouslyNotifiedValue: string; - _input: HTMLInputElement | null = null; - _takeInputRef = (input: HTMLInputElement | null) => (this._input = input); - - constructor(props: Props) { - super(props); - this.state = { - value: props.defaultValue || '', - previousDefaultValue: props.defaultValue || '', + // Sync state when defaultValue changes externally + useEffect(() => { + setValue(defaultValue || ''); + previouslyNotifiedValue.current = defaultValue || ''; + }, [defaultValue]); + + // Cleanup timeout on unmount + useEffect(() => { + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }; + }, []); + + const notifyIfChanged = (newValue: string) => { + if (newValue !== previouslyNotifiedValue.current) { + previouslyNotifiedValue.current = newValue; + onIdleAfterChange(newValue); + } }; - this._previouslyNotifiedValue = this.state.value; - } - _onSearchFieldFocus = (e: React.FocusEvent) => { - e.currentTarget.select(); - - if (this.props.onFocus) { - this.props.onFocus(); - } - }; - - _onSearchFieldBlur = (e: { relatedTarget: Element | null }) => { - if (this.props.onBlur) { - this.props.onBlur(e.relatedTarget); - } - }; - - _onSearchFieldChange = (e: React.ChangeEvent) => { - this.setState({ - value: e.currentTarget.value, - }); - - if (this._timeout) { - clearTimeout(this._timeout); - } - this._timeout = setTimeout(this._onTimeout, this.props.idlePeriod); - }; - - _onTimeout = () => { - this._timeout = null; - this._notifyIfChanged(this.state.value); - }; - - _notifyIfChanged(value: string) { - if (value !== this._previouslyNotifiedValue) { - this._previouslyNotifiedValue = value; - this.props.onIdleAfterChange(value); - } - } + const onTimeout = () => { + timeoutRef.current = null; + notifyIfChanged(value); + }; - _onClearButtonClick = () => { - if (this._input) { - this._input.focus(); - } + const onSearchFieldFocus = ( + e: React.FocusEvent + ) => { + e.currentTarget.select(); - if (this._timeout !== null) { - clearTimeout(this._timeout); - this._timeout = null; - } + if (onFocus) { + onFocus(); + } + }; - this.setState({ value: '' }); - this._notifyIfChanged(''); - }; + const onSearchFieldBlur = ( + e: React.FocusEvent + ) => { + if (onBlur) { + onBlur(e.relatedTarget); + } + }; - _onFormSubmit(e: React.FormEvent) { - e.preventDefault(); - } + const onSearchFieldChange = ( + e: React.ChangeEvent + ) => { + const newValue = e.currentTarget.value; - override componentDidUpdate(prevProps: Props) { - // When the defaultValue prop changes externally (e.g., from Redux), - // getDerivedStateFromProps will update the state value. We need to sync - // _previouslyNotifiedValue to match so that subsequent changes are detected - // correctly by _notifyIfChanged. - if (prevProps.defaultValue !== this.props.defaultValue) { - this._previouslyNotifiedValue = this.state.value; - } - } + setValue(newValue); - static getDerivedStateFromProps(props: Props, state: State) { - if (props.defaultValue !== state.previousDefaultValue) { - return { - previousDefaultValue: props.defaultValue || '', - value: props.defaultValue || '', - }; - } - return null; - } + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + + timeoutRef.current = setTimeout(onTimeout, idlePeriod); + }; + + const onClearButtonClick = () => { + if (internalInputRef.current) { + internalInputRef.current.focus(); + } + + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + + setValue(''); + notifyIfChanged(''); + }; + + const onFormSubmit = ( + e: React.FormEvent + ) => { + e.preventDefault(); + }; - override render() { - const { className, title } = this.props; return (
+
); } -} +); + +IdleSearchField.displayName = 'IdleSearchField'; +```