Skip to main content

useSearchForm

The useSearchForm hook manages search form state, user interactions, and synchronization with URL parameters. It implements debouncing for text input and manages refs for form elements.

Location

/src/hooks/useSearchForm.js

Function Signature

export const useSearchForm = ({
  onTextFilter,
  onSearch,
  idTechnology,
  idLocation,
  idExperienceLevel,
  idText
}) => {
  // Returns form handlers and refs
}

Parameters

The hook accepts a configuration object:
ParameterTypeDescription
onTextFilterfunctionCallback when text input changes (debounced)
onSearchfunctionCallback when filters change
idTechnologystringUnique ID for technology select
idLocationstringUnique ID for location select
idExperienceLevelstringUnique ID for experience level select
idTextstringUnique ID for text input

Return Value

Returns an object with handlers and refs:
PropertyTypeDescription
searchTextstringCurrent search text state
handleSubmitfunctionForm submission handler
handleTextChangefunctionText input change handler (debounced)
handleClearFiltersfunctionClear all filters handler
inputRefRefObjectRef for text input element
selectRefTechnologyRefObjectRef for technology select
selectRefLocationRefObjectRef for location select
selectRefExperienceLevelRefObjectRef for experience level select

Usage Example

From src/components/SearchFormSection/SearchFormSection.jsx:
import { useId } from "react"
import { useSearchForm } from "@/hooks/useSearchForm"

export function SearchFormSection({ onTextFilter, onSearch, initialTextInput }) {
  // Generate unique IDs for form elements
  const idText = useId()
  const idTechnology = useId()
  const idLocation = useId()
  const idExperienceLevel = useId()

  const { 
    handleSubmit, 
    handleTextChange, 
    handleClearFilters, 
    inputRef, 
    selectRefTechnology, 
    selectRefLocation, 
    selectRefExperienceLevel 
  } = useSearchForm({ 
    onTextFilter, 
    onSearch, 
    idTechnology, 
    idLocation, 
    idExperienceLevel, 
    idText 
  })

  return (
    <form onChange={handleSubmit} id="empleos-search-form" role="search">
      <div className="search-bar">
        <input
          name={idText}
          id="empleos-search-input"
          type="text"
          ref={inputRef}
          defaultValue={initialTextInput}
          placeholder="Buscar trabajos, empresas o habilidades"
          onChange={handleTextChange}
        />
        <button 
          onClick={handleClearFilters} 
          type="button" 
          aria-label="Clear search input"
        >
          x
        </button>
      </div>

      <div className="search-filters">
        <select name={idTechnology} ref={selectRefTechnology}>
          <option value="">Tecnología</option>
          <option value="javascript">JavaScript</option>
          <option value="python">Python</option>
          <option value="react">React</option>
        </select>

        <select name={idLocation} ref={selectRefLocation}>
          <option value="">Ubicación</option>
          <option value="remoto">Remoto</option>
          <option value="cdmx">Ciudad de México</option>
        </select>

        <select name={idExperienceLevel} ref={selectRefExperienceLevel}>
          <option value="">Nivel de experiencia</option>
          <option value="junior">Junior</option>
          <option value="senior">Senior</option>
        </select>
      </div>
    </form>
  )
}

Handler Functions

handleSubmit(event)

Handles form submission and filter changes. Behavior:
  • Prevents default form submission
  • Extracts form data using FormData
  • Ignores text input changes (handled separately)
  • Calls onSearch with filter values
Implementation:
const handleSubmit = (event) => {
  event.preventDefault()
  
  const formData = new FormData(event.currentTarget)

  // Skip if triggered by text input
  if (event.target.name === idText) {
    return
  }

  const filters = {
    technology: formData.get(idTechnology),
    location: formData.get(idLocation),
    experienceLevel: formData.get(idExperienceLevel)
  }

  onSearch(filters)
}

handleTextChange(event)

Handles text input changes with 500ms debouncing. Behavior:
  • Updates local searchText state immediately
  • Cancels previous timeout if exists
  • Calls onTextFilter after 500ms of inactivity
Implementation:
const handleTextChange = (event) => {
  const text = event.target.value
  setSearchText(text)

  // Cancel previous timeout
  if (timeoutId.current) {
    clearTimeout(timeoutId.current)
  }

  // Set new timeout
  timeoutId.current = setTimeout(() => {
    onTextFilter(text)
  }, 500)
}
Why Debouncing? Debouncing prevents excessive API calls while typing:
User types: "r" "e" "a" "c" "t"
Without debounce: 5 API calls
With 500ms debounce: 1 API call (after user stops typing)

