I have a loader for path /items which fetches a lot of data from API, so I use a deferred loader. I want to conditionally render the <Supense><Await> code only when there are no search query params (meaning initial fetch from the server), but then to use the already fetched items and filter them based on the search query parameters.
/items-> fetch from API for 3 seconds- submit a form (e.g. search input) which navigates with query params (e.g.
search=test) /items?search=test-> filters the already fetched items based on the query params withuseSearchParams
- Issue: it re-renders the component, goes through the
<Suspense><Await>again
What is the best approach with such case? I tried using useState, Location State (from <Form state>), but it is difficult to go around not rendering the <Await> in order to use the already fetched data.
Am I missing a valuable pattern that can be used in such scenario? It seems too common to not have an already established pattern
Update - a quick code example using Form state and Location for initially displaying items fetched from API vs locally saved items filtered based on a query param (search):
Loader function: const loader= (() => defer({ itemsAsync: getItems() })) satisfies LoaderFunction; // getItems() is Promise
Component:
const { itemsAsync } = useLoaderData();
const { state: { savedItems } } = useLocation();
const [searchParams] = useSearchParams();
const searchQueryParam = searchParams.get('search')?.toLowerCase() ?? '';
const filterItems = (items: Item[]) => items.filter((item) => item.content.toLowerCase().includes(searchQueryParam));
return state ? (
<div>
<Form state={{savedItems: items}}>
<input type="text" name="search" />
<input type="submit" hidden />
</Form>
{filterItems(state?.items).map((item) => <div>{item}</div>)}
</div>
) : (
<Suspense fallback={<div>Loading items...</div>}>
<Await resolve={itemsAsync}>
{(items) => (
<div>
<Form state={{savedItems: items}}>
<input type="text" name="search" />
<input type="submit" hidden />
</Form>
{items.map((item) => <div>{item}</div>)}
</div>
)}
</Await>
</Suspense>
);
On submitting the Form it simply adds search as a query param /items?search=test then based on the test we should filter the list and display a subset of the fetched items. This is not working for many reasons - for example the search query param is only added in the URL at the very end of the loading process, in this case it fetches new items from the API (e.g. 5 seconds delay) and only then it updates the actual URL. I know that I can also get the params from the loader's request, but then I will need to return non-deferred data based on if there are params or not? There must be a simpler way to deal with "caching" data while using query params and I am reinventing the wheel right now.
As I already mentioned the ideal flow in my head would be:
/items-> fetch from API (slow request ~3 seconds)- submit a form (e.g. search input) which navigates with query params (e.g.
search=test) /items?search=test-> filters the already fetched items based on the query params instead of fetching the same items again and then filtering
I might be wrong here but I don't think we can differentiate between routes which differ only by search params on router level.
Hence I'd try to solve it in loader directly and make expensive fetch conditional. Loader function parameter has two properties params and request. You can convert request.url to an URL object and check if searchParams is non-empty. If it is, you can bail out or use some buffered value.