I used React wrong for years, upgrade your code with React Hooks.

Davide Wietlisbach
3 min readAug 4, 2023

--

Use React Hooks to decouple your code from presentation and logic to achieve higher reusability and maintainability.

Photo by Lautaro Andreani on Unsplash

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.

--

--

Davide Wietlisbach

Writing about business, economy, and technology. I believe those three topics together are responsible for exciting changes in the future ahead.