When it comes to creating a smooth user experience, even small details can make a big difference. Imagine using a website where every click feels responsive and every interaction smooth. But sometimes, things can feel too quick, making you wonder if your action was registered at all.
Submit Button
Try clicking this button
What do you think? Fast, right? But kind of janky. When users see that, their first thought probably isn’t “Wow, that’s fast,” but more like, “Did it run? Is it submitted?” It gets even worse if there’s no other notification, like a toast message or a completed page, to confirm the action.
Let’s jump to the code for that button:
const Component = () => {
const [isLoading, setIsLoading] = useState(false);
return (
<button
onClick={async () => {
setIsLoading(true);
await something();
setIsLoading(false);
}}
>
{isLoading ? <Spinner /> : 'Submit'}
</button>
);
};
Here is the button again but with some stats.
Can you see anything wrong with it? Nope, it’s perfectly functional. But that doesn’t mean there’s nothing to improve. One tweak we can make is to intentionally slow down the loading a bit.
const sleep = async (value: number) => {
return await new Promise((resolve) => setTimeout(resolve, value));
};
const Component = () => {
const [isLoading, setIsLoading] = useState(false);
return (
<button
onClick={async () => {
setIsLoading(true);
await something();
await Promise.all([something(), sleep(500)]);
setIsLoading(false);
}}
>
{isLoading ? <Spinner /> : 'Submit'}
</button>
);
};
I made 2 changes:
- Now there is a
sleep
function, which is just a small function that resolves after a certain amount of milliseconds. - I also wrapped
something()
together withsleep(500)
using Promise.all
Now what’s going to happen? If something
takes 200ms to complete, the <Spinner />
will still show up because sleep
is still going for another 300ms.
Let’s see it in action
Now it’s so much better. It will be more visible to the user that the submit is running, and they won’t question whether it is registered or not again.
Tree View
Next, let’s look at another example with a tree view:
Here is the code for the <Folder />
component
const Folder = ({ folder }) => {
const [isOpen, setIsOpen] = useState(false);
const { get, isLoading } = useData(folder.id);
return (
<div
onClick={async () => {
const v = !isOpen;
setIsOpen(v);
if (v) await get();
}}
>
<ChevronIcon />
{isLoading ? <Spinner /> : isOpen ? <FolderOpenIcon /> : <FolderIcon />}
{folder.name}
</div>
);
};
Notice how the current implementation feels a bit too quick, with the spinner showing up and disappearing almost instantly for some folders. Now, let’s look at the improved version where I delay the spinner for 150ms.
See? Much better, right? By slowing down the spinner, it removes the jankiness, and the overall experience feels more seamless and immediate.
Here’s how I made this improvement:
-
I added a useDebounce function to handle debouncing. This ensures that the value is only updated after it has remained unchanged for the entire delay period, even if it changes frequently.
-
I wrapped the
isLoading
state withuseDebounce
so that the<Spinner />
only appears after a short delay. This way, the spinner doesn’t show up instantly with every brief change in theisLoading
state, resulting in a smoother and less jarring user experience.
const Folder = ({ folder }) => {
const [isOpen, setIsOpen] = useState(false);
const { get, isLoading } = useData(folder.id);
const debouncedIsLoading = useDebounce(isLoading, 150);
return (
<div
onClick={async () => {
const v = !isOpen;
setIsOpen(v);
if (v) await get();
}}
>
<ChevronIcon />
{isLoading ? <Spinner /> : isOpen ? <FolderOpenIcon /> : <FolderIcon />}
{debouncedIsLoading ? <Spinner /> : isOpen ? <FolderOpenIcon /> : <FolderIcon />}
{folder.name}
</div>
);
};
By adding this debounce, we ensure that the <Spinner />
doesn’t flash too quickly, providing a more pleasant experience for the user.
Here is the <TreeView />
again with the delay control so you can see the effect of different values. Experiment with the delay values to find the perfect balance for your application.
By making these small adjustments, we can significantly enhance the user experience, making interactions feel more intuitive and less jarring. Remember, speed is important, but clarity and user comfort are equally crucial.