I used React wrong for years, upgrade your code with React Hooks.
Use React Hooks to decouple your code from presentation and logic to achieve higher reusability and maintainability.
I have been working with React for several years now and have optimized my programming skills in this framework over time. When I first started, I didn’t really understand the concept of hooks and got used to ignoring them. In the meantime, I changed my mind and this article explains why you should use hooks.
To give you some context, we have a presentation component called LoadingButton, it is a simple button that can switch its state from loading (showing a progress circle) to a normal button. In our scenario, we want to execute an async function, that sets the state to loading during the execution. To simplify the state handling we want to create an additional small helper utility that does the handling for us.
⛔️ The wrong way
To achieve this I would have wrapped the component with a higher-level logic component that does the controlling for me, taking care of the execution, handling the loading state, and sending a complete or failure event once the execution is completed. The usage of the component would look like this:
export function TheWrongWay(){
const handleClick = () => {
return doSomethign.promise
}
const handleSuccess = () => {
alert("succeeded")
}
const handleFailure = () => {
alert("error")
}
return <ActionButton
onClick={handleClick}
onSuccess={handleComplete}
onFailure={handleFailure} />
}
The implemented logic component called “ActionButton” wraps the “LoadingButton” component.
export type ActionButtonProps<T> = Omit<LoadingButtonProps,'onClick'> & {
onClick?: ()=>Promise<T>,
onSuccess?: (result : T)=>void,
onFailure?: (error : any)=>void,
}
export default function ActionButton<T>(props : ActionButtonProps<T>) {
const {onClick, onSuccess, onFailure, ...forwardedProps} = props;
const [loading, setLoading] = useState<boolean>(false)
const handleClick = () => {
if(onClick){
setLoading(true)
onClick()
.then(onSuccess)
.catch(onFailure)
.finally(()=>setLoading(false))
}
}
return <LoadingButton
loading={loading}
onClick={handleClick}
{...forwardedProps} >
{props.children}
</LoadingButton>
}
✅ The right way
Nowadays I do it the following way, instead of wrapping the component I create a hook that contains the logic and is controlling the state.
export function TheRightWay(){
const action = useAsyncAction({
onExecute: ()=> doSomethign.promise,
onSuccess: ()=> alert("succeeded")
onFailure: ()=> alert("error")
})
return <LoadingButton onClick={action.execute}
loading={action.isExecuting} />
}
The “useAsyncAction” hook that is controlling the async execution.
import {useState} from "react";
export type ActionProps<R, I = undefined> = {
onExecute: (input?: I)=>Promise<R>,
onSucceed?: (result : R)=>void,
onFailure?: (error : any)=>void
}
export type ActionResult<R, I = undefined> = {
isExecuting: boolean,
result?: R,
error?: any
execute: (input?: I)=>void,
}
export default function useAsyncAction<R, I = undefined>(config: ActionProps<R,I>) : ActionResult<R,I> {
const [isExecuting, setIsExecuting] = useState<boolean>(false)
const [error, setError] = useState<any>();
const [result, setResult] = useState<R>();
const handleExecute = (input?: I) => {
setIsExecuting(true)
setError(undefined)
setResult(undefined)
config.onExecute(input)
.then((result: R)=>{
setResult(result)
if(config.onSucceed) config.onSucceed(result)
})
.catch((error)=>{
setError(error);
if(config.onFailure) config.onFailure(error)
})
.finally(()=>setIsExecuting(false))
}
return {
isExecuting,
result,
error,
execute: handleExecute
}
}
😮 The why — Advantages
It not only looks more beautiful, but it also gives some great advantages to improve the overall code of the application.
Decoupling
In the first example, I was completely dependent on the lower-level “LoadingButton” component making a replacement more difficult. With the separation, I am more flexible in comparing it with other functionalities.
Reusability
With the first example, it is also not possible to share the code with another component. In the new example, I can use the hook for every component I want, making it great for reusability.
Hook stacking & further logic decoupling
Another great advantage is that it lets you further separate your logic by stacking hooks inside each other making the code extremely clean and maintainable (I prefer to put my entire logic inside a “controller” hook). To give you a short example:
export function useCopyOperation(){
const action = useAsyncAction({
onExecute: ()=> doCopy.promise,
onSuccess: ()=> alert("succeeded")
onFailure: ()=> alert("Error")
})
return action;
}
Conclusion
Use Hooks! Hooks are great and can help you decouple your code and achieve higher reusability in your application.
Read also about the importance of the user interface and user experience as a whole.