handleClearFilters(event)

Resets all filters and form inputs. Behavior:
  • Prevents default button behavior
  • Calls onTextFilter with empty string
  • Calls onSearch with empty filters
  • Resets all form element values via refs
  • Clears local searchText state
Implementation:
const handleClearFilters = (event) => {
  event.preventDefault()
  
  onTextFilter("")
  onSearch({
    technology: '',
    location: '',
    experienceLevel: ''
  })
  
  inputRef.current.value = ""
  selectRefTechnology.current.value = ""
  selectRefLocation.current.value = ""
  selectRefExperienceLevel.current.value = ""
  setSearchText("")
}

URL Synchronization

The hook synchronizes form element values with URL parameters:
useEffect(() => {
  const tech = searchParams.get('technology') || ''
  const location = searchParams.get('type') || ''
  const level = searchParams.get('level') || ''

  if (selectRefTechnology.current) {
    selectRefTechnology.current.value = tech
  }
  if (selectRefLocation.current) {
    selectRefLocation.current.value = location
  }
  if (selectRefExperienceLevel.current) {
    selectRefExperienceLevel.current.value = level
  }
}, [searchParams])
Effect:
  • On mount: Sets form values from URL
  • On URL change: Updates form to match URL
  • Enables browser back/forward navigation

Ref Management

The hook creates refs for all form elements:
const inputRef = useRef(null)
const selectRefTechnology = useRef(null)
const selectRefLocation = useRef(null)
const selectRefExperienceLevel = useRef(null)
Why Refs?
  • Direct DOM manipulation for clearing values
  • Synchronizing with URL without re-renders
  • Accessing form elements imperatively

State Management

Search Text State

const [searchText, setSearchText] = useState("")
Tracks the current text input value for immediate UI feedback.

Timeout Ref

const timeoutId = useRef(null)
Stores the debounce timeout ID for cleanup.

Best Practices

1. Use Unique IDs

Generate unique IDs with React’s useId:
import { useId } from "react"

const idTechnology = useId()
const idLocation = useId()
This prevents ID conflicts when multiple forms exist.

2. Debounce Text Input

Always debounce search inputs to reduce API calls:
// User types: "react"
// Without debounce: 5 API calls (r, re, rea, reac, react)
// With 500ms debounce: 1 API call (react)

3. Separate Text and Filter Handlers

Use different callbacks for text and select filters:
useSearchForm({
  onTextFilter: handleTextFilter,  // Debounced
  onSearch: handleSearch,            // Immediate
  // ...
})

4. Clear All State Together

When clearing, reset all related state:
// Clear callbacks
onTextFilter("")
onSearch({ technology: '', location: '', experienceLevel: '' })

// Clear DOM
inputRef.current.value = ""

// Clear local state
setSearchText("")

Common Patterns

Integration with useFilters

function SearchPage() {
  const { handleSearch, handleTextFilter, textToFilter } = useFilters()
  
  return (
    <SearchFormSection 
      initialTextInput={textToFilter}
      onSearch={handleSearch}
      onTextFilter={handleTextFilter}
    />
  )
}

Controlled vs Uncontrolled

This hook uses a hybrid approach:
  • Uncontrolled: Uses defaultValue and refs for form elements
  • Controlled: Uses searchText state for display/logic
<input
  ref={inputRef}
  defaultValue={initialTextInput}  // Uncontrolled
  onChange={handleTextChange}       // Updates controlled state
/>

Form Change Events

The form uses onChange on the <form> element:
<form onChange={handleSubmit}>
  <select name={idTechnology}>
    {/* options */}
  </select>
</form>
This triggers handleSubmit whenever any select changes.

Debouncing Implementation

The debounce pattern:
const timeoutId = useRef(null)

const handleTextChange = (event) => {
  const text = event.target.value
  setSearchText(text)  // Immediate UI update

  // Cancel previous timeout
  if (timeoutId.current) {
    clearTimeout(timeoutId.current)
  }

  // Set new timeout
  timeoutId.current = setTimeout(() => {
    onTextFilter(text)  // Delayed API call
  }, 500)
}
Timeline:
Time:   0ms   100ms  200ms  300ms  400ms  500ms  600ms  1000ms
User:    r      e      a      c      t     [wait]
State:   r      e      a      c      t
API:                                              onTextFilter("react")
  • useFilters - Provides the callbacks this hook uses
  • useRouter - Alternative for simple navigation forms

Dependencies

  • react: useEffect, useRef, useState, useId
  • react-router-dom: useSearchParams

Build docs developers (and LLMs) love