Frontend Development¶
The OneSearch frontend is a React 18 + TypeScript single-page application.
Getting Started¶
Prerequisites¶
- Node.js 18 or later
- npm (comes with Node.js)
Initial Setup¶
Install dependencies:
Start Development Server¶
The dev server runs at http://localhost:5173 and proxies API requests to the backend at http://localhost:8000.
Make sure the backend is running separately for full functionality.
Project Structure¶
frontend/
├── src/
│ ├── main.tsx # Entry point (renders App)
│ ├── App.tsx # Router setup + TanStack Query provider
│ ├── pages/ # Page components
│ │ ├── SearchPage.tsx # Main search (/)
│ │ ├── DocumentPage.tsx # Document preview (/document/:id)
│ │ └── admin/
│ │ ├── SourcesPage.tsx # Source management
│ │ └── StatusPage.tsx # Indexing status
│ ├── components/ # Reusable components
│ │ ├── SearchBox.tsx
│ │ ├── FilterPanel.tsx
│ │ ├── ResultCard.tsx
│ │ ├── SourceForm.tsx
│ │ ├── SourceTable.tsx
│ │ └── ui/ # shadcn/ui components
│ ├── lib/ # Utilities
│ │ ├── api.ts # API client functions
│ │ └── utils.ts # Helper functions
│ ├── types/ # TypeScript types
│ │ └── api.ts # API interfaces
│ └── index.css # Global styles (Tailwind)
├── public/ # Static assets
├── package.json
├── tsconfig.json # TypeScript config
├── vite.config.ts # Vite config
└── tailwind.config.js # Tailwind config
Tech Stack¶
React 18 - UI library with hooks and functional components
TypeScript - Type safety catches bugs early
Vite - Fast dev server and build tool
TanStack Query (React Query) - Server state management, caching, refetching
React Router - Client-side routing
shadcn/ui - Accessible UI components (copied into project, not npm deps)
Tailwind CSS - Utility-first styling
Lucide React - Icon library
Development Workflow¶
Making Changes¶
-
Create a feature branch:
-
Make your changes - Edit files in
src/ -
Check for errors:
-
Commit and push:
-
Create a pull request
Adding Dependencies¶
Always commit package-lock.json after adding dependencies.
Key Concepts¶
State Management¶
OneSearch uses TanStack Query for server state (search results, sources, status) and React hooks for local UI state.
Server state example:
import { useQuery } from '@tanstack/react-query';
import { fetchSources } from '@/lib/api';
function SourcesPage() {
const { data, isLoading, error } = useQuery({
queryKey: ['sources'],
queryFn: fetchSources,
refetchInterval: 30000 // Auto-refresh every 30s
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <SourceTable sources={data} />;
}
Local state example:
function SearchBox() {
const [query, setQuery] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// Do something with query
};
return (
<form onSubmit={handleSubmit}>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
</form>
);
}
TanStack Query handles caching, refetching, and loading states automatically. No need for Redux or Zustand.
API Client¶
API calls live in src/lib/api.ts. Each function wraps fetch:
export async function fetchSources(): Promise<Source[]> {
const response = await fetch('/api/sources');
if (!response.ok) {
throw new Error('Failed to fetch sources');
}
return response.json();
}
export async function createSource(source: SourceCreate): Promise<Source> {
const response = await fetch('/api/sources', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(source),
});
if (!response.ok) {
throw new Error('Failed to create source');
}
return response.json();
}
Mutations for writes:
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { createSource } from '@/lib/api';
function SourceForm() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: createSource,
onSuccess: () => {
// Invalidate sources query to trigger refetch
queryClient.invalidateQueries({ queryKey: ['sources'] });
},
});
const handleSubmit = (data: SourceCreate) => {
mutation.mutate(data);
};
return <form onSubmit={handleSubmit}>...</form>;
}
Routing¶
React Router handles navigation:
// App.tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<SearchPage />} />
<Route path="/document/:id" element={<DocumentPage />} />
<Route path="/admin">
<Route path="sources" element={<SourcesPage />} />
<Route path="status" element={<StatusPage />} />
</Route>
</Routes>
</BrowserRouter>
);
}
Navigate programmatically:
import { useNavigate } from 'react-router-dom';
function ResultCard({ result }) {
const navigate = useNavigate();
return (
<div onClick={() => navigate(`/document/${result.id}`)}>
{result.title}
</div>
);
}
Components¶
Components are functional with hooks. Keep them small and focused.
Good component:
interface SearchBoxProps {
onSearch: (query: string) => void;
}
function SearchBox({ onSearch }: SearchBoxProps) {
const [query, setQuery] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSearch(query);
};
return (
<form onSubmit={handleSubmit}>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
<button type="submit">Search</button>
</form>
);
}
Extract logic into custom hooks:
function useDebounce(value: string, delay: number) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// Use it
function SearchPage() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
// Search with debouncedQuery
}
Styling¶
OneSearch uses Tailwind CSS for styling:
function Button({ children, onClick }) {
return (
<button
onClick={onClick}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
{children}
</button>
);
}
shadcn/ui components provide consistent styling:
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
function Form() {
return (
<div>
<Input placeholder="Enter name..." />
<Button>Submit</Button>
</div>
);
}
These components are copied into your project (not npm dependencies), so you can customize them.
Building for Production¶
Build the production bundle:
Output goes to dist/. This is what gets deployed in the Docker image.
Preview the production build locally:
Common Tasks¶
Adding a New Page¶
- Create component in
src/pages/ - Add route in
App.tsx - Add navigation link if needed
Example:
// pages/SettingsPage.tsx
export function SettingsPage() {
return <div>Settings</div>;
}
// App.tsx
<Route path="/settings" element={<SettingsPage />} />
Adding a New API Endpoint¶
- Define TypeScript types in
src/types/api.ts - Add API function in
src/lib/api.ts - Use with TanStack Query in components
Adding a shadcn/ui Component¶
This copies the component into src/components/ui/.
TypeScript Tips¶
Always define types for props:
interface CardProps {
title: string;
onClick?: () => void;
}
function Card({ title, onClick }: CardProps) {
// ...
}
Use interfaces from src/types/api.ts for API data:
import { Source, SearchResult } from '@/types/api';
function ResultList({ results }: { results: SearchResult[] }) {
// ...
}
Let TypeScript infer when obvious:
// Good - type is obvious
const [count, setCount] = useState(0);
// Unnecessary
const [count, setCount] = useState<number>(0);
Debugging¶
React DevTools - Install the browser extension to inspect component state.
Console logging:
Network tab - Check API requests in browser DevTools.
TanStack Query DevTools:
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
function App() {
return (
<>
{/* Your app */}
<ReactQueryDevtools />
</>
);
}
Shows query status, cache, and refetches in the browser.
Code Style¶
Formatting - ESLint and Prettier are configured. Run:
Component naming - PascalCase for components, camelCase for functions:
File naming - PascalCase for component files: SearchPage.tsx, ResultCard.tsx
Performance Tips¶
Memoize expensive computations:
import { useMemo } from 'react';
function ResultList({ results }) {
const sortedResults = useMemo(
() => results.sort((a, b) => b.score - a.score),
[results]
);
return <div>{sortedResults.map(...)}</div>;
}
Lazy load routes:
import { lazy, Suspense } from 'react';
const SourcesPage = lazy(() => import('./pages/admin/SourcesPage'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Route path="/admin/sources" element={<SourcesPage />} />
</Suspense>
);
}
Debounce search input - Already implemented in SearchPage.
Troubleshooting¶
Build errors¶
Clear cache and reinstall:
TypeScript errors¶
Check types:
Vite dev server issues¶
Restart the server:
API proxy not working¶
Check vite.config.ts:
Make sure backend is running on port 8000.
Next Steps¶
- Architecture - Understand the overall system
- Backend Development - Work on the backend
- Contributing - Contribution guidelines