Boost React Performance with 4 Powerful Custom Hooks

Enhance your React app with these 4 custom hooks designed to improve performance, productivity, and the user experience.

#react
#javascript
#hooks

After 9 months since the first stable release, React Hooks have changed the way frontend developers write React components. It’s a normal step in the evolution process of libraries like this, the introduction of new features decides which libraries and frameworks survive over others in the continuous race of frontend development. React was already a highly regarded frontend library, but with the React Hooks introduction, it made a big step.

Why React Hooks?

They help developers to keep control of side effects for our functional components without the need for using a class approach and lifecycle methods such as componentDidMount, componentDidUpdate, etc. Furthermore, they allow developers to share logic across components, reducing code duplication and human errors.

React has built-in hooks that provide functionality around the component control process, but the most amazing feature is the ability to combine them to create custom hooks and the ability for hooks to interact with the browser’s API.

Numerous libraries provide custom hooks, and some of those may handle the functionality we’re creating here and in a better way. However, it’s always good to see what happens behind the scene of functions that you already use every day! Let’s write our custom hooks!

usePrefetch

The usePrefetch hook can help you improve the loading time of the main app page by lazy loading other secondary components that don’t need to render on the first view. The goal is to reduce bundle size and make your application respond quicker.

For our example, let’s assume that we have a page with some information and a button that opens a modal. Before we press the button, the modal shouldn’t render and we don’t need it yet. But if we import it at the beginning of the main component, we will need more time to download its JavaScript code because it will be included in the original bundle. What we want to do is lazy load the modal component to split its code from the main component and prefetch it when the main component has been rendered the first time.

jsx
// usePrefetch hook
function usePrefetch(factory) {
const [component, setComponent] = useState(null);

useEffect(() => {
factory();
const comp = lazy(factory);
setComponent(comp);
}, [factory]);
return component;
}

const importModal = () => import('./Modal');

// Usage into a main component
function App(props) {
const [isShown, setIsShown] = useState(false);
const Modal = usePrefetch(importModal);
return (
<div>
<h1>This is part of the first build</h1>
<button onclick={() => setIsShown(true)}>Show Modal</button>
<Suspense fallback={<h1>Waiting for...</h1>}>{isShown && <Modal />}</Suspense>
</div>
);
}

Q.A. Why do we define an **importModal** function? Because if we write the import inline, it will happen every time a new function is called, and it will run the useEffect inside the hooks on every render.

useGeo

This hook gets the current position and the updated value whenever the user moves:

jsx
// useGeo hook
function useGeo(opts) {
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
const [geo, setGeo] = useState({});
let isLoad = true;
let id;

function onSuccess(event) {
if (isLoad) {
setIsLoading(false);
setGeo(event.coords);
}
}

function onFailure(error) {
if (isLoad) {
setIsLoading(false);
setError(error);
}
}

useEffect(() => {
navigator.geolocation.getCurrentPosition(onSuccess, onFailure, opts);
id = navigator.geolocation.watchPosition(onSuccess, onFailure, opts);

return () => {
isLoad = false;
navigator.geolocation.clearwatch(id);
};
}, [opts]);
return { geo, isLoading, error };
}

// Usage of useGeo
function App(props) {
const { geo, isLoading, error } = useGeo();
return !isLoading && !error ? (
<div>
<h1>Lat: {geo.latitude}</h1>
<h1>Lng: {geo.longitude}</h1>
</div>
) : null;
}

Q.A. Why do we define an **isLoad** variable? Since getting the position is an async operation, it is possible that your component re-renders. So we run the cleanup function before it effectively retrieves the location. With this workaround, we can prevent running the onSuccess or onFailure handlers if the component has been unmounted too soon.

useInterval

The useInterval effect is probably the best-known custom React hook, but here I’ll show its implementation by Dan Abramov since we’ll use it for the next hook:

jsx
// useInterval hook
function useInterval(callback, delay) {
const savedCallback = useRef();

useEffect(() => {
savedCallback.current = callback;
}, [callback]);

useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
// Usage of useInterval
function Counter() {
let [count, setCount] = useState(0);

useInterval(() => {
setCount(count + 1);
}, 1000);
return <h1>{count}</h1>;
}

useTimer

Occasionally developers face the need to implement an efficient way to get a countdown timer, so let’s create a countdown timer starting from a timestamp that represents the amount of time we want to countdown in milliseconds:

jsx
// useTimer Hook
function useTimer(timestamp) {
function decreaseSecond(state) {
let { dd, hh, mm, ss } = state;
if (ss > 0) ss--;
else if (mm > 0) {
ss = 59;
mm--;
} else if (hh > 0) {
ss = 59;
mm = 59;
hh--;
} else if (dd > 0) {
ss = 59;
mm = 59;
hh = 23;
dd--;
} else {
return state;
}
return { dd, hh, mm, ss };
}

function reducer(state, action) {
switch (action.type) {
case 'SET_TIME':
return action.timeState;
case 'DECREASE SECOND':
return decreaseSecond(state);
}
}

const initialState = {
dd: 0,
hh: 0,
mm: 0,
ss: 0
};
const [timer, dispatch] = useReducer(reducer, initialState);

useEffect(() => {
const timeDate = new Date(timestamp);

const timeState = {
dd: Math.floor(timeDate / (1000 * 60 * 60 * 24)),
hh: Math.floor((timeDate % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)),
mm: Math.floor((timeDate % (1000 * 60 * 60)) / (1000 * 60)),
ss: Math.floor((timeDate % (1000 * 60)) / 1000)
};

dispatch({ type: 'SET_TIME', timeState });
}, [timestamp]);

useInterval(() => {
dispatch({ type: 'DECREASE_SECOND' });
}, 1000);

return timer;
}
// Usage of useTimer
function App() {
const timestamp = 86400000;
const { dd, hh, mm, ss } = useTimer(timestamp);

return (
<h1>
{dd}:{hh}:{mm}:{ss}
</h1>
);
}

Last updated: