npm package: @rain-component-ui/ui-web
Description: A Search component for React and Next.js with built-in debounce, clear button, autocomplete suggestions, full style customisation, and production-grade error handling.
Latest version: 0.20.0

# @rain-component-ui/ui-web

A Search component for React and Next.js with built-in debounce, clear button, autocomplete suggestions, full style customisation, and production-grade error handling.

---

## Installation

```bash
npm install @rain-component-ui/ui-web @rain-component-ui/ui-core
```

**Peer dependencies:** React 17 or higher

---

## Quick Start

```tsx
import { useState } from 'react';
import { Search } from '@rain-component-ui/ui-web';

export default function App() {
  const [value, setValue] = useState('');

  return (
    <Search
      value={value}
      onChange={setValue}
      onSearch={(v) => console.log('Search:', v)}
      onClear={() => setValue('')}
      placeholder="Search..."
    />
  );
}
```

---

## Examples

### Basic search (submit on button click or Enter)

```tsx
<Search
  value={value}
  onChange={setValue}
  onSearch={(v) => fetchResults(v)}
  onClear={() => setValue('')}
  placeholder="Search..."
/>
```

### Debounced live search (fires automatically as you type)

```tsx
<Search
  value={value}
  onChange={setValue}
  onSearch={(v) => fetchResults(v)}
  onClear={() => setValue('')}
  debounceMs={400}
  placeholder="Type to search..."
/>
```

### Address / autocomplete search with dropdown suggestions

```tsx
const [value, setValue] = useState('');
const [suggestions, setSuggestions] = useState<string[]>([]);

async function fetchSuggestions(query: string) {
  if (!query) return setSuggestions([]);
  const results = await googlePlacesAutocomplete(query);
  setSuggestions(results.map(r => r.description));
}

<Search
  value={value}
  onChange={(v) => { setValue(v); fetchSuggestions(v); }}
  onSearch={(v) => searchOnMap(v)}
  onClear={() => { setValue(''); setSuggestions([]); }}
  onSuggestionSelect={(v) => { setValue(v); setSuggestions([]); }}
  suggestions={suggestions}
  debounceMs={300}
  placeholder="Search address..."
/>
```

### Custom styles

```tsx
<Search
  value={value}
  onChange={setValue}
  onSearch={handleSearch}
  onClear={() => setValue('')}
  inputStyle={{ borderColor: '#7c3aed', borderRadius: '24px', color: '#7c3aed' }}
  submitButtonStyle={{ background: 'rgba(124,58,237,0.07)', borderLeft: '1px solid #7c3aed', color: '#7c3aed' }}
  clearButtonStyle={{ color: '#ef4444', borderLeft: '1px solid #ef4444' }}
/>
```

### Error handling

The component has three layers of built-in error protection:

**Layer 1 — Callback errors are caught automatically**

If `onSearch`, `onChange`, or `onClear` throws, the component catches the error, logs it to the console, and keeps working. The user never sees a crash.

**Layer 2 — Render crashes show a fallback UI**

An `ErrorBoundary` wraps the component. If a catastrophic render error occurs, the user sees `⚠ Search unavailable` instead of a blank page. The rest of your app keeps working.

**Layer 3 — `onError` gives you full visibility**

Use `onError` to send errors to your logging service (Sentry, Datadog, etc.) or show a toast to the user:

```tsx
<Search
  value={value}
  onChange={setValue}
  onSearch={async (address) => {
    // This could fail: network down, API key expired, rate limited
    const coords = await geocodeAddress(address);
    map.flyTo(coords);
  }}
  onClear={() => setValue('')}
  onError={(error, context) => {
    // context: 'change' | 'search' | 'clear' | 'suggestion' | 'render'
    Sentry.captureException(error, { extra: { context } });
    toast.error('Could not find that location, please try again');
  }}
/>
```

The `context` parameter tells you exactly where the error came from:

| Context | When it fires |
|---------|--------------|
| `'search'` | `onSearch` callback threw, or debounced search failed |
| `'change'` | `onChange` callback threw |
| `'clear'` | `onClear` callback threw |
| `'suggestion'` | `onSuggestionSelect` callback threw |
| `'render'` | Component crashed during rendering |

