Preserve value for a dropdown
-- 2 min read
We use dropdowns all the time to filter views in our UIs e.g to sort from oldest to newest or newest to oldest. In order to provide the best possible user experience, the values that the user has selected should be persisted and reapplied in case of any disruptions.
In this article we want to see how we can create the following user experience:
Implementation
The dropdown in the example above persists its value even when the page refreshes; let's say due to bad network conditions. We'll be using Remix framework to build this example.
Let's start by creating the route to display the withdrawals
// /withdrawals
import { Form, useLoaderData } from "@remix-run/react";
export async function loader() {
let withdrawals = await getWithdrawals();
return withdrawals;
}
export default function Withdrawals() {
let withdrawals = useLoaderData();
return (
<main>
<h1>Withdrawals</h1>
{/* Dropdown to filter by the approval status */}
<Form>
<select>
<option>All</option>
<option>Approved</option>
<option>Unapproved</option>
</select>
</Form>
<div>
{/* Show the list of withdrawals */}
{withdrawals.map( withdrawal ) => (<Withdrawal key={withdrawal.id} item={withdrawal} />)}
</div>
</main>
);
}
This page shows a list of withdrawals. Ideally the list shown should be in sync with the value that the user selected in the dropdown.
How do we ensure the value in the dropdown (<select>) is in sync with the server?
The first step to achieving this is to submit the value of the <select> whenever a user selects something. We'll do this by submitting the form anytime there is a 'change' event.
// /withdrawals
import { Form, useSubmit } from "@remix-run/react";
export default function Withdrawals() {
let submit = useSubmit();
return (
<main>
<Form
onChange={(event) => submit(event.currentTarget, { replace: true })}
>
<select name="status">
<option value="all">All</option>
<option value="approved">Approved</option>
<option value="unapproved">Unapproved</option>
</select>
</Form>
</main>
);
}
We are using the useSubmit hook to submit the form whenever the value in the <select> changes. This value will be accessed from the loader because it was submitted using a 'GET' request.
// /withdrawals
export async function loader({ request }) {
let withdrawals = await getWithdrawals();
// Get the value submitted using URLSearchParams
let searchParams = new URL(request.url).searchParams;
let status = searchParams.get("status");
return withdrawals;
}
Now we can use this value to filter the withdrawals by status and return the filtered data to our UI.
// /withdrawals
export async function loader({ request }) {
let withdrawals = await getWithdrawals();
// Get the value submitted using URLSearchParams
let searchParams = new URL(request.url).searchParams;
let status = searchParams.get("status");
let filteredData = withdrawals;
if (status) {
if (status === "approved") {
filteredData = withdrawals.filter((item) => item.is_approved);
} else if (status === "unapproved") {
filteredData = withdrawals.filter((item) => !item.is_approved);
}
}
return filteredData;
}
Our UI now displays the filtered withdrawals from the server.
// /withdrawals
export default function Withdrawals() {
let withdrawals = useLoaderData();
return (
<div>
{/* Show the list of withdrawals */}
{withdrawals.map( withdrawal ) => (<Withdrawal key={withdrawal.id} item={withdrawal} />)}
</div>
);
}
Finally, let's set the default value of the <select> if the user had selected something. This value will be used in case the page refreshes. This value is obtained from the URL as search params. Remix offers useSearchParams hook for interacting with the search params from your UI component.
// /withdrawals
import { Form, useSubmit, useSearchParams } from "@remix-run/react";
export default function Withdrawals() {
let submit = useSubmit();
// Use the value from the search params
let [searchParams] = useSearchParams();
let status = searchParams.get("status");
return (
<main>
<Form
onChange={(event) => submit(event.currentTarget, { replace: true })}
>
<select name="status" defaultValue={status || undefined}>
<option value="all">All</option>
<option value="approved">Approved</option>
<option value="unapproved">Unapproved</option>
</select>
</Form>
</main>
);
}
Now our filtering experience is smooth and resilient.
That's it 👋🏾; that's all it takes.