Improvements to the search experience
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 103
- Forks
- 18
- Avg merge
- 1h 19m
- Merged PRs (30d)
- 555
Description
👋 Saw your X post and wanted to share some suggestions for improving search.
Current State
Based on this commit, it looks like you currently:
- Have various places that are passing functions of props into components related to search
- Kick off a search by setting and then updating local react state
- This calls tRPC to fetch new data based on the search value
- A callback function eventually calls
router.pushto update the URL
There's an opportunity to simplify and consolidate this logic, while improving the UX. Right now, you're mostly using client components and tRPC. While there's nothing wrong with this, you could centralize searching into one component, which triggers a server component to fetch new data when the URL state changes.
Desired State
I believe the UX you want is:
- Users can instantly type into an input and start searching
- Searching starts immediately – you do not need to wait until enter is pressed / form is submitted
- When a search is pending, show some visual feedback to the user
- Allow for multiple searches / permutations to happen, without showing visual jank from entering/exiting search state
- Indicate to the user searching is happening with some animation on the containing element / inline spinner
- Don't block the UI or drop frames when trying into the input quickly
Proposed Solution
Here's one option. It's using a new feature of Next.js (on canary) but it's not required (you could use a normal <form>).
- The
<Search>component gets the initial search value fromsearchParamsfrom a Server Component above - This value is stored into local state, as well as a deferred value
- When there are changes in the input, the local state is updated and the form is submitted
- Submitting the form updates the URL state with the
?search=query parameter (replaced, not pushed onto the stack) - This fires a React transition to the new URL, which means
useFormStatusispending - This now shows a loading spinner inline for the search input, but also adds an attribute to the DOM element
- In other parts of our codebase, we can use CSS to look for that element and conditionally add an animation
- This also includes a
useEffectto focus the input on mount (optional)
'use client';
import { useEffect, useRef, useState, useDeferredValue } from 'react';
import { useFormStatus } from 'react-dom';
import Form from 'next/form';
import { SearchIcon } from 'lucide-react';
import { Input } from '@/components/ui/input';
export function Search({ query: initialQuery }: { query: string }) {
let [query, setQuery] = useState(initialQuery);
let deferredQuery = useDeferredValue(query);
let inputRef = useRef<HTMLInputElement>(null);
let formRef = useRef<HTMLFormElement>(null);
let isStale = query !== deferredQuery;
useEffect(() => {
if (inputRef.current && document.activeElement !== inputRef.current) {
inputRef.current.focus();
inputRef.current.setSelectionRange(
inputRef.current.value.length,
inputRef.current.value.length
);
}
}, []);
function handleInputChange(e: React.ChangeEvent<HTMLInputElement>) {
setQuery(e.target.value);
formRef.current?.requestSubmit();
}
return (
<Form
ref={formRef}
action="/"
replace
className="relative flex flex-1 flex-shrink-0 w-full"
>
<label htmlFor="search" className="sr-only">
Search
</label>
<SearchIcon className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground pointer-events-none" />
<Input
ref={inputRef}
onChange={handleInputChange}
type="text"
name="search"
id="search"
placeholder="Search..."
value={query}
className="w-full rounded-none border-0 px-10 py-6 m-1 focus-visible:ring-0 text-base md:text-sm"
/>
<LoadingIcon isStale={isStale} />
</Form>
);
}
function LoadingIcon({ isStale }: { isStale: boolean }) {
let { pending } = useFormStatus();
let loading = pending || isStale;
return loading ? (
<div
data-pending={loading ? '' : undefined}
className="absolute right-3 top-1/2 -translate-y-1/2"
>
<div
className="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent"
role="status"
>
<span className="sr-only">Loading...</span>
</div>
</div>
) : null;
}
This likely isn't perfect yet but it's closer in the direction you're looking for. Happy to help out. I'm trying to build a similar example of searching here: https://next-books-search.vercel.app
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the search-related components described in commit f8a0ccf2f2eed17bb897529215316a6670b5653c and compare their current local state, tRPC fetching, and router.push flow. Use the proposed Search component as a direction, then verify that typing updates the URL immediately, pending searches show feedback, repeated searches remain responsive, and the containing element can reflect pending state.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- next.js, react, typescript
- Domain
- frontend, search, web-dev
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100