**Without error handling vs with:**

```
Without → API fails → component crashes → user sees blank page
          → you find out from a support ticket 3 days later

With    → API fails → component catches it → user sees your toast
          → onError fires → Sentry alert within seconds
```

### Disabled state

```tsx
<Search
  value="Read only value"
  onChange={() => {}}
  onSearch={() => {}}
  onClear={() => {}}
  disabled
/>
```

---

## Props

### Required

| Prop | Type | Description |
|------|------|-------------|
| `value` | `string` | Current input value. You own this state. |
| `onChange` | `(value: string) => void` | Called on every keystroke. |
| `onSearch` | `(value: string) => void` | Called on submit (button click, Enter key, or after debounce). |
| `onClear` | `() => void` | Called when the clear button is clicked. |

### Optional — Behaviour

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `debounceMs` | `number` | `0` | Milliseconds to wait after typing before calling `onSearch`. Set to `300`–`500` for live search. |
| `disabled` | `boolean` | `false` | Disables the input and all buttons. |
| `placeholder` | `string` | `'Search...'` | Placeholder text. |
| `suggestions` | `string[]` | `[]` | List of suggestion strings shown in a dropdown. |
| `onSuggestionSelect` | `(value: string) => void` | — | Called when the user clicks a suggestion. |
| `onError` | `(error: Error, context: string) => void` | — | Called when any error occurs inside the component or its callbacks. Use to log to Sentry, show toasts, etc. |

### Optional — CSS Class Names

Use these to apply your own CSS classes. Note that default inline styles take precedence — use the style override props below if you need to change specific properties.

| Prop | What it targets |
|------|----------------|
| `className` | Outer container div |
| `inputClassName` | The text input |
| `submitButtonClassName` | Search icon button |
| `clearButtonClassName` | Clear icon button |
| `suggestionsClassName` | Dropdown container |
| `suggestionItemClassName` | Each dropdown item |

### Optional — Style Overrides

These are merged on top of the default styles. Pass only the properties you want to change — defaults apply for everything else.

| Prop | Type | What it targets |
|------|------|----------------|
| `containerStyle` | `CSSProperties` | Outer container div |
| `inputStyle` | `CSSProperties` | The text input |
| `submitButtonStyle` | `CSSProperties` | Search icon button |
| `clearButtonStyle` | `CSSProperties` | Clear icon button |
| `suggestionsStyle` | `CSSProperties` | Dropdown container |
| `suggestionItemStyle` | `CSSProperties` | Each dropdown item |

---

## Default Styles

The component ships with a styled look out of the box:

- **Colour:** `#0077C8` blue for text, icons, borders, and placeholder
- **Clear button:** orange `#ef6c00` with a subtle background tint
- **Search button:** blue tint background, always visible
- **Input:** white background, `14px` font, `font-weight: 300`, `border-radius: 8px`
- **Dropdown:** white background, light shadow, blue text, hover tint

---

## How debounce works

When `debounceMs > 0`, `onSearch` is called automatically after the user stops typing for that many milliseconds. The submit button and Enter key still trigger an immediate search (cancelling any pending debounce).

```
User types "r" → "ra" → "rai" → "rain"
                                        ← 400ms pause → onSearch("rain")
```

---

## Error handling architecture

```
User interaction
      │
      ▼
useSearch hook  ──── try/catch around all callbacks
      │                      │
      │               error caught
      │                      │
      ▼                      ▼
Search renders        onError(error, context)
      │                      │
  ErrorBoundary         your code runs:
  wraps output       - Sentry.captureException()
      │               - toast.error()
  render crash?       - custom logging
      │
      ▼
⚠ Search unavailable
(rest of page intact)
```

---

## TypeScript

All props are fully typed. Import the type directly if needed:

```ts
import type { SearchProps } from '@rain-component-ui/ui-core';
```

---

## Packages

| Package | Description |
|---------|-------------|
| `@rain-component-ui/ui-core` | Shared hook (`useSearch`) and TypeScript types |
| `@rain-component-ui/ui-web` | React / Next.js component |
| `@rain-component-ui/ui-native` | React Native component |

---

## License

MIT