As web developers, we all know the importance of buttons in our applications. Whether it's submitting a form, triggering an action, or making an API call, buttons play a crucial role in driving user interactions. However, when it comes to adding asynchronous code to button clicks, such as performing an API call and waiting for the result, we tend to get lazy, or forget to add it.
The problem for users
Have you ever encountered a situation where the button appeared stuck or unresponsive while waiting for the asynchronous operation to complete? It happens to the best of us. In the heat of coding, it's easy to forget to add the necessary loading state to our buttons, leaving users confused and frustrated.
In this article, we'll explore a simple yet powerful approach to enhance button functionality, specifically focusing on streamlining the loading state. Although we'll be using React as our example framework, these techniques can be applied to any web development framework.
Adding Loading State to Buttons
Let's begin by understanding the typical workflow involved in adding asynchronous, continuously running code to a button click:
- The user clicks the button, initiating the action.
- We set the loading state to true, indicating that the button is in progress.
- We start performing the asynchronous code, such as making an API call.
- Once the operation completes, we update the loading state to false.
However, every time we need to incorporate this functionality, we find ourselves writing repetitive boilerplate code. Moreover, overlooking the loading state can lead to poor user experience.
Simplifying Button Loading
To address these challenges and make the button loading experience easier to handle, we need an intuitive API and less boilerplate code.
Let's take a look at a typical button code snippet in React:
import { Button } from 'Button';
const APIButton = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const handleClick = () => {
setLoading(true);
setError('');
submitFormToBackend()
.then(() => {
setLoading(false);
})
.catch((err) => {
setError(err);
});
}
return (
<>
<Button
onClick={handleClick}
className={`${loading && 'btn-loading'} ${error && 'btn-error'}`}
disabled={loading}
>
Label
</Button>
{error && <div>{error}</div>}
</>
);
}
This code demonstrates a common pattern where we manually manage the loading and error states while handling the button click. However, it seems like too much boilerplate (I am lazy to write a few extra key-strokes).
Streamlining Button Loading with an Enhanced Component
To make the button loading state easier to manage, we can modify the button component itself. The goal is not to clutter our button code but to complement it in a way that still keeps our code readable and maintainable.
Imagine treating the button as an input field with disabled, busy, and error states.
Let's enhance the button component to handle loading internally and simplify our code:
interface ButtonProps {
loading?: boolean;
disabled?: boolean;
onClick?: (e: MouseEvent<HTMLButtonElement>) => (void | Promise<void>);
}
export const Button: FunctionComponent<ButtonProps> = (props) => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
const clickResult = props.onClick?.(e);
// If our onClick returns a Promise, handle the loading states
if (clickResult instanceof Promise) {
setLoading(true);
setError('');
clickResult
.catch((err) => {
if (err.status === 403) {
setError('Unauthorized');
}
})
.finally(() => {
setLoading(false);
});
}
}
const buttonClasses = classnames('btn', {
'btn-loading': loading,
'btn-error': !!error
});
let buttonEl = (
<button
disabled={props.disabled || loading}
className={buttonClasses}
role={loading ? "progressbar" : ""}
>
{props.children}
</button>
);
if (error) {
buttonEl = (
<Tooltip content={error} color="danger">
{buttonEl}
</Tooltip>
);
}
return buttonEl;
}
With this enhanced button component, we have a more powerful and intuitive way of managing loading and error states. The component takes care of the loading state internally, allowing us to focus on writing cleaner and more concise code.
Implementing the Supercharged Button
Now, let's see how our new and improved button can simplify our code:
import { Button } from 'Button';
const APIButton = () => {
const handleClick = () => {
return submitFormToBackend()
.catch((err) => {
if (err.status === 403) {
throw new Error('Unauthorized');
}
});
};
return (
<Button onClick={handleClick}>
Label
</Button>
);
}
By simply returning the promise from our handleClick
function, we can effortlessly incorporate loading and error states. It's a single additional word that significantly simplifies our code and improves the user experience.
Taking Control of Loading and Error States
The promisified Button component we've built empowers us to handle different scenarios effectively:
- If we don't want to show the loading state without a promise, we can simply omit the promise return from the handleClick function.
- If we want to display the loading state but without an error tooltip, we can return the promise but catch the error, preventing the Button component from catching them.
Conclusion
In this article, we've explored how to supercharge our buttons by streamlining the loading state. By enhancing our button component, we can simplify the process, reduce boilerplate code, and provide a smoother user experience.
Lets contiue the discussion:
- Is there any issues that you can find with this approach?
- Let me know about your favorite mini-hacks that have eased your development process.
Top comments (4)
This looks cool! Any demo?
I'll add the demo shortly
Thanks!
Thanks for sharing it looks good 